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}