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