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