001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      https://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.lang3.time;
018
019import java.io.IOException;
020import java.io.ObjectInputStream;
021import java.io.Serializable;
022import java.text.DateFormat;
023import java.text.DateFormatSymbols;
024import java.text.FieldPosition;
025import java.text.SimpleDateFormat;
026import java.util.ArrayList;
027import java.util.Calendar;
028import java.util.Date;
029import java.util.List;
030import java.util.Locale;
031import java.util.TimeZone;
032import java.util.concurrent.ConcurrentHashMap;
033import java.util.concurrent.ConcurrentMap;
034
035import org.apache.commons.lang3.CharUtils;
036import org.apache.commons.lang3.ClassUtils;
037import org.apache.commons.lang3.LocaleUtils;
038import org.apache.commons.lang3.exception.ExceptionUtils;
039
040/**
041 * FastDatePrinter is a fast and thread-safe version of
042 * {@link java.text.SimpleDateFormat}.
043 *
044 * <p>
045 * To obtain a FastDatePrinter, use {@link FastDateFormat#getInstance(String, TimeZone, Locale)}
046 * or another variation of the factory methods of {@link FastDateFormat}.
047 * </p>
048 *
049 * <p>
050 * Since FastDatePrinter is thread safe, you can use a static member instance:
051 * </p>
052 * {@code
053 *     private static final DatePrinter DATE_PRINTER = FastDateFormat.getInstance("yyyy-MM-dd");
054 * }
055 *
056 * <p>
057 * This class can be used as a direct replacement to
058 * {@link SimpleDateFormat} in most formatting situations.
059 * This class is especially useful in multi-threaded server environments.
060 * {@link SimpleDateFormat} is not thread-safe in any JDK version,
061 * nor will it be as Sun have closed the bug/RFE.
062 * </p>
063 *
064 * <p>
065 * Only formatting is supported by this class, but all patterns are compatible with
066 * SimpleDateFormat (except time zones and some year patterns - see below).
067 * </p>
068 *
069 * <p>
070 * Java 1.4 introduced a new pattern letter, {@code 'Z'}, to represent
071 * time zones in RFC822 format (for example, {@code +0800} or {@code -1100}).
072 * This pattern letter can be used here (on all JDK versions).
073 * </p>
074 *
075 * <p>
076 * In addition, the pattern {@code 'ZZ'} has been made to represent
077 * ISO 8601 extended format time zones (for example, {@code +08:00} or {@code -11:00}).
078 * This introduces a minor incompatibility with Java 1.4, but at a gain of
079 * useful functionality.
080 * </p>
081 *
082 * <p>
083 * Starting with JDK7, ISO 8601 support was added using the pattern {@code 'X'}.
084 * To maintain compatibility, {@code 'ZZ'} will continue to be supported, but using
085 * one of the {@code 'X'} formats is recommended.
086 *
087 * <p>
088 * Javadoc cites for the year pattern: <i>For formatting, if the number of
089 * pattern letters is 2, the year is truncated to 2 digits; otherwise it is
090 * interpreted as a number.</i> Starting with Java 1.7 a pattern of 'Y' or
091 * 'YYY' will be formatted as '2003', while it was '03' in former Java
092 * versions. FastDatePrinter implements the behavior of Java 7.
093 * </p>
094 *
095 * @since 3.2
096 * @see FastDateParser
097 */
098public class FastDatePrinter implements DatePrinter, Serializable {
099    // 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}