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