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