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