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