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