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