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 * }
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> cTimeZoneDisplayCache =
903        new ConcurrentHashMap<>(7);
904
905    /**
906     * Appends two digits to the given buffer.
907     *
908     * @param buffer the buffer to append to.
909     * @param value the value to append digits from.
910     * @throws IOException If an I/O error occurs
911     */
912    private static void appendDigits(final Appendable buffer, final int value) throws IOException {
913        buffer.append((char) (value / 10 + '0'));
914        buffer.append((char) (value % 10 + '0'));
915    }
916
917    /**
918     * Appends all digits to the given buffer.
919     *
920     * @param buffer the buffer to append to.
921     * @param value the value to append digits from.
922     * @param minFieldWidth Minimum field width.
923     * @throws IOException If an I/O error occurs
924     */
925    private static void appendFullDigits(final Appendable buffer, int value, int minFieldWidth) throws IOException {
926        // specialized paths for 1 to 4 digits -> avoid the memory allocation from the temporary work array
927        // see LANG-1248
928        if (value < 10000) {
929            // less memory allocation path works for four digits or less
930
931            int nDigits = 4;
932            if (value < 1000) {
933                --nDigits;
934                if (value < 100) {
935                    --nDigits;
936                    if (value < 10) {
937                        --nDigits;
938                    }
939                }
940            }
941            // left zero pad
942            for (int i = minFieldWidth - nDigits; i > 0; --i) {
943                buffer.append('0');
944            }
945
946            switch (nDigits) {
947            case 4:
948                buffer.append((char) (value / 1000 + '0'));
949                value %= 1000;
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            case 2:
958                if (value >= 10) {
959                    buffer.append((char) (value / 10 + '0'));
960                    value %= 10;
961                } else {
962                    buffer.append('0');
963                }
964            case 1:
965                buffer.append((char) (value + '0'));
966            }
967        } else {
968            // more memory allocation path works for any digits
969
970            // build up decimal representation in reverse
971            final char[] work = new char[MAX_DIGITS];
972            int digit = 0;
973            while (value != 0) {
974                work[digit++] = (char) (value % 10 + '0');
975                value /= 10;
976            }
977
978            // pad with zeros
979            while (digit < minFieldWidth) {
980                buffer.append('0');
981                --minFieldWidth;
982            }
983
984            // reverse
985            while (--digit >= 0) {
986                buffer.append(work[digit]);
987            }
988        }
989    }
990
991    /**
992     * Gets the time zone display name, using a cache for performance.
993     *
994     * @param tz  the zone to query
995     * @param daylight  true if daylight savings
996     * @param style  the style to use {@code TimeZone.LONG} or {@code TimeZone.SHORT}
997     * @param locale  the locale to use
998     * @return the textual name of the time zone
999     */
1000    static String getTimeZoneDisplay(final TimeZone tz, final boolean daylight, final int style, final Locale locale) {
1001        final TimeZoneDisplayKey key = new TimeZoneDisplayKey(tz, daylight, style, locale);
1002        // This is a very slow call, so cache the results.
1003        return cTimeZoneDisplayCache.computeIfAbsent(key, k -> tz.getDisplayName(daylight, style, locale));
1004    }
1005
1006    /**
1007     * The pattern.
1008     */
1009    private final String pattern;
1010
1011    /**
1012     * The time zone.
1013     */
1014    private final TimeZone timeZone;
1015
1016    /**
1017     * The locale.
1018     */
1019    private final Locale locale;
1020
1021    /**
1022     * The parsed rules.
1023     */
1024    private transient Rule[] rules;
1025
1026    /**
1027     * The estimated maximum length.
1028     */
1029    private transient int maxLengthEstimate;
1030
1031    // Constructor
1032    /**
1033     * Constructs a new FastDatePrinter.
1034     * Use {@link FastDateFormat#getInstance(String, TimeZone, Locale)}  or another variation of the
1035     * factory methods of {@link FastDateFormat} to get a cached FastDatePrinter instance.
1036     *
1037     * @param pattern  {@link java.text.SimpleDateFormat} compatible pattern
1038     * @param timeZone  non-null time zone to use
1039     * @param locale  non-null locale to use
1040     * @throws NullPointerException if pattern, timeZone, or locale is null.
1041     */
1042    protected FastDatePrinter(final String pattern, final TimeZone timeZone, final Locale locale) {
1043        this.pattern = pattern;
1044        this.timeZone = timeZone;
1045        this.locale = LocaleUtils.toLocale(locale);
1046        init();
1047    }
1048
1049    /**
1050     * Performs the formatting by applying the rules to the
1051     * specified calendar.
1052     *
1053     * @param calendar  the calendar to format
1054     * @param buf  the buffer to format into
1055     * @param <B> the Appendable class type, usually StringBuilder or StringBuffer.
1056     * @return the specified string buffer
1057     */
1058    private <B extends Appendable> B applyRules(final Calendar calendar, final B buf) {
1059        try {
1060            for (final Rule rule : rules) {
1061                rule.appendTo(buf, calendar);
1062            }
1063        } catch (final IOException ioe) {
1064            ExceptionUtils.asRuntimeException(ioe);
1065        }
1066        return buf;
1067    }
1068
1069    /**
1070     * Performs the formatting by applying the rules to the
1071     * specified calendar.
1072     *
1073     * @param calendar the calendar to format
1074     * @param buf the buffer to format into
1075     * @return the specified string buffer
1076     *
1077     * @deprecated use {@link #format(Calendar)} or {@link #format(Calendar, Appendable)}
1078     */
1079    @Deprecated
1080    protected StringBuffer applyRules(final Calendar calendar, final StringBuffer buf) {
1081        return (StringBuffer) applyRules(calendar, (Appendable) buf);
1082    }
1083
1084    /**
1085     * Creates a String representation of the given Calendar by applying the rules of this printer to it.
1086     * @param c the Calendar to apply the rules to.
1087     * @return a String representation of the given Calendar.
1088     */
1089    private String applyRulesToString(final Calendar c) {
1090        return applyRules(c, new StringBuilder(maxLengthEstimate)).toString();
1091    }
1092
1093    // Basics
1094    /**
1095     * Compares two objects for equality.
1096     *
1097     * @param obj  the object to compare to
1098     * @return {@code true} if equal
1099     */
1100    @Override
1101    public boolean equals(final Object obj) {
1102        if (!(obj instanceof FastDatePrinter)) {
1103            return false;
1104        }
1105        final FastDatePrinter other = (FastDatePrinter) obj;
1106        return pattern.equals(other.pattern)
1107            && timeZone.equals(other.timeZone)
1108            && locale.equals(other.locale);
1109    }
1110
1111    /* (non-Javadoc)
1112     * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar)
1113     */
1114    @Override
1115    public String format(final Calendar calendar) {
1116        return format(calendar, new StringBuilder(maxLengthEstimate)).toString();
1117    }
1118
1119    /* (non-Javadoc)
1120     * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar, Appendable)
1121     */
1122    @Override
1123    public <B extends Appendable> B format(Calendar calendar, final B buf) {
1124        // do not pass in calendar directly, this will cause TimeZone of FastDatePrinter to be ignored
1125        if (!calendar.getTimeZone().equals(timeZone)) {
1126            calendar = (Calendar) calendar.clone();
1127            calendar.setTimeZone(timeZone);
1128        }
1129        return applyRules(calendar, buf);
1130    }
1131
1132    /* (non-Javadoc)
1133     * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar, StringBuffer)
1134     */
1135    @Override
1136    public StringBuffer format(final Calendar calendar, final StringBuffer buf) {
1137        // do not pass in calendar directly, this will cause TimeZone of FastDatePrinter to be ignored
1138        return format(calendar.getTime(), buf);
1139    }
1140
1141    /* (non-Javadoc)
1142     * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date)
1143     */
1144    @Override
1145    public String format(final Date date) {
1146        final Calendar c = newCalendar();
1147        c.setTime(date);
1148        return applyRulesToString(c);
1149    }
1150
1151    /* (non-Javadoc)
1152     * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date, Appendable)
1153     */
1154    @Override
1155    public <B extends Appendable> B format(final Date date, final B buf) {
1156        final Calendar c = newCalendar();
1157        c.setTime(date);
1158        return applyRules(c, buf);
1159    }
1160
1161    /* (non-Javadoc)
1162     * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date, StringBuffer)
1163     */
1164    @Override
1165    public StringBuffer format(final Date date, final StringBuffer buf) {
1166        final Calendar c = newCalendar();
1167        c.setTime(date);
1168        return (StringBuffer) applyRules(c, (Appendable) buf);
1169    }
1170
1171    /* (non-Javadoc)
1172     * @see org.apache.commons.lang3.time.DatePrinter#format(long)
1173     */
1174    @Override
1175    public String format(final long millis) {
1176        final Calendar c = newCalendar();
1177        c.setTimeInMillis(millis);
1178        return applyRulesToString(c);
1179    }
1180
1181    /* (non-Javadoc)
1182     * @see org.apache.commons.lang3.time.DatePrinter#format(long, Appendable)
1183     */
1184    @Override
1185    public <B extends Appendable> B format(final long millis, final B buf) {
1186        final Calendar c = newCalendar();
1187        c.setTimeInMillis(millis);
1188        return applyRules(c, buf);
1189    }
1190
1191    /* (non-Javadoc)
1192     * @see org.apache.commons.lang3.time.DatePrinter#format(long, StringBuffer)
1193     */
1194    @Override
1195    public StringBuffer format(final long millis, final StringBuffer buf) {
1196        final Calendar c = newCalendar();
1197        c.setTimeInMillis(millis);
1198        return (StringBuffer) applyRules(c, (Appendable) buf);
1199    }
1200
1201    /**
1202     * Formats a {@link Date}, {@link Calendar} or
1203     * {@link Long} (milliseconds) object.
1204     * @since 3.5
1205     * @param obj  the object to format
1206     * @return The formatted value.
1207     */
1208    String format(final Object obj) {
1209        if (obj instanceof Date) {
1210            return format((Date) obj);
1211        }
1212        if (obj instanceof Calendar) {
1213            return format((Calendar) obj);
1214        }
1215        if (obj instanceof Long) {
1216            return format(((Long) obj).longValue());
1217        }
1218        throw new IllegalArgumentException("Unknown class: " + ClassUtils.getName(obj, "<null>"));
1219    }
1220
1221    // Format methods
1222    /**
1223     * Formats a {@link Date}, {@link Calendar} or
1224     * {@link Long} (milliseconds) object.
1225     * @deprecated Use {{@link #format(Date)}, {{@link #format(Calendar)}, {{@link #format(long)}.
1226     * @param obj  the object to format
1227     * @param toAppendTo  the buffer to append to
1228     * @param pos  the position - ignored
1229     * @return the buffer passed in
1230     */
1231    @Deprecated
1232    @Override
1233    public StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos) {
1234        if (obj instanceof Date) {
1235            return format((Date) obj, toAppendTo);
1236        }
1237        if (obj instanceof Calendar) {
1238            return format((Calendar) obj, toAppendTo);
1239        }
1240        if (obj instanceof Long) {
1241            return format(((Long) obj).longValue(), toAppendTo);
1242        }
1243        throw new IllegalArgumentException("Unknown class: " + ClassUtils.getName(obj, "<null>"));
1244    }
1245
1246    /* (non-Javadoc)
1247     * @see org.apache.commons.lang3.time.DatePrinter#getLocale()
1248     */
1249    @Override
1250    public Locale getLocale() {
1251        return locale;
1252    }
1253
1254    /**
1255     * Gets an estimate for the maximum string length that the
1256     * formatter will produce.
1257     *
1258     * <p>The actual formatted length will almost always be less than or
1259     * equal to this amount.</p>
1260     *
1261     * @return the maximum formatted length
1262     */
1263    public int getMaxLengthEstimate() {
1264        return maxLengthEstimate;
1265    }
1266
1267    // Accessors
1268    /* (non-Javadoc)
1269     * @see org.apache.commons.lang3.time.DatePrinter#getPattern()
1270     */
1271    @Override
1272    public String getPattern() {
1273        return pattern;
1274    }
1275
1276    /* (non-Javadoc)
1277     * @see org.apache.commons.lang3.time.DatePrinter#getTimeZone()
1278     */
1279    @Override
1280    public TimeZone getTimeZone() {
1281        return timeZone;
1282    }
1283
1284    /**
1285     * Returns a hash code compatible with equals.
1286     *
1287     * @return a hash code compatible with equals
1288     */
1289    @Override
1290    public int hashCode() {
1291        return pattern.hashCode() + 13 * (timeZone.hashCode() + 13 * locale.hashCode());
1292    }
1293
1294    /**
1295     * Initializes the instance for first use.
1296     */
1297    private void init() {
1298        final List<Rule> rulesList = parsePattern();
1299        rules = rulesList.toArray(EMPTY_RULE_ARRAY);
1300
1301        int len = 0;
1302        for (int i = rules.length; --i >= 0;) {
1303            len += rules[i].estimateLength();
1304        }
1305
1306        maxLengthEstimate = len;
1307    }
1308
1309    /**
1310     * Creates a new Calendar instance.
1311     * @return a new Calendar instance.
1312     */
1313    private Calendar newCalendar() {
1314        return Calendar.getInstance(timeZone, locale);
1315    }
1316
1317    // Parse the pattern
1318    /**
1319     * Returns a list of Rules given a pattern.
1320     *
1321     * @return a {@link List} of Rule objects
1322     * @throws IllegalArgumentException if pattern is invalid
1323     */
1324    protected List<Rule> parsePattern() {
1325        final DateFormatSymbols symbols = new DateFormatSymbols(locale);
1326        final List<Rule> rules = new ArrayList<>();
1327
1328        final String[] ERAs = symbols.getEras();
1329        final String[] months = symbols.getMonths();
1330        final String[] shortMonths = symbols.getShortMonths();
1331        final String[] weekdays = symbols.getWeekdays();
1332        final String[] shortWeekdays = symbols.getShortWeekdays();
1333        final String[] AmPmStrings = symbols.getAmPmStrings();
1334
1335        final int length = pattern.length();
1336        final int[] indexRef = new int[1];
1337
1338        for (int i = 0; i < length; i++) {
1339            indexRef[0] = i;
1340            final String token = parseToken(pattern, indexRef);
1341            i = indexRef[0];
1342
1343            final int tokenLen = token.length();
1344            if (tokenLen == 0) {
1345                break;
1346            }
1347
1348            Rule rule;
1349            final char c = token.charAt(0);
1350
1351            switch (c) {
1352            case 'G': // era designator (text)
1353                rule = new TextField(Calendar.ERA, ERAs);
1354                break;
1355            case 'y': // year (number)
1356            case 'Y': // week year
1357                if (tokenLen == 2) {
1358                    rule = TwoDigitYearField.INSTANCE;
1359                } else {
1360                    rule = selectNumberRule(Calendar.YEAR, Math.max(tokenLen, 4));
1361                }
1362                if (c == 'Y') {
1363                    rule = new WeekYear((NumberRule) rule);
1364                }
1365                break;
1366            case 'M': // month in year (text and number)
1367                if (tokenLen >= 4) {
1368                    rule = new TextField(Calendar.MONTH, months);
1369                } else if (tokenLen == 3) {
1370                    rule = new TextField(Calendar.MONTH, shortMonths);
1371                } else if (tokenLen == 2) {
1372                    rule = TwoDigitMonthField.INSTANCE;
1373                } else {
1374                    rule = UnpaddedMonthField.INSTANCE;
1375                }
1376                break;
1377            case 'L': // month in year (text and number)
1378                if (tokenLen >= 4) {
1379                    rule = new TextField(Calendar.MONTH, CalendarUtils.getInstance(locale).getStandaloneLongMonthNames());
1380                } else if (tokenLen == 3) {
1381                    rule = new TextField(Calendar.MONTH, CalendarUtils.getInstance(locale).getStandaloneShortMonthNames());
1382                } else if (tokenLen == 2) {
1383                    rule = TwoDigitMonthField.INSTANCE;
1384                } else {
1385                    rule = UnpaddedMonthField.INSTANCE;
1386                }
1387                break;
1388            case 'd': // day in month (number)
1389                rule = selectNumberRule(Calendar.DAY_OF_MONTH, tokenLen);
1390                break;
1391            case 'h': // hour in am/pm (number, 1..12)
1392                rule = new TwelveHourField(selectNumberRule(Calendar.HOUR, tokenLen));
1393                break;
1394            case 'H': // hour in day (number, 0..23)
1395                rule = selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen);
1396                break;
1397            case 'm': // minute in hour (number)
1398                rule = selectNumberRule(Calendar.MINUTE, tokenLen);
1399                break;
1400            case 's': // second in minute (number)
1401                rule = selectNumberRule(Calendar.SECOND, tokenLen);
1402                break;
1403            case 'S': // millisecond (number)
1404                rule = selectNumberRule(Calendar.MILLISECOND, tokenLen);
1405                break;
1406            case 'E': // day in week (text)
1407                rule = new TextField(Calendar.DAY_OF_WEEK, tokenLen < 4 ? shortWeekdays : weekdays);
1408                break;
1409            case 'u': // day in week (number)
1410                rule = new DayInWeekField(selectNumberRule(Calendar.DAY_OF_WEEK, tokenLen));
1411                break;
1412            case 'D': // day in year (number)
1413                rule = selectNumberRule(Calendar.DAY_OF_YEAR, tokenLen);
1414                break;
1415            case 'F': // day of week in month (number)
1416                rule = selectNumberRule(Calendar.DAY_OF_WEEK_IN_MONTH, tokenLen);
1417                break;
1418            case 'w': // week in year (number)
1419                rule = selectNumberRule(Calendar.WEEK_OF_YEAR, tokenLen);
1420                break;
1421            case 'W': // week in month (number)
1422                rule = selectNumberRule(Calendar.WEEK_OF_MONTH, tokenLen);
1423                break;
1424            case 'a': // am/pm marker (text)
1425                rule = new TextField(Calendar.AM_PM, AmPmStrings);
1426                break;
1427            case 'k': // hour in day (1..24)
1428                rule = new TwentyFourHourField(selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen));
1429                break;
1430            case 'K': // hour in am/pm (0..11)
1431                rule = selectNumberRule(Calendar.HOUR, tokenLen);
1432                break;
1433            case 'X': // ISO 8601
1434                rule = Iso8601_Rule.getRule(tokenLen);
1435                break;
1436            case 'z': // time zone (text)
1437                if (tokenLen >= 4) {
1438                    rule = new TimeZoneNameRule(timeZone, locale, TimeZone.LONG);
1439                } else {
1440                    rule = new TimeZoneNameRule(timeZone, locale, TimeZone.SHORT);
1441                }
1442                break;
1443            case 'Z': // time zone (value)
1444                if (tokenLen == 1) {
1445                    rule = TimeZoneNumberRule.INSTANCE_NO_COLON;
1446                } else if (tokenLen == 2) {
1447                    rule = Iso8601_Rule.ISO8601_HOURS_COLON_MINUTES;
1448                } else {
1449                    rule = TimeZoneNumberRule.INSTANCE_COLON;
1450                }
1451                break;
1452            case '\'': // literal text
1453                final String sub = token.substring(1);
1454                if (sub.length() == 1) {
1455                    rule = new CharacterLiteral(sub.charAt(0));
1456                } else {
1457                    rule = new StringLiteral(sub);
1458                }
1459                break;
1460            default:
1461                throw new IllegalArgumentException("Illegal pattern component: " + token);
1462            }
1463
1464            rules.add(rule);
1465        }
1466
1467        return rules;
1468    }
1469
1470    /**
1471     * Performs the parsing of tokens.
1472     *
1473     * @param pattern  the pattern
1474     * @param indexRef  index references
1475     * @return parsed token
1476     */
1477    protected String parseToken(final String pattern, final int[] indexRef) {
1478        final StringBuilder buf = new StringBuilder();
1479
1480        int i = indexRef[0];
1481        final int length = pattern.length();
1482
1483        char c = pattern.charAt(i);
1484        if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') {
1485            // Scan a run of the same character, which indicates a time
1486            // pattern.
1487            buf.append(c);
1488
1489            while (i + 1 < length) {
1490                final char peek = pattern.charAt(i + 1);
1491                if (peek != c) {
1492                    break;
1493                }
1494                buf.append(c);
1495                i++;
1496            }
1497        } else {
1498            // This will identify token as text.
1499            buf.append('\'');
1500
1501            boolean inLiteral = false;
1502
1503            for (; i < length; i++) {
1504                c = pattern.charAt(i);
1505
1506                if (c == '\'') {
1507                    if (i + 1 < length && pattern.charAt(i + 1) == '\'') {
1508                        // '' is treated as escaped '
1509                        i++;
1510                        buf.append(c);
1511                    } else {
1512                        inLiteral = !inLiteral;
1513                    }
1514                } else if (!inLiteral &&
1515                         (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z')) {
1516                    i--;
1517                    break;
1518                } else {
1519                    buf.append(c);
1520                }
1521            }
1522        }
1523
1524        indexRef[0] = i;
1525        return buf.toString();
1526    }
1527
1528    // Serializing
1529    /**
1530     * Create the object after serialization. This implementation reinitializes the
1531     * transient properties.
1532     *
1533     * @param in ObjectInputStream from which the object is being deserialized.
1534     * @throws IOException if there is an IO issue.
1535     * @throws ClassNotFoundException if a class cannot be found.
1536     */
1537    private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
1538        in.defaultReadObject();
1539        init();
1540    }
1541
1542    /**
1543     * Gets an appropriate rule for the padding required.
1544     *
1545     * @param field  the field to get a rule for
1546     * @param padding  the padding required
1547     * @return a new rule with the correct padding
1548     */
1549    protected NumberRule selectNumberRule(final int field, final int padding) {
1550        switch (padding) {
1551        case 1:
1552            return new UnpaddedNumberField(field);
1553        case 2:
1554            return new TwoDigitNumberField(field);
1555        default:
1556            return new PaddedNumberField(field, padding);
1557        }
1558    }
1559
1560    /**
1561     * Gets a debugging string version of this formatter.
1562     *
1563     * @return a debugging string
1564     */
1565    @Override
1566    public String toString() {
1567        return "FastDatePrinter[" + pattern + "," + locale + "," + timeZone.getID() + "]";
1568    }
1569}