    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017package org.apache.commons.lang3.time;
    018
    019import java.io.IOException;
    020import java.io.ObjectInputStream;
    021import java.io.Serializable;
    022import java.text.DateFormat;
    023import java.text.DateFormatSymbols;
    024import java.text.FieldPosition;
    025import java.util.ArrayList;
    026import java.util.Calendar;
    027import java.util.Date;
    028import java.util.List;
    029import java.util.Locale;
    030import java.util.TimeZone;
    031import java.util.concurrent.ConcurrentHashMap;
    032import java.util.concurrent.ConcurrentMap;
    033
    034import org.apache.commons.lang3.LocaleUtils;
    035import org.apache.commons.lang3.exception.ExceptionUtils;
    036
    037/**
    038 * <p>FastDatePrinter is a fast and thread-safe version of
    039 * {@link java.text.SimpleDateFormat}.</p>
    040 *
    041 * <p>To obtain a FastDatePrinter, use {@link FastDateFormat#getInstance(String, TimeZone, Locale)}
    042 * or another variation of the factory methods of {@link FastDateFormat}.</p>
    043 *
    044 * <p>Since FastDatePrinter is thread safe, you can use a static member instance:</p>
    045 * <code>
    046 *     private static final DatePrinter DATE_PRINTER = FastDateFormat.getInstance("yyyy-MM-dd");
    047 * </code>
    048 *
    049 * <p>This class can be used as a direct replacement to
    050 * {@code SimpleDateFormat} in most formatting situations.
    051 * This class is especially useful in multi-threaded server environments.
    052 * {@code SimpleDateFormat} is not thread-safe in any JDK version,
    053 * nor will it be as Sun have closed the bug/RFE.
    054 * </p>
    055 *
    056 * <p>Only formatting is supported by this class, but all patterns are compatible with
    057 * SimpleDateFormat (except time zones and some year patterns - see below).</p>
    058 *
    059 * <p>Java 1.4 introduced a new pattern letter, {@code 'Z'}, to represent
    060 * time zones in RFC822 format (eg. {@code +0800} or {@code -1100}).
    061 * This pattern letter can be used here (on all JDK versions).</p>
    062 *
    063 * <p>In addition, the pattern {@code 'ZZ'} has been made to represent
    064 * ISO 8601 extended format time zones (eg. {@code +08:00} or {@code -11:00}).
    065 * This introduces a minor incompatibility with Java 1.4, but at a gain of
    066 * useful functionality.</p>
    067 *
    068 * <p>Starting with JDK7, ISO 8601 support was added using the pattern {@code 'X'}.
    069 * To maintain compatibility, {@code 'ZZ'} will continue to be supported, but using
    070 * one of the {@code 'X'} formats is recommended.
    071 *
    072 * <p>Javadoc cites for the year pattern: <i>For formatting, if the number of
    073 * pattern letters is 2, the year is truncated to 2 digits; otherwise it is
    074 * interpreted as a number.</i> Starting with Java 1.7 a pattern of 'Y' or
    075 * 'YYY' will be formatted as '2003', while it was '03' in former Java
    076 * versions. FastDatePrinter implements the behavior of Java 7.</p>
    077 *
    078 * @since 3.2
    079 * @see FastDateParser
    080 */
    081public class FastDatePrinter implements DatePrinter, Serializable {
    082    // A lot of the speed in this class comes from caching, but some comes
    083    // from the special int to StringBuffer conversion.
    084    //
    085    // The following produces a padded 2 digit number:
    086    //   buffer.append((char)(value / 10 + '0'));
    087    //   buffer.append((char)(value % 10 + '0'));
    088    //
    089    // Note that the fastest append to StringBuffer is a single char (used here).
    090    // Note that Integer.toString() is not called, the conversion is simply
    091    // taking the value and adding (mathematically) the ASCII value for '0'.
    092    // So, don't change this code! It works and is very fast.
    093
    094    /** Empty array. */
    095    private static final Rule[] EMPTY_RULE_ARRAY = new Rule[0];
    096
    097    /**
    098     * Required for serialization support.
    099     *
    100     * @see java.io.Serializable
    101     */
    102    private static final long serialVersionUID = 1L;
    103
    104    /**
    105     * FULL locale dependent date or time style.
    106     */
    107    public static final int FULL = DateFormat.FULL;
    108    /**
    109     * LONG locale dependent date or time style.
    110     */
    111    public static final int LONG = DateFormat.LONG;
    112    /**
    113     * MEDIUM locale dependent date or time style.
    114     */
    115    public static final int MEDIUM = DateFormat.MEDIUM;
    116    /**
    117     * SHORT locale dependent date or time style.
    118     */
    119    public static final int SHORT = DateFormat.SHORT;
    120
    121    /**
    122     * The pattern.
    123     */
    124    private final String mPattern;
    125    /**
    126     * The time zone.
    127     */
    128    private final TimeZone mTimeZone;
    129    /**
    130     * The locale.
    131     */
    132    private final Locale mLocale;
    133    /**
    134     * The parsed rules.
    135     */
    136    private transient Rule[] mRules;
    137    /**
    138     * The estimated maximum length.
    139     */
    140    private transient int mMaxLengthEstimate;
    141
    142    // Constructor
    143    //-----------------------------------------------------------------------
    144    /**
    145     * <p>Constructs a new FastDatePrinter.</p>
    146     * Use {@link FastDateFormat#getInstance(String, TimeZone, Locale)}  or another variation of the
    147     * factory methods of {@link FastDateFormat} to get a cached FastDatePrinter instance.
    148     *
    149     * @param pattern  {@link java.text.SimpleDateFormat} compatible pattern
    150     * @param timeZone  non-null time zone to use
    151     * @param locale  non-null locale to use
    152     * @throws NullPointerException if pattern, timeZone, or locale is null.
    153     */
    154    protected FastDatePrinter(final String pattern, final TimeZone timeZone, final Locale locale) {
    155        mPattern = pattern;
    156        mTimeZone = timeZone;
    157        mLocale = LocaleUtils.toLocale(locale);
    158
    159        init();
    160    }
    161
    162    /**
    163     * <p>Initializes the instance for first use.</p>
    164     */
    165    private void init() {
    166        final List<Rule> rulesList = parsePattern();
    167        mRules = rulesList.toArray(EMPTY_RULE_ARRAY);
    168
    169        int len = 0;
    170        for (int i=mRules.length; --i >= 0; ) {
    171            len += mRules[i].estimateLength();
    172        }
    173
    174        mMaxLengthEstimate = len;
    175    }
    176
    177    // Parse the pattern
    178    //-----------------------------------------------------------------------
    179    /**
    180     * <p>Returns a list of Rules given a pattern.</p>
    181     *
    182     * @return a {@code List} of Rule objects
    183     * @throws IllegalArgumentException if pattern is invalid
    184     */
    185    protected List<Rule> parsePattern() {
    186        final DateFormatSymbols symbols = new DateFormatSymbols(mLocale);
    187        final List<Rule> rules = new ArrayList<>();
    188
    189        final String[] ERAs = symbols.getEras();
    190        final String[] months = symbols.getMonths();
    191        final String[] shortMonths = symbols.getShortMonths();
    192        final String[] weekdays = symbols.getWeekdays();
    193        final String[] shortWeekdays = symbols.getShortWeekdays();
    194        final String[] AmPmStrings = symbols.getAmPmStrings();
    195
    196        final int length = mPattern.length();
    197        final int[] indexRef = new int[1];
    198
    199        for (int i = 0; i < length; i++) {
    200            indexRef[0] = i;
    201            final String token = parseToken(mPattern, indexRef);
    202            i = indexRef[0];
    203
    204            final int tokenLen = token.length();
    205            if (tokenLen == 0) {
    206                break;
    207            }
    208
    209            Rule rule;
    210            final char c = token.charAt(0);
    211
    212            switch (c) {
    213            case 'G': // era designator (text)
    214                rule = new TextField(Calendar.ERA, ERAs);
    215                break;
    216            case 'y': // year (number)
    217            case 'Y': // week year
    218                if (tokenLen == 2) {
    219                    rule = TwoDigitYearField.INSTANCE;
    220                } else {
    221                    rule = selectNumberRule(Calendar.YEAR, Math.max(tokenLen, 4));
    222                }
    223                if (c == 'Y') {
    224                    rule = new WeekYear((NumberRule) rule);
    225                }
    226                break;
    227            case 'M': // month in year (text and number)
    228                if (tokenLen >= 4) {
    229                    rule = new TextField(Calendar.MONTH, months);
    230                } else if (tokenLen == 3) {
    231                    rule = new TextField(Calendar.MONTH, shortMonths);
    232                } else if (tokenLen == 2) {
    233                    rule = TwoDigitMonthField.INSTANCE;
    234                } else {
    235                    rule = UnpaddedMonthField.INSTANCE;
    236                }
    237                break;
    238            case 'd': // day in month (number)
    239                rule = selectNumberRule(Calendar.DAY_OF_MONTH, tokenLen);
    240                break;
    241            case 'h': // hour in am/pm (number, 1..12)
    242                rule = new TwelveHourField(selectNumberRule(Calendar.HOUR, tokenLen));
    243                break;
    244            case 'H': // hour in day (number, 0..23)
    245                rule = selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen);
    246                break;
    247            case 'm': // minute in hour (number)
    248                rule = selectNumberRule(Calendar.MINUTE, tokenLen);
    249                break;
    250            case 's': // second in minute (number)
    251                rule = selectNumberRule(Calendar.SECOND, tokenLen);
    252                break;
    253            case 'S': // millisecond (number)
    254                rule = selectNumberRule(Calendar.MILLISECOND, tokenLen);
    255                break;
    256            case 'E': // day in week (text)
    257                rule = new TextField(Calendar.DAY_OF_WEEK, tokenLen < 4 ? shortWeekdays : weekdays);
    258                break;
    259            case 'u': // day in week (number)
    260                rule = new DayInWeekField(selectNumberRule(Calendar.DAY_OF_WEEK, tokenLen));
    261                break;
    262            case 'D': // day in year (number)
    263                rule = selectNumberRule(Calendar.DAY_OF_YEAR, tokenLen);
    264                break;
    265            case 'F': // day of week in month (number)
    266                rule = selectNumberRule(Calendar.DAY_OF_WEEK_IN_MONTH, tokenLen);
    267                break;
    268            case 'w': // week in year (number)
    269                rule = selectNumberRule(Calendar.WEEK_OF_YEAR, tokenLen);
    270                break;
    271            case 'W': // week in month (number)
    272                rule = selectNumberRule(Calendar.WEEK_OF_MONTH, tokenLen);
    273                break;
    274            case 'a': // am/pm marker (text)
    275                rule = new TextField(Calendar.AM_PM, AmPmStrings);
    276                break;
    277            case 'k': // hour in day (1..24)
    278                rule = new TwentyFourHourField(selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen));
    279                break;
    280            case 'K': // hour in am/pm (0..11)
    281                rule = selectNumberRule(Calendar.HOUR, tokenLen);
    282                break;
    283            case 'X': // ISO 8601
    284                rule = Iso8601_Rule.getRule(tokenLen);
    285                break;
    286            case 'z': // time zone (text)
    287                if (tokenLen >= 4) {
    288                    rule = new TimeZoneNameRule(mTimeZone, mLocale, TimeZone.LONG);
    289                } else {
    290                    rule = new TimeZoneNameRule(mTimeZone, mLocale, TimeZone.SHORT);
    291                }
    292                break;
    293            case 'Z': // time zone (value)
    294                if (tokenLen == 1) {
    295                    rule = TimeZoneNumberRule.INSTANCE_NO_COLON;
    296                } else if (tokenLen == 2) {
    297                    rule = Iso8601_Rule.ISO8601_HOURS_COLON_MINUTES;
    298                } else {
    299                    rule = TimeZoneNumberRule.INSTANCE_COLON;
    300                }
    301                break;
    302            case '\'': // literal text
    303                final String sub = token.substring(1);
    304                if (sub.length() == 1) {
    305                    rule = new CharacterLiteral(sub.charAt(0));
    306                } else {
    307                    rule = new StringLiteral(sub);
    308                }
    309                break;
    310            default:
    311                throw new IllegalArgumentException("Illegal pattern component: " + token);
    312            }
    313
    314            rules.add(rule);
    315        }
    316
    317        return rules;
    318    }
    319
    320    /**
    321     * <p>Performs the parsing of tokens.</p>
    322     *
    323     * @param pattern  the pattern
    324     * @param indexRef  index references
    325     * @return parsed token
    326     */
    327    protected String parseToken(final String pattern, final int[] indexRef) {
    328        final StringBuilder buf = new StringBuilder();
    329
    330        int i = indexRef[0];
    331        final int length = pattern.length();
    332
    333        char c = pattern.charAt(i);
    334        if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') {
    335            // Scan a run of the same character, which indicates a time
    336            // pattern.
    337            buf.append(c);
    338
    339            while (i + 1 < length) {
    340                final char peek = pattern.charAt(i + 1);
    341                if (peek == c) {
    342                    buf.append(c);
    343                    i++;
    344                } else {
    345                    break;
    346                }
    347            }
    348        } else {
    349            // This will identify token as text.
    350            buf.append('\'');
    351
    352            boolean inLiteral = false;
    353
    354            for (; i < length; i++) {
    355                c = pattern.charAt(i);
    356
    357                if (c == '\'') {
    358                    if (i + 1 < length && pattern.charAt(i + 1) == '\'') {
    359                        // '' is treated as escaped '
    360                        i++;
    361                        buf.append(c);
    362                    } else {
    363                        inLiteral = !inLiteral;
    364                    }
    365                } else if (!inLiteral &&
    366                         (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z')) {
    367                    i--;
    368                    break;
    369                } else {
    370                    buf.append(c);
    371                }
    372            }
    373        }
    374
    375        indexRef[0] = i;
    376        return buf.toString();
    377    }
    378
    379    /**
    380     * <p>Gets an appropriate rule for the padding required.</p>
    381     *
    382     * @param field  the field to get a rule for
    383     * @param padding  the padding required
    384     * @return a new rule with the correct padding
    385     */
    386    protected NumberRule selectNumberRule(final int field, final int padding) {
    387        switch (padding) {
    388        case 1:
    389            return new UnpaddedNumberField(field);
    390        case 2:
    391            return new TwoDigitNumberField(field);
    392        default:
    393            return new PaddedNumberField(field, padding);
    394        }
    395    }
    396
    397    // Format methods
    398    //-----------------------------------------------------------------------
    399    /**
    400     * <p>Formats a {@code Date}, {@code Calendar} or
    401     * {@code Long} (milliseconds) object.</p>
    402     * @deprecated Use {{@link #format(Date)}, {{@link #format(Calendar)}, {{@link #format(long)}, or {{@link #format(Object)}
    403     * @param obj  the object to format
    404     * @param toAppendTo  the buffer to append to
    405     * @param pos  the position - ignored
    406     * @return the buffer passed in
    407     */
    408    @Deprecated
    409    @Override
    410    public StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos) {
    411        if (obj instanceof Date) {
    412            return format((Date) obj, toAppendTo);
    413        } else if (obj instanceof Calendar) {
    414            return format((Calendar) obj, toAppendTo);
    415        } else if (obj instanceof Long) {
    416            return format(((Long) obj).longValue(), toAppendTo);
    417        } else {
    418            throw new IllegalArgumentException("Unknown class: " +
    419                (obj == null ? "<null>" : obj.getClass().getName()));
    420        }
    421    }
    422
    423    /**
    424     * <p>Formats a {@code Date}, {@code Calendar} or
    425     * {@code Long} (milliseconds) object.</p>
    426     * @since 3.5
    427     * @param obj  the object to format
    428     * @return The formatted value.
    429     */
    430    String format(final Object obj) {
    431        if (obj instanceof Date) {
    432            return format((Date) obj);
    433        } else if (obj instanceof Calendar) {
    434            return format((Calendar) obj);
    435        } else if (obj instanceof Long) {
    436            return format(((Long) obj).longValue());
    437        } else {
    438            throw new IllegalArgumentException("Unknown class: " +
    439                (obj == null ? "<null>" : obj.getClass().getName()));
    440        }
    441    }
    442
    443    /* (non-Javadoc)
    444     * @see org.apache.commons.lang3.time.DatePrinter#format(long)
    445     */
    446    @Override
    447    public String format(final long millis) {
    448        final Calendar c = newCalendar();
    449        c.setTimeInMillis(millis);
    450        return applyRulesToString(c);
    451    }
    452
    453    /**
    454     * Creates a String representation of the given Calendar by applying the rules of this printer to it.
    455     * @param c the Calender to apply the rules to.
    456     * @return a String representation of the given Calendar.
    457     */
    458    private String applyRulesToString(final Calendar c) {
    459        return applyRules(c, new StringBuilder(mMaxLengthEstimate)).toString();
    460    }
    461
    462    /**
    463     * Creation method for new calender instances.
    464     * @return a new Calendar instance.
    465     */
    466    private Calendar newCalendar() {
    467        return Calendar.getInstance(mTimeZone, mLocale);
    468    }
    469
    470    /* (non-Javadoc)
    471     * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date)
    472     */
    473    @Override
    474    public String format(final Date date) {
    475        final Calendar c = newCalendar();
    476        c.setTime(date);
    477        return applyRulesToString(c);
    478    }
    479
    480    /* (non-Javadoc)
    481     * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar)
    482     */
    483    @Override
    484    public String format(final Calendar calendar) {
    485        return format(calendar, new StringBuilder(mMaxLengthEstimate)).toString();
    486    }
    487
    488    /* (non-Javadoc)
    489     * @see org.apache.commons.lang3.time.DatePrinter#format(long, java.lang.StringBuffer)
    490     */
    491    @Override
    492    public StringBuffer format(final long millis, final StringBuffer buf) {
    493        final Calendar c = newCalendar();
    494        c.setTimeInMillis(millis);
    495        return (StringBuffer) applyRules(c, (Appendable) buf);
    496    }
    497
    498    /* (non-Javadoc)
    499     * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date, java.lang.StringBuffer)
    500     */
    501    @Override
    502    public StringBuffer format(final Date date, final StringBuffer buf) {
    503        final Calendar c = newCalendar();
    504        c.setTime(date);
    505        return (StringBuffer) applyRules(c, (Appendable) buf);
    506    }
    507
    508    /* (non-Javadoc)
    509     * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar, java.lang.StringBuffer)
    510     */
    511    @Override
    512    public StringBuffer format(final Calendar calendar, final StringBuffer buf) {
    513        // do not pass in calendar directly, this will cause TimeZone of FastDatePrinter to be ignored
    514        return format(calendar.getTime(), buf);
    515    }
    516
    517    /* (non-Javadoc)
    518     * @see org.apache.commons.lang3.time.DatePrinter#format(long, java.lang.Appendable)
    519     */
    520    @Override
    521    public <B extends Appendable> B format(final long millis, final B buf) {
    522        final Calendar c = newCalendar();
    523        c.setTimeInMillis(millis);
    524        return applyRules(c, buf);
    525    }
    526
    527    /* (non-Javadoc)
    528     * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date, java.lang.Appendable)
    529     */
    530    @Override
    531    public <B extends Appendable> B format(final Date date, final B buf) {
    532        final Calendar c = newCalendar();
    533        c.setTime(date);
    534        return applyRules(c, buf);
    535    }
    536
    537    /* (non-Javadoc)
    538     * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar, java.lang.Appendable)
    539     */
    540    @Override
    541    public <B extends Appendable> B format(Calendar calendar, final B buf) {
    542        // do not pass in calendar directly, this will cause TimeZone of FastDatePrinter to be ignored
    543        if (!calendar.getTimeZone().equals(mTimeZone)) {
    544            calendar = (Calendar) calendar.clone();
    545            calendar.setTimeZone(mTimeZone);
    546        }
    547        return applyRules(calendar, buf);
    548    }
    549
    550    /**
    551     * Performs the formatting by applying the rules to the
    552     * specified calendar.
    553     *
    554     * @param calendar the calendar to format
    555     * @param buf the buffer to format into
    556     * @return the specified string buffer
    557     *
    558     * @deprecated use {@link #format(Calendar)} or {@link #format(Calendar, Appendable)}
    559     */
    560    @Deprecated
    561    protected StringBuffer applyRules(final Calendar calendar, final StringBuffer buf) {
    562        return (StringBuffer) applyRules(calendar, (Appendable) buf);
    563    }
    564
    565    /**
    566     * <p>Performs the formatting by applying the rules to the
    567     * specified calendar.</p>
    568     *
    569     * @param calendar  the calendar to format
    570     * @param buf  the buffer to format into
    571     * @param <B> the Appendable class type, usually StringBuilder or StringBuffer.
    572     * @return the specified string buffer
    573     */
    574    private <B extends Appendable> B applyRules(final Calendar calendar, final B buf) {
    575        try {
    576            for (final Rule rule : mRules) {
    577                rule.appendTo(buf, calendar);
    578            }
    579        } catch (final IOException ioe) {
    580            ExceptionUtils.rethrow(ioe);
    581        }
    582        return buf;
    583    }
    584
    585    // Accessors
    586    //-----------------------------------------------------------------------
    587    /* (non-Javadoc)
    588     * @see org.apache.commons.lang3.time.DatePrinter#getPattern()
    589     */
    590    @Override
    591    public String getPattern() {
    592        return mPattern;
    593    }
    594
    595    /* (non-Javadoc)
    596     * @see org.apache.commons.lang3.time.DatePrinter#getTimeZone()
    597     */
    598    @Override
    599    public TimeZone getTimeZone() {
    600        return mTimeZone;
    601    }
    602
    603    /* (non-Javadoc)
    604     * @see org.apache.commons.lang3.time.DatePrinter#getLocale()
    605     */
    606    @Override
    607    public Locale getLocale() {
    608        return mLocale;
    609    }
    610
    611    /**
    612     * <p>Gets an estimate for the maximum string length that the
    613     * formatter will produce.</p>
    614     *
    615     * <p>The actual formatted length will almost always be less than or
    616     * equal to this amount.</p>
    617     *
    618     * @return the maximum formatted length
    619     */
    620    public int getMaxLengthEstimate() {
    621        return mMaxLengthEstimate;
    622    }
    623
    624    // Basics
    625    //-----------------------------------------------------------------------
    626    /**
    627     * <p>Compares two objects for equality.</p>
    628     *
    629     * @param obj  the object to compare to
    630     * @return {@code true} if equal
    631     */
    632    @Override
    633    public boolean equals(final Object obj) {
    634        if (!(obj instanceof FastDatePrinter)) {
    635            return false;
    636        }
    637        final FastDatePrinter other = (FastDatePrinter) obj;
    638        return mPattern.equals(other.mPattern)
    639            && mTimeZone.equals(other.mTimeZone)
    640            && mLocale.equals(other.mLocale);
    641    }
    642
    643    /**
    644     * <p>Returns a hash code compatible with equals.</p>
    645     *
    646     * @return a hash code compatible with equals
    647     */
    648    @Override
    649    public int hashCode() {
    650        return mPattern.hashCode() + 13 * (mTimeZone.hashCode() + 13 * mLocale.hashCode());
    651    }
    652
    653    /**
    654     * <p>Gets a debugging string version of this formatter.</p>
    655     *
    656     * @return a debugging string
    657     */
    658    @Override
    659    public String toString() {
    660        return "FastDatePrinter[" + mPattern + "," + mLocale + "," + mTimeZone.getID() + "]";
    661    }
    662
    663    // Serializing
    664    //-----------------------------------------------------------------------
    665    /**
    666     * Create the object after serialization. This implementation reinitializes the
    667     * transient properties.
    668     *
    669     * @param in ObjectInputStream from which the object is being deserialized.
    670     * @throws IOException if there is an IO issue.
    671     * @throws ClassNotFoundException if a class cannot be found.
    672     */
    673    private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
    674        in.defaultReadObject();
    675        init();
    676    }
    677
    678    /**
    679     * Appends two digits to the given buffer.
    680     *
    681     * @param buffer the buffer to append to.
    682     * @param value the value to append digits from.
    683     */
    684    private static void appendDigits(final Appendable buffer, final int value) throws IOException {
    685        buffer.append((char) (value / 10 + '0'));
    686        buffer.append((char) (value % 10 + '0'));
    687    }
    688
    689    private static final int MAX_DIGITS = 10; // log10(Integer.MAX_VALUE) ~= 9.3
    690
    691    /**
    692     * Appends all digits to the given buffer.
    693     *
    694     * @param buffer the buffer to append to.
    695     * @param value the value to append digits from.
    696     */
    697    private static void appendFullDigits(final Appendable buffer, int value, int minFieldWidth) throws IOException {
    698        // specialized paths for 1 to 4 digits -> avoid the memory allocation from the temporary work array
    699        // see LANG-1248
    700        if (value < 10000) {
    701            // less memory allocation path works for four digits or less
    702
    703            int nDigits = 4;
    704            if (value < 1000) {
    705                --nDigits;
    706                if (value < 100) {
    707                    --nDigits;
    708                    if (value < 10) {
    709                        --nDigits;
    710                    }
    711                }
    712            }
    713            // left zero pad
    714            for (int i = minFieldWidth - nDigits; i > 0; --i) {
    715                buffer.append('0');
    716            }
    717
    718            switch (nDigits) {
    719            case 4:
    720                buffer.append((char) (value / 1000 + '0'));
    721                value %= 1000;
    722            case 3:
    723                if (value >= 100) {
    724                    buffer.append((char) (value / 100 + '0'));
    725                    value %= 100;
    726                } else {
    727                    buffer.append('0');
    728                }
    729            case 2:
    730                if (value >= 10) {
    731                    buffer.append((char) (value / 10 + '0'));
    732                    value %= 10;
    733                } else {
    734                    buffer.append('0');
    735                }
    736            case 1:
    737                buffer.append((char) (value + '0'));
    738            }
    739        } else {
    740            // more memory allocation path works for any digits
    741
    742            // build up decimal representation in reverse
    743            final char[] work = new char[MAX_DIGITS];
    744            int digit = 0;
    745            while (value != 0) {
    746                work[digit++] = (char) (value % 10 + '0');
    747                value = value / 10;
    748            }
    749
    750            // pad with zeros
    751            while (digit < minFieldWidth) {
    752                buffer.append('0');
    753                --minFieldWidth;
    754            }
    755
    756            // reverse
    757            while (--digit >= 0) {
    758                buffer.append(work[digit]);
    759            }
    760        }
    761    }
    762
    763    // Rules
    764    //-----------------------------------------------------------------------
    765    /**
    766     * <p>Inner class defining a rule.</p>
    767     */
    768    private interface Rule {
    769        /**
    770         * Returns the estimated length of the result.
    771         *
    772         * @return the estimated length
    773         */
    774        int estimateLength();
    775
    776        /**
    777         * Appends the value of the specified calendar to the output buffer based on the rule implementation.
    778         *
    779         * @param buf the output buffer
    780         * @param calendar calendar to be appended
    781         * @throws IOException if an I/O error occurs.
    782         */
    783        void appendTo(Appendable buf, Calendar calendar) throws IOException;
    784    }
    785
    786    /**
    787     * <p>Inner class defining a numeric rule.</p>
    788     */
    789    private interface NumberRule extends Rule {
    790        /**
    791         * Appends the specified value to the output buffer based on the rule implementation.
    792         *
    793         * @param buffer the output buffer
    794         * @param value the value to be appended
    795         * @throws IOException if an I/O error occurs.
    796         */
    797        void appendTo(Appendable buffer, int value) throws IOException;
    798    }
    799
    800    /**
    801     * <p>Inner class to output a constant single character.</p>
    802     */
    803    private static class CharacterLiteral implements Rule {
    804        private final char mValue;
    805
    806        /**
    807         * Constructs a new instance of {@code CharacterLiteral}
    808         * to hold the specified value.
    809         *
    810         * @param value the character literal
    811         */
    812        CharacterLiteral(final char value) {
    813            mValue = value;
    814        }
    815
    816        /**
    817         * {@inheritDoc}
    818         */
    819        @Override
    820        public int estimateLength() {
    821            return 1;
    822        }
    823
    824        /**
    825         * {@inheritDoc}
    826         */
    827        @Override
    828        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
    829            buffer.append(mValue);
    830        }
    831    }
    832
    833    /**
    834     * <p>Inner class to output a constant string.</p>
    835     */
    836    private static class StringLiteral implements Rule {
    837        private final String mValue;
    838
    839        /**
    840         * Constructs a new instance of {@code StringLiteral}
    841         * to hold the specified value.
    842         *
    843         * @param value the string literal
    844         */
    845        StringLiteral(final String value) {
    846            mValue = value;
    847        }
    848
    849        /**
    850         * {@inheritDoc}
    851         */
    852        @Override
    853        public int estimateLength() {
    854            return mValue.length();
    855        }
    856
    857        /**
    858         * {@inheritDoc}
    859         */
    860        @Override
    861        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
    862            buffer.append(mValue);
    863        }
    864    }
    865
    866    /**
    867     * <p>Inner class to output one of a set of values.</p>
    868     */
    869    private static class TextField implements Rule {
    870        private final int mField;
    871        private final String[] mValues;
    872
    873        /**
    874         * Constructs an instance of {@code TextField}
    875         * with the specified field and values.
    876         *
    877         * @param field the field
    878         * @param values the field values
    879         */
    880        TextField(final int field, final String[] values) {
    881            mField = field;
    882            mValues = values;
    883        }
    884
    885        /**
    886         * {@inheritDoc}
    887         */
    888        @Override
    889        public int estimateLength() {
    890            int max = 0;
    891            for (int i=mValues.length; --i >= 0; ) {
    892                final int len = mValues[i].length();
    893                if (len > max) {
    894                    max = len;
    895                }
    896            }
    897            return max;
    898        }
    899
    900        /**
    901         * {@inheritDoc}
    902         */
    903        @Override
    904        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
    905            buffer.append(mValues[calendar.get(mField)]);
    906        }
    907    }
    908
    909    /**
    910     * <p>Inner class to output an unpadded number.</p>
    911     */
    912    private static class UnpaddedNumberField implements NumberRule {
    913        private final int mField;
    914
    915        /**
    916         * Constructs an instance of {@code UnpadedNumberField} with the specified field.
    917         *
    918         * @param field the field
    919         */
    920        UnpaddedNumberField(final int field) {
    921            mField = field;
    922        }
    923
    924        /**
    925         * {@inheritDoc}
    926         */
    927        @Override
    928        public int estimateLength() {
    929            return 4;
    930        }
    931
    932        /**
    933         * {@inheritDoc}
    934         */
    935        @Override
    936        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
    937            appendTo(buffer, calendar.get(mField));
    938        }
    939
    940        /**
    941         * {@inheritDoc}
    942         */
    943        @Override
    944        public final void appendTo(final Appendable buffer, final int value) throws IOException {
    945            if (value < 10) {
    946                buffer.append((char) (value + '0'));
    947            } else if (value < 100) {
    948                appendDigits(buffer, value);
    949            } else {
    950               appendFullDigits(buffer, value, 1);
    951            }
    952        }
    953    }
    954
    955    /**
    956     * <p>Inner class to output an unpadded month.</p>
    957     */
    958    private static class UnpaddedMonthField implements NumberRule {
    959        static final UnpaddedMonthField INSTANCE = new UnpaddedMonthField();
    960
    961        /**
    962         * Constructs an instance of {@code UnpaddedMonthField}.
    963         *
    964         */
    965        UnpaddedMonthField() {
    966        }
    967
    968        /**
    969         * {@inheritDoc}
    970         */
    971        @Override
    972        public int estimateLength() {
    973            return 2;
    974        }
    975
    976        /**
    977         * {@inheritDoc}
    978         */
    979        @Override
    980        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
    981            appendTo(buffer, calendar.get(Calendar.MONTH) + 1);
    982        }
    983
    984        /**
    985         * {@inheritDoc}
    986         */
    987        @Override
    988        public final void appendTo(final Appendable buffer, final int value) throws IOException {
    989            if (value < 10) {
    990                buffer.append((char) (value + '0'));
    991            } else {
    992                appendDigits(buffer, value);
    993            }
    994        }
    995    }
    996
    997    /**
    998     * <p>Inner class to output a padded number.</p>
    999     */
    1000    private static class PaddedNumberField implements NumberRule {
    1001        private final int mField;
    1002        private final int mSize;
    1003
    1004        /**
    1005         * Constructs an instance of {@code PaddedNumberField}.
    1006         *
    1007         * @param field the field
    1008         * @param size size of the output field
    1009         */
    1010        PaddedNumberField(final int field, final int size) {
    1011            if (size < 3) {
    1012                // Should use UnpaddedNumberField or TwoDigitNumberField.
    1013                throw new IllegalArgumentException();
    1014            }
    1015            mField = field;
    1016            mSize = size;
    1017        }
    1018
    1019        /**
    1020         * {@inheritDoc}
    1021         */
    1022        @Override
    1023        public int estimateLength() {
    1024            return mSize;
    1025        }
    1026
    1027        /**
    1028         * {@inheritDoc}
    1029         */
    1030        @Override
    1031        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
    1032            appendTo(buffer, calendar.get(mField));
    1033        }
    1034
    1035        /**
    1036         * {@inheritDoc}
    1037         */
    1038        @Override
    1039        public final void appendTo(final Appendable buffer, final int value) throws IOException {
    1040            appendFullDigits(buffer, value, mSize);
    1041        }
    1042    }
    1043
    1044    /**
    1045     * <p>Inner class to output a two digit number.</p>
    1046     */
    1047    private static class TwoDigitNumberField implements NumberRule {
    1048        private final int mField;
    1049
    1050        /**
    1051         * Constructs an instance of {@code TwoDigitNumberField} with the specified field.
    1052         *
    1053         * @param field the field
    1054         */
    1055        TwoDigitNumberField(final int field) {
    1056            mField = field;
    1057        }
    1058
    1059        /**
    1060         * {@inheritDoc}
    1061         */
    1062        @Override
    1063        public int estimateLength() {
    1064            return 2;
    1065        }
    1066
    1067        /**
    1068         * {@inheritDoc}
    1069         */
    1070        @Override
    1071        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
    1072            appendTo(buffer, calendar.get(mField));
    1073        }
    1074
    1075        /**
    1076         * {@inheritDoc}
    1077         */
    1078        @Override
    1079        public final void appendTo(final Appendable buffer, final int value) throws IOException {
    1080            if (value < 100) {
    1081                appendDigits(buffer, value);
    1082            } else {
    1083                appendFullDigits(buffer, value, 2);
    1084            }
    1085        }
    1086    }
    1087
    1088    /**
    1089     * <p>Inner class to output a two digit year.</p>
    1090     */
    1091    private static class TwoDigitYearField implements NumberRule {
    1092        static final TwoDigitYearField INSTANCE = new TwoDigitYearField();
    1093
    1094        /**
    1095         * Constructs an instance of {@code TwoDigitYearField}.
    1096         */
    1097        TwoDigitYearField() {
    1098        }
    1099
    1100        /**
    1101         * {@inheritDoc}
    1102         */
    1103        @Override
    1104        public int estimateLength() {
    1105            return 2;
    1106        }
    1107
    1108        /**
    1109         * {@inheritDoc}
    1110         */
    1111        @Override
    1112        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
    1113            appendTo(buffer, calendar.get(Calendar.YEAR) % 100);
    1114        }
    1115
    1116        /**
    1117         * {@inheritDoc}
    1118         */
    1119        @Override
    1120        public final void appendTo(final Appendable buffer, final int value) throws IOException {
    1121            appendDigits(buffer, value % 100);
    1122        }
    1123    }
    1124
    1125    /**
    1126     * <p>Inner class to output a two digit month.</p>
    1127     */
    1128    private static class TwoDigitMonthField implements NumberRule {
    1129        static final TwoDigitMonthField INSTANCE = new TwoDigitMonthField();
    1130
    1131        /**
    1132         * Constructs an instance of {@code TwoDigitMonthField}.
    1133         */
    1134        TwoDigitMonthField() {
    1135        }
    1136
    1137        /**
    1138         * {@inheritDoc}
    1139         */
    1140        @Override
    1141        public int estimateLength() {
    1142            return 2;
    1143        }
    1144
    1145        /**
    1146         * {@inheritDoc}
    1147         */
    1148        @Override
    1149        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
    1150            appendTo(buffer, calendar.get(Calendar.MONTH) + 1);
    1151        }
    1152
    1153        /**
    1154         * {@inheritDoc}
    1155         */
    1156        @Override
    1157        public final void appendTo(final Appendable buffer, final int value) throws IOException {
    1158            appendDigits(buffer, value);
    1159        }
    1160    }
    1161
    1162    /**
    1163     * <p>Inner class to output the twelve hour field.</p>
    1164     */
    1165    private static class TwelveHourField implements NumberRule {
    1166        private final NumberRule mRule;
    1167
    1168        /**
    1169         * Constructs an instance of {@code TwelveHourField} with the specified
    1170         * {@code NumberRule}.
    1171         *
    1172         * @param rule the rule
    1173         */
    1174        TwelveHourField(final NumberRule rule) {
    1175            mRule = rule;
    1176        }
    1177
    1178        /**
    1179         * {@inheritDoc}
    1180         */
    1181        @Override
    1182        public int estimateLength() {
    1183            return mRule.estimateLength();
    1184        }
    1185
    1186        /**
    1187         * {@inheritDoc}
    1188         */
    1189        @Override
    1190        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
    1191            int value = calendar.get(Calendar.HOUR);
    1192            if (value == 0) {
    1193                value = calendar.getLeastMaximum(Calendar.HOUR) + 1;
    1194            }
    1195            mRule.appendTo(buffer, value);
    1196        }
    1197
    1198        /**
    1199         * {@inheritDoc}
    1200         */
    1201        @Override
    1202        public void appendTo(final Appendable buffer, final int value) throws IOException {
    1203            mRule.appendTo(buffer, value);
    1204        }
    1205    }
    1206
    1207    /**
    1208     * <p>Inner class to output the twenty four hour field.</p>
    1209     */
    1210    private static class TwentyFourHourField implements NumberRule {
    1211        private final NumberRule mRule;
    1212
    1213        /**
    1214         * Constructs an instance of {@code TwentyFourHourField} with the specified
    1215         * {@code NumberRule}.
    1216         *
    1217         * @param rule the rule
    1218         */
    1219        TwentyFourHourField(final NumberRule rule) {
    1220            mRule = rule;
    1221        }
    1222
    1223        /**
    1224         * {@inheritDoc}
    1225         */
    1226        @Override
    1227        public int estimateLength() {
    1228            return mRule.estimateLength();
    1229        }
    1230
    1231        /**
    1232         * {@inheritDoc}
    1233         */
    1234        @Override
    1235        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
    1236            int value = calendar.get(Calendar.HOUR_OF_DAY);
    1237            if (value == 0) {
    1238                value = calendar.getMaximum(Calendar.HOUR_OF_DAY) + 1;
    1239            }
    1240            mRule.appendTo(buffer, value);
    1241        }
    1242
    1243        /**
    1244         * {@inheritDoc}
    1245         */
    1246        @Override
    1247        public void appendTo(final Appendable buffer, final int value) throws IOException {
    1248            mRule.appendTo(buffer, value);
    1249        }
    1250    }
    1251
    1252    /**
    1253     * <p>Inner class to output the numeric day in week.</p>
    1254     */
    1255    private static class DayInWeekField implements NumberRule {
    1256        private final NumberRule mRule;
    1257
    1258        DayInWeekField(final NumberRule rule) {
    1259            mRule = rule;
    1260        }
    1261
    1262        @Override
    1263        public int estimateLength() {
    1264            return mRule.estimateLength();
    1265        }
    1266
    1267        @Override
    1268        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
    1269            final int value = calendar.get(Calendar.DAY_OF_WEEK);
    1270            mRule.appendTo(buffer, value == Calendar.SUNDAY ? 7 : value - 1);
    1271        }
    1272
    1273        @Override
    1274        public void appendTo(final Appendable buffer, final int value) throws IOException {
    1275            mRule.appendTo(buffer, value);
    1276        }
    1277    }
    1278
    1279    /**
    1280     * <p>Inner class to output the numeric day in week.</p>
    1281     */
    1282    private static class WeekYear implements NumberRule {
    1283        private final NumberRule mRule;
    1284
    1285        WeekYear(final NumberRule rule) {
    1286            mRule = rule;
    1287        }
    1288
    1289        @Override
    1290        public int estimateLength() {
    1291            return mRule.estimateLength();
    1292        }
    1293
    1294        @Override
    1295        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
    1296            mRule.appendTo(buffer, calendar.getWeekYear());
    1297        }
    1298
    1299        @Override
    1300        public void appendTo(final Appendable buffer, final int value) throws IOException {
    1301            mRule.appendTo(buffer, value);
    1302        }
    1303    }
    1304
    1305    //-----------------------------------------------------------------------
    1306
    1307    private static final ConcurrentMap<TimeZoneDisplayKey, String> cTimeZoneDisplayCache =
    1308        new ConcurrentHashMap<>(7);
    1309    /**
    1310     * <p>Gets the time zone display name, using a cache for performance.</p>
    1311     *
    1312     * @param tz  the zone to query
    1313     * @param daylight  true if daylight savings
    1314     * @param style  the style to use {@code TimeZone.LONG} or {@code TimeZone.SHORT}
    1315     * @param locale  the locale to use
    1316     * @return the textual name of the time zone
    1317     */
    1318    static String getTimeZoneDisplay(final TimeZone tz, final boolean daylight, final int style, final Locale locale) {
    1319        final TimeZoneDisplayKey key = new TimeZoneDisplayKey(tz, daylight, style, locale);
    1320        String value = cTimeZoneDisplayCache.get(key);
    1321        if (value == null) {
    1322            // This is a very slow call, so cache the results.
    1323            value = tz.getDisplayName(daylight, style, locale);
    1324            final String prior = cTimeZoneDisplayCache.putIfAbsent(key, value);
    1325            if (prior != null) {
    1326                value= prior;
    1327            }
    1328        }
    1329        return value;
    1330    }
    1331
    1332    /**
    1333     * <p>Inner class to output a time zone name.</p>
    1334     */
    1335    private static class TimeZoneNameRule implements Rule {
    1336        private final Locale mLocale;
    1337        private final int mStyle;
    1338        private final String mStandard;
    1339        private final String mDaylight;
    1340
    1341        /**
    1342         * Constructs an instance of {@code TimeZoneNameRule} with the specified properties.
    1343         *
    1344         * @param timeZone the time zone
    1345         * @param locale the locale
    1346         * @param style the style
    1347         */
    1348        TimeZoneNameRule(final TimeZone timeZone, final Locale locale, final int style) {
    1349            mLocale = LocaleUtils.toLocale(locale);
    1350            mStyle = style;
    1351
    1352            mStandard = getTimeZoneDisplay(timeZone, false, style, locale);
    1353            mDaylight = getTimeZoneDisplay(timeZone, true, style, locale);
    1354        }
    1355
    1356        /**
    1357         * {@inheritDoc}
    1358         */
    1359        @Override
    1360        public int estimateLength() {
    1361            // We have no access to the Calendar object that will be passed to
    1362            // appendTo so base estimate on the TimeZone passed to the
    1363            // constructor
    1364            return Math.max(mStandard.length(), mDaylight.length());
    1365        }
    1366
    1367        /**
    1368         * {@inheritDoc}
    1369         */
    1370        @Override
    1371        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
    1372            final TimeZone zone = calendar.getTimeZone();
    1373            if (calendar.get(Calendar.DST_OFFSET) == 0) {
    1374                buffer.append(getTimeZoneDisplay(zone, false, mStyle, mLocale));
    1375            } else {
    1376                buffer.append(getTimeZoneDisplay(zone, true, mStyle, mLocale));
    1377            }
    1378        }
    1379    }
    1380
    1381    /**
    1382     * <p>Inner class to output a time zone as a number {@code +/-HHMM}
    1383     * or {@code +/-HH:MM}.</p>
    1384     */
    1385    private static class TimeZoneNumberRule implements Rule {
    1386        static final TimeZoneNumberRule INSTANCE_COLON = new TimeZoneNumberRule(true);
    1387        static final TimeZoneNumberRule INSTANCE_NO_COLON = new TimeZoneNumberRule(false);
    1388
    1389        final boolean mColon;
    1390
    1391        /**
    1392         * Constructs an instance of {@code TimeZoneNumberRule} with the specified properties.
    1393         *
    1394         * @param colon add colon between HH and MM in the output if {@code true}
    1395         */
    1396        TimeZoneNumberRule(final boolean colon) {
    1397            mColon = colon;
    1398        }
    1399
    1400        /**
    1401         * {@inheritDoc}
    1402         */
    1403        @Override
    1404        public int estimateLength() {
    1405            return 5;
    1406        }
    1407
    1408        /**
    1409         * {@inheritDoc}
    1410         */
    1411        @Override
    1412        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
    1413
    1414            int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET);
    1415
    1416            if (offset < 0) {
    1417                buffer.append('-');
    1418                offset = -offset;
    1419            } else {
    1420                buffer.append('+');
    1421            }
    1422
    1423            final int hours = offset / (60 * 60 * 1000);
    1424            appendDigits(buffer, hours);
    1425
    1426            if (mColon) {
    1427                buffer.append(':');
    1428            }
    1429
    1430            final int minutes = offset / (60 * 1000) - 60 * hours;
    1431            appendDigits(buffer, minutes);
    1432        }
    1433    }
    1434
    1435    /**
    1436     * <p>Inner class to output a time zone as a number {@code +/-HHMM}
    1437     * or {@code +/-HH:MM}.</p>
    1438     */
    1439    private static class Iso8601_Rule implements Rule {
    1440
    1441        // Sign TwoDigitHours or Z
    1442        static final Iso8601_Rule ISO8601_HOURS = new Iso8601_Rule(3);
    1443        // Sign TwoDigitHours Minutes or Z
    1444        static final Iso8601_Rule ISO8601_HOURS_MINUTES = new Iso8601_Rule(5);
    1445        // Sign TwoDigitHours : Minutes or Z
    1446        static final Iso8601_Rule ISO8601_HOURS_COLON_MINUTES = new Iso8601_Rule(6);
    1447
    1448        /**
    1449         * Factory method for Iso8601_Rules.
    1450         *
    1451         * @param tokenLen a token indicating the length of the TimeZone String to be formatted.
    1452         * @return a Iso8601_Rule that can format TimeZone String of length {@code tokenLen}. If no such
    1453         *          rule exists, an IllegalArgumentException will be thrown.
    1454         */
    1455        static Iso8601_Rule getRule(final int tokenLen) {
    1456            switch(tokenLen) {
    1457            case 1:
    1458                return ISO8601_HOURS;
    1459            case 2:
    1460                return ISO8601_HOURS_MINUTES;
    1461            case 3:
    1462                return ISO8601_HOURS_COLON_MINUTES;
    1463            default:
    1464                throw new IllegalArgumentException("invalid number of X");
    1465            }
    1466        }
    1467
    1468        final int length;
    1469
    1470        /**
    1471         * Constructs an instance of {@code Iso8601_Rule} with the specified properties.
    1472         *
    1473         * @param length The number of characters in output (unless Z is output)
    1474         */
    1475        Iso8601_Rule(final int length) {
    1476            this.length = length;
    1477        }
    1478
    1479        /**
    1480         * {@inheritDoc}
    1481         */
    1482        @Override
    1483        public int estimateLength() {
    1484            return length;
    1485        }
    1486
    1487        /**
    1488         * {@inheritDoc}
    1489         */
    1490        @Override
    1491        public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
    1492            int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET);
    1493            if (offset == 0) {
    1494                buffer.append("Z");
    1495                return;
    1496            }
    1497
    1498            if (offset < 0) {
    1499                buffer.append('-');
    1500                offset = -offset;
    1501            } else {
    1502                buffer.append('+');
    1503            }
    1504
    1505            final int hours = offset / (60 * 60 * 1000);
    1506            appendDigits(buffer, hours);
    1507
    1508            if (length<5) {
    1509                return;
    1510            }
    1511
    1512            if (length==6) {
    1513                buffer.append(':');
    1514            }
    1515
    1516            final int minutes = offset / (60 * 1000) - 60 * hours;
    1517            appendDigits(buffer, minutes);
    1518        }
    1519    }
    1520
    1521    // ----------------------------------------------------------------------
    1522    /**
    1523     * <p>Inner class that acts as a compound key for time zone names.</p>
    1524     */
    1525    private static class TimeZoneDisplayKey {
    1526        private final TimeZone mTimeZone;
    1527        private final int mStyle;
    1528        private final Locale mLocale;
    1529
    1530        /**
    1531         * Constructs an instance of {@code TimeZoneDisplayKey} with the specified properties.
    1532         *
    1533         * @param timeZone the time zone
    1534         * @param daylight adjust the style for daylight saving time if {@code true}
    1535         * @param style the time zone style
    1536         * @param locale the time zone locale
    1537         */
    1538        TimeZoneDisplayKey(final TimeZone timeZone,
    1539                           final boolean daylight, final int style, final Locale locale) {
    1540            mTimeZone = timeZone;
    1541            if (daylight) {
    1542                mStyle = style | 0x80000000;
    1543            } else {
    1544                mStyle = style;
    1545            }
    1546            mLocale = LocaleUtils.toLocale(locale);
    1547        }
    1548
    1549        /**
    1550         * {@inheritDoc}
    1551         */
    1552        @Override
    1553        public int hashCode() {
    1554            return (mStyle * 31 + mLocale.hashCode() ) * 31 + mTimeZone.hashCode();
    1555        }
    1556
    1557        /**
    1558         * {@inheritDoc}
    1559         */
    1560        @Override
    1561        public boolean equals(final Object obj) {
    1562            if (this == obj) {
    1563                return true;
    1564            }
    1565            if (obj instanceof TimeZoneDisplayKey) {
    1566                final TimeZoneDisplayKey other = (TimeZoneDisplayKey) obj;
    1567                return
    1568                    mTimeZone.equals(other.mTimeZone) &&
    1569                    mStyle == other.mStyle &&
    1570                    mLocale.equals(other.mLocale);
    1571            }
    1572            return false;
    1573        }
    1574    }
    1575}