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