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.DateFormatSymbols;
23  import java.text.ParseException;
24  import java.text.ParsePosition;
25  import java.text.SimpleDateFormat;
26  import java.util.ArrayList;
27  import java.util.Arrays;
28  import java.util.Calendar;
29  import java.util.Comparator;
30  import java.util.Date;
31  import java.util.HashMap;
32  import java.util.List;
33  import java.util.ListIterator;
34  import java.util.Locale;
35  import java.util.Map;
36  import java.util.Objects;
37  import java.util.Set;
38  import java.util.TimeZone;
39  import java.util.TreeMap;
40  import java.util.TreeSet;
41  import java.util.concurrent.ConcurrentHashMap;
42  import java.util.concurrent.ConcurrentMap;
43  import java.util.regex.Matcher;
44  import java.util.regex.Pattern;
45  import java.util.stream.Stream;
46  
47  import org.apache.commons.lang3.ArraySorter;
48  import org.apache.commons.lang3.CharUtils;
49  import org.apache.commons.lang3.LocaleUtils;
50  
51  /**
52   * FastDateParser is a fast and thread-safe version of {@link java.text.SimpleDateFormat}.
53   *
54   * <p>
55   * To obtain a proxy to a FastDateParser, use {@link FastDateFormat#getInstance(String, TimeZone, Locale)} or another variation of the factory methods of
56   * {@link FastDateFormat}.
57   * </p>
58   *
59   * <p>
60   * Since FastDateParser is thread safe, you can use a static member instance:
61   * </p>
62   * {@code
63   *     private static final DateParser DATE_PARSER = FastDateFormat.getInstance("yyyy-MM-dd");
64   * }
65   *
66   * <p>
67   * This class can be used as a direct replacement for {@link SimpleDateFormat} in most parsing situations. This class is especially useful in multi-threaded
68   * server environments. {@link SimpleDateFormat} is not thread-safe in any JDK version, nor will it be as Sun has closed the
69   * <a href="https://bugs.openjdk.org/browse/JDK-4228335">bug</a>/RFE.
70   * </p>
71   *
72   * <p>
73   * Only parsing is supported by this class, but all patterns are compatible with SimpleDateFormat.
74   * </p>
75   *
76   * <p>
77   * The class operates in lenient mode, so for example a time of 90 minutes is treated as 1 hour 30 minutes.
78   * </p>
79   *
80   * <p>
81   * Timing tests indicate this class is as about as fast as SimpleDateFormat in single thread applications and about 25% faster in multi-thread applications.
82   * </p>
83   *
84   * @since 3.2
85   * @see FastDatePrinter
86   */
87  public class FastDateParser implements DateParser, Serializable {
88  
89      /**
90       * A strategy that handles a text field in the parsing pattern
91       */
92      private static final class CaseInsensitiveTextStrategy extends PatternStrategy {
93  
94          private final int field;
95          private final Locale locale;
96          private final Map<String, Integer> lKeyValues;
97  
98          /**
99           * Constructs a Strategy that parses a Text field
100          *
101          * @param field            The Calendar field
102          * @param definingCalendar The Calendar to use
103          * @param locale           The Locale to use
104          */
105         CaseInsensitiveTextStrategy(final int field, final Calendar definingCalendar, final Locale locale) {
106             this.field = field;
107             this.locale = LocaleUtils.toLocale(locale);
108 
109             final StringBuilder regex = new StringBuilder();
110             regex.append("((?iu)");
111             lKeyValues = appendDisplayNames(definingCalendar, locale, field, regex);
112             regex.setLength(regex.length() - 1);
113             regex.append(")");
114             createPattern(regex);
115         }
116 
117         /**
118          * {@inheritDoc}
119          */
120         @Override
121         void setCalendar(final FastDateParser parser, final Calendar calendar, final String value) {
122             final String lowerCase = value.toLowerCase(locale);
123             Integer iVal = lKeyValues.get(lowerCase);
124             if (iVal == null) {
125                 // match missing the optional trailing period
126                 iVal = lKeyValues.get(lowerCase + '.');
127             }
128             // LANG-1669: Mimic fix done in OpenJDK 17 to resolve issue with parsing newly supported day periods added in OpenJDK 16
129             if (Calendar.AM_PM != this.field || iVal <= 1) {
130                 calendar.set(field, iVal.intValue());
131             }
132         }
133 
134         /**
135          * Converts this instance to a handy debug string.
136          *
137          * @since 3.12.0
138          */
139         @Override
140         public String toString() {
141             return "CaseInsensitiveTextStrategy [field=" + field + ", locale=" + locale + ", lKeyValues=" + lKeyValues + ", pattern=" + pattern + "]";
142         }
143     }
144 
145     /**
146      * A strategy that copies the static or quoted field in the parsing pattern
147      */
148     private static final class CopyQuotedStrategy extends Strategy {
149 
150         private final String formatField;
151 
152         /**
153          * Constructs a Strategy that ensures the formatField has literal text
154          *
155          * @param formatField The literal text to match
156          */
157         CopyQuotedStrategy(final String formatField) {
158             this.formatField = formatField;
159         }
160 
161         /**
162          * {@inheritDoc}
163          */
164         @Override
165         boolean isNumber() {
166             return false;
167         }
168 
169         @Override
170         boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) {
171             for (int idx = 0; idx < formatField.length(); ++idx) {
172                 final int sIdx = idx + pos.getIndex();
173                 if (sIdx == source.length()) {
174                     pos.setErrorIndex(sIdx);
175                     return false;
176                 }
177                 if (formatField.charAt(idx) != source.charAt(sIdx)) {
178                     pos.setErrorIndex(sIdx);
179                     return false;
180                 }
181             }
182             pos.setIndex(formatField.length() + pos.getIndex());
183             return true;
184         }
185 
186         /**
187          * Converts this instance to a handy debug string.
188          *
189          * @since 3.12.0
190          */
191         @Override
192         public String toString() {
193             return "CopyQuotedStrategy [formatField=" + formatField + "]";
194         }
195     }
196 
197     private static final class ISO8601TimeZoneStrategy extends PatternStrategy {
198         // Z, +hh, -hh, +hhmm, -hhmm, +hh:mm or -hh:mm
199 
200         private static final Strategy ISO_8601_1_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}))");
201 
202         private static final Strategy ISO_8601_2_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}\\d{2}))");
203 
204         private static final Strategy ISO_8601_3_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}(?::)\\d{2}))");
205         /**
206          * Factory method for ISO8601TimeZoneStrategies.
207          *
208          * @param tokenLen a token indicating the length of the TimeZone String to be formatted.
209          * @return a ISO8601TimeZoneStrategy that can format TimeZone String of length {@code tokenLen}. If no such strategy exists, an IllegalArgumentException
210          *         will be thrown.
211          */
212         static Strategy getStrategy(final int tokenLen) {
213             switch (tokenLen) {
214             case 1:
215                 return ISO_8601_1_STRATEGY;
216             case 2:
217                 return ISO_8601_2_STRATEGY;
218             case 3:
219                 return ISO_8601_3_STRATEGY;
220             default:
221                 throw new IllegalArgumentException("invalid number of X");
222             }
223         }
224         /**
225          * Constructs a Strategy that parses a TimeZone
226          *
227          * @param pattern The Pattern
228          */
229         ISO8601TimeZoneStrategy(final String pattern) {
230             createPattern(pattern);
231         }
232 
233         /**
234          * {@inheritDoc}
235          */
236         @Override
237         void setCalendar(final FastDateParser parser, final Calendar calendar, final String value) {
238             calendar.setTimeZone(FastTimeZone.getGmtTimeZone(value));
239         }
240     }
241 
242     /**
243      * A strategy that handles a number field in the parsing pattern
244      */
245     private static class NumberStrategy extends Strategy {
246 
247         private final int field;
248 
249         /**
250          * Constructs a Strategy that parses a Number field
251          *
252          * @param field The Calendar field
253          */
254         NumberStrategy(final int field) {
255             this.field = field;
256         }
257 
258         /**
259          * {@inheritDoc}
260          */
261         @Override
262         boolean isNumber() {
263             return true;
264         }
265 
266         /**
267          * Make any modifications to parsed integer
268          *
269          * @param parser The parser
270          * @param iValue The parsed integer
271          * @return The modified value
272          */
273         int modify(final FastDateParser parser, final int iValue) {
274             return iValue;
275         }
276 
277         @Override
278         boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) {
279             int idx = pos.getIndex();
280             int last = source.length();
281 
282             if (maxWidth == 0) {
283                 // if no maxWidth, strip leading white space
284                 for (; idx < last; ++idx) {
285                     final char c = source.charAt(idx);
286                     if (!Character.isWhitespace(c)) {
287                         break;
288                     }
289                 }
290                 pos.setIndex(idx);
291             } else {
292                 final int end = idx + maxWidth;
293                 if (last > end) {
294                     last = end;
295                 }
296             }
297 
298             for (; idx < last; ++idx) {
299                 final char c = source.charAt(idx);
300                 if (!Character.isDigit(c)) {
301                     break;
302                 }
303             }
304 
305             if (pos.getIndex() == idx) {
306                 pos.setErrorIndex(idx);
307                 return false;
308             }
309 
310             final int value = Integer.parseInt(source.substring(pos.getIndex(), idx));
311             pos.setIndex(idx);
312 
313             calendar.set(field, modify(parser, value));
314             return true;
315         }
316 
317         /**
318          * Converts this instance to a handy debug string.
319          *
320          * @since 3.12.0
321          */
322         @Override
323         public String toString() {
324             return "NumberStrategy [field=" + field + "]";
325         }
326     }
327 
328     /**
329      * A strategy to parse a single field from the parsing pattern
330      */
331     private abstract static class PatternStrategy extends Strategy {
332 
333         Pattern pattern;
334 
335         void createPattern(final String regex) {
336             this.pattern = Pattern.compile(regex);
337         }
338 
339         void createPattern(final StringBuilder regex) {
340             createPattern(regex.toString());
341         }
342 
343         /**
344          * Is this field a number? The default implementation returns false.
345          *
346          * @return true, if field is a number
347          */
348         @Override
349         boolean isNumber() {
350             return false;
351         }
352 
353         @Override
354         boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) {
355             final Matcher matcher = pattern.matcher(source.substring(pos.getIndex()));
356             if (!matcher.lookingAt()) {
357                 pos.setErrorIndex(pos.getIndex());
358                 return false;
359             }
360             pos.setIndex(pos.getIndex() + matcher.end(1));
361             setCalendar(parser, calendar, matcher.group(1));
362             return true;
363         }
364 
365         abstract void setCalendar(FastDateParser parser, Calendar calendar, String value);
366 
367         /**
368          * Converts this instance to a handy debug string.
369          *
370          * @since 3.12.0
371          */
372         @Override
373         public String toString() {
374             return getClass().getSimpleName() + " [pattern=" + pattern + "]";
375         }
376 
377     }
378 
379     /**
380      * A strategy to parse a single field from the parsing pattern
381      */
382     private abstract static class Strategy {
383 
384         /**
385          * Is this field a number? The default implementation returns false.
386          *
387          * @return true, if field is a number
388          */
389         boolean isNumber() {
390             return false;
391         }
392 
393         abstract boolean parse(FastDateParser parser, Calendar calendar, String source, ParsePosition pos, int maxWidth);
394     }
395 
396     /**
397      * Holds strategy and field width
398      */
399     private static final class StrategyAndWidth {
400 
401         final Strategy strategy;
402         final int width;
403 
404         StrategyAndWidth(final Strategy strategy, final int width) {
405             this.strategy = Objects.requireNonNull(strategy, "strategy");
406             this.width = width;
407         }
408 
409         int getMaxWidth(final ListIterator<StrategyAndWidth> lt) {
410             if (!strategy.isNumber() || !lt.hasNext()) {
411                 return 0;
412             }
413             final Strategy nextStrategy = lt.next().strategy;
414             lt.previous();
415             return nextStrategy.isNumber() ? width : 0;
416         }
417 
418         @Override
419         public String toString() {
420             return "StrategyAndWidth [strategy=" + strategy + ", width=" + width + "]";
421         }
422     }
423 
424     /**
425      * Parse format into Strategies
426      */
427     private final class StrategyParser {
428         private final Calendar definingCalendar;
429         private int currentIdx;
430 
431         StrategyParser(final Calendar definingCalendar) {
432             this.definingCalendar = Objects.requireNonNull(definingCalendar, "definingCalendar");
433         }
434 
435         StrategyAndWidth getNextStrategy() {
436             if (currentIdx >= pattern.length()) {
437                 return null;
438             }
439             final char c = pattern.charAt(currentIdx);
440             if (CharUtils.isAsciiAlpha(c)) {
441                 return letterPattern(c);
442             }
443             return literal();
444         }
445 
446         private StrategyAndWidth letterPattern(final char c) {
447             final int begin = currentIdx;
448             while (++currentIdx < pattern.length()) {
449                 if (pattern.charAt(currentIdx) != c) {
450                     break;
451                 }
452             }
453             final int width = currentIdx - begin;
454             return new StrategyAndWidth(getStrategy(c, width, definingCalendar), width);
455         }
456 
457         private StrategyAndWidth literal() {
458             boolean activeQuote = false;
459 
460             final StringBuilder sb = new StringBuilder();
461             while (currentIdx < pattern.length()) {
462                 final char c = pattern.charAt(currentIdx);
463                 if (!activeQuote && CharUtils.isAsciiAlpha(c)) {
464                     break;
465                 }
466                 if (c == '\'' && (++currentIdx == pattern.length() || pattern.charAt(currentIdx) != '\'')) {
467                     activeQuote = !activeQuote;
468                     continue;
469                 }
470                 ++currentIdx;
471                 sb.append(c);
472             }
473             if (activeQuote) {
474                 throw new IllegalArgumentException("Unterminated quote");
475             }
476             final String formatField = sb.toString();
477             return new StrategyAndWidth(new CopyQuotedStrategy(formatField), formatField.length());
478         }
479     }
480 
481     /**
482      * A strategy that handles a time zone field in the parsing pattern
483      */
484     static class TimeZoneStrategy extends PatternStrategy {
485 
486         private static final class TzInfo {
487             final TimeZone zone;
488             final int dstOffset;
489 
490             TzInfo(final TimeZone tz, final boolean useDst) {
491                 zone = tz;
492                 dstOffset = useDst ? tz.getDSTSavings() : 0;
493             }
494 
495             @Override
496             public String toString() {
497                 return "TzInfo [zone=" + zone + ", dstOffset=" + dstOffset + "]";
498             }
499         }
500 
501         private static final String RFC_822_TIME_ZONE = "[+-]\\d{4}";
502 
503         private static final String GMT_OPTION = TimeZones.GMT_ID + "[+-]\\d{1,2}:\\d{2}";
504 
505         /**
506          * Index of zone id from {@link DateFormatSymbols#getZoneStrings()}.
507          */
508         private static final int ID = 0;
509 
510         /**
511          * Tests whether to skip the given time zone, true if TimeZone.getTimeZone().
512          * <p>
513          * On Java 25 and up, skips short IDs if {@code ignoreTimeZoneShortIDs} is true.
514          * </p>
515          * <p>
516          * This method is package private only for testing.
517          * </p>
518          *
519          * @param tzId the ID to test.
520          * @return Whether to skip the given time zone ID.
521          */
522         static boolean skipTimeZone(final String tzId) {
523             return tzId.equalsIgnoreCase(TimeZones.GMT_ID);
524         }
525 
526         private final Locale locale;
527 
528         /**
529          * Using lower case only or upper case only will cause problems with some Locales like Turkey, Armenia, Colognian and also depending on the Java
530          * version. For details, see https://garygregory.wordpress.com/2015/11/03/java-lowercase-conversion-turkey/
531          */
532         private final Map<String, TzInfo> tzNames = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
533 
534         /**
535          * Constructs a Strategy that parses a TimeZone.
536          *
537          * @param locale The Locale.
538          */
539         TimeZoneStrategy(final Locale locale) {
540             this.locale = LocaleUtils.toLocale(locale);
541 
542             final StringBuilder sb = new StringBuilder();
543             sb.append("((?iu)" + RFC_822_TIME_ZONE + "|" + GMT_OPTION);
544 
545             final Set<String> sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE);
546 
547             // Order is undefined.
548             // TODO Use of getZoneStrings() is discouraged per its Javadoc.
549             final String[][] zones = DateFormatSymbols.getInstance(locale).getZoneStrings();
550             for (final String[] zoneNames : zones) {
551                 // offset 0 is the time zone ID and is not localized
552                 final String tzId = zoneNames[ID];
553                 if (skipTimeZone(tzId)) {
554                     continue;
555                 }
556                 final TimeZone tz = TimeZones.getTimeZone(tzId);
557                 // offset 1 is long standard name
558                 // offset 2 is short standard name
559                 final TzInfo standard = new TzInfo(tz, false);
560                 TzInfo tzInfo = standard;
561                 for (int i = 1; i < zoneNames.length; ++i) {
562                     switch (i) {
563                     case 3: // offset 3 is long daylight savings (or summertime) name
564                             // offset 4 is the short summertime name
565                         tzInfo = new TzInfo(tz, true);
566                         break;
567                     case 5: // offset 5 starts additional names, probably standard time
568                         tzInfo = standard;
569                         break;
570                     default:
571                         break;
572                     }
573                     final String zoneName = zoneNames[i];
574                     // ignore the data associated with duplicates supplied in the additional names
575                     if (zoneName != null && sorted.add(zoneName)) {
576                         tzNames.put(zoneName, tzInfo);
577                     }
578                 }
579             }
580             // Order is undefined.
581             for (final String tzId : ArraySorter.sort(TimeZone.getAvailableIDs())) {
582                 if (skipTimeZone(tzId)) {
583                     continue;
584                 }
585                 final TimeZone tz = TimeZones.getTimeZone(tzId);
586                 final String zoneName = tz.getDisplayName(locale);
587                 if (sorted.add(zoneName)) {
588                     tzNames.put(zoneName, new TzInfo(tz, tz.observesDaylightTime()));
589                 }
590             }
591             // order the regex alternatives with longer strings first, greedy
592             // match will ensure the longest string will be consumed
593             sorted.forEach(zoneName -> simpleQuote(sb.append('|'), zoneName));
594             sb.append(")");
595             createPattern(sb);
596         }
597 
598         /**
599          * {@inheritDoc}
600          */
601         @Override
602         void setCalendar(final FastDateParser parser, final Calendar calendar, final String timeZone) {
603             final TimeZone tz = FastTimeZone.getGmtTimeZone(timeZone);
604             if (tz != null) {
605                 calendar.setTimeZone(tz);
606             } else {
607                 TzInfo tzInfo = tzNames.get(timeZone);
608                 if (tzInfo == null) {
609                     // match missing the optional trailing period
610                     tzInfo = tzNames.get(timeZone + '.');
611                     if (tzInfo == null) {
612                         // show chars in case this is multiple byte character issue
613                         final char[] charArray = timeZone.toCharArray();
614                         throw new IllegalStateException(String.format("Can't find time zone '%s' (%d %s) in %s", timeZone, charArray.length,
615                                 Arrays.toString(charArray), new TreeSet<>(tzNames.keySet())));
616                     }
617                 }
618                 calendar.set(Calendar.DST_OFFSET, tzInfo.dstOffset);
619                 calendar.set(Calendar.ZONE_OFFSET, tzInfo.zone.getRawOffset());
620             }
621         }
622 
623         /**
624          * Converts this instance to a handy debug string.
625          *
626          * @since 3.12.0
627          */
628         @Override
629         public String toString() {
630             return "TimeZoneStrategy [locale=" + locale + ", tzNames=" + tzNames + ", pattern=" + pattern + "]";
631         }
632 
633     }
634 
635     /**
636      * Required for serialization support.
637      *
638      * @see java.io.Serializable
639      */
640     private static final long serialVersionUID = 3L;
641 
642     static final Locale JAPANESE_IMPERIAL = new Locale("ja", "JP", "JP");
643 
644     /**
645      * comparator used to sort regex alternatives. Alternatives should be ordered longer first, and shorter last. ('february' before 'feb'). All entries must be
646      * lower-case by locale.
647      */
648     private static final Comparator<String> LONGER_FIRST_LOWERCASE = Comparator.reverseOrder();
649 
650     // helper classes to parse the format string
651 
652     @SuppressWarnings("unchecked") // OK because we are creating an array with no entries
653     private static final ConcurrentMap<Locale, Strategy>[] CACHES = new ConcurrentMap[Calendar.FIELD_COUNT];
654 
655     private static final Strategy ABBREVIATED_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR) {
656         /**
657          * {@inheritDoc}
658          */
659         @Override
660         int modify(final FastDateParser parser, final int iValue) {
661             return iValue < 100 ? parser.adjustYear(iValue) : iValue;
662         }
663     };
664 
665     private static final Strategy NUMBER_MONTH_STRATEGY = new NumberStrategy(Calendar.MONTH) {
666         @Override
667         int modify(final FastDateParser parser, final int iValue) {
668             return iValue - 1;
669         }
670     };
671 
672     private static final Strategy LITERAL_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR);
673 
674     private static final Strategy WEEK_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_YEAR);
675 
676     private static final Strategy WEEK_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_MONTH);
677 
678     private static final Strategy DAY_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.DAY_OF_YEAR);
679 
680     private static final Strategy DAY_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_MONTH);
681 
682     private static final Strategy DAY_OF_WEEK_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK) {
683         @Override
684         int modify(final FastDateParser parser, final int iValue) {
685             return iValue == 7 ? Calendar.SUNDAY : iValue + 1;
686         }
687     };
688 
689     private static final Strategy DAY_OF_WEEK_IN_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK_IN_MONTH);
690 
691     private static final Strategy HOUR_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY);
692 
693     private static final Strategy HOUR24_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY) {
694         @Override
695         int modify(final FastDateParser parser, final int iValue) {
696             return iValue == 24 ? 0 : iValue;
697         }
698     };
699 
700     private static final Strategy HOUR12_STRATEGY = new NumberStrategy(Calendar.HOUR) {
701         @Override
702         int modify(final FastDateParser parser, final int iValue) {
703             return iValue == 12 ? 0 : iValue;
704         }
705     };
706 
707     private static final Strategy HOUR_STRATEGY = new NumberStrategy(Calendar.HOUR);
708 
709     private static final Strategy MINUTE_STRATEGY = new NumberStrategy(Calendar.MINUTE);
710 
711     private static final Strategy SECOND_STRATEGY = new NumberStrategy(Calendar.SECOND);
712 
713     private static final Strategy MILLISECOND_STRATEGY = new NumberStrategy(Calendar.MILLISECOND);
714 
715     /**
716      * Gets the short and long values displayed for a field
717      *
718      * @param calendar The calendar to obtain the short and long values
719      * @param locale   The locale of display names
720      * @param field    The field of interest
721      * @param regex    The regular expression to build
722      * @return The map of string display names to field values
723      */
724     private static Map<String, Integer> appendDisplayNames(final Calendar calendar, final Locale locale, final int field, final StringBuilder regex) {
725         Objects.requireNonNull(calendar, "calendar");
726         final Map<String, Integer> values = new HashMap<>();
727         final Locale actualLocale = LocaleUtils.toLocale(locale);
728         final Map<String, Integer> displayNames = calendar.getDisplayNames(field, Calendar.ALL_STYLES, actualLocale);
729         final TreeSet<String> sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE);
730         displayNames.forEach((k, v) -> {
731             final String keyLc = k.toLowerCase(actualLocale);
732             if (sorted.add(keyLc)) {
733                 values.put(keyLc, v);
734             }
735         });
736         sorted.forEach(symbol -> simpleQuote(regex, symbol).append('|'));
737         return values;
738     }
739 
740     /**
741      * Clears the cache.
742      */
743     static void clear() {
744         Stream.of(CACHES).filter(Objects::nonNull).forEach(ConcurrentMap::clear);
745     }
746 
747     /**
748      * Gets a cache of Strategies for a particular field
749      *
750      * @param field The Calendar field
751      * @return a cache of Locale to Strategy
752      */
753     private static ConcurrentMap<Locale, Strategy> getCache(final int field) {
754         synchronized (CACHES) {
755             if (CACHES[field] == null) {
756                 CACHES[field] = new ConcurrentHashMap<>(3);
757             }
758             return CACHES[field];
759         }
760     }
761 
762     private static StringBuilder simpleQuote(final StringBuilder sb, final String value) {
763         for (int i = 0; i < value.length(); ++i) {
764             final char c = value.charAt(i);
765             switch (c) {
766             case '\\':
767             case '^':
768             case '$':
769             case '.':
770             case '|':
771             case '?':
772             case '*':
773             case '+':
774             case '(':
775             case ')':
776             case '[':
777             case '{':
778                 sb.append('\\');
779                 // falls-through
780             default:
781                 sb.append(c);
782             }
783         }
784         if (sb.charAt(sb.length() - 1) == '.') {
785             // trailing '.' is optional
786             sb.append('?');
787         }
788         return sb;
789     }
790 
791     /** Input pattern. */
792     private final String pattern;
793 
794     /** Input TimeZone. */
795     private final TimeZone timeZone;
796 
797     /** Input Locale. */
798     private final Locale locale;
799 
800     /**
801      * Century from Date.
802      */
803     private final int century;
804 
805     /**
806      * Start year from Date.
807      */
808     private final int startYear;
809 
810     /** Initialized from Calendar. */
811     private transient List<StrategyAndWidth> patterns;
812 
813     /**
814      * Constructs a new FastDateParser.
815      *
816      * Use {@link FastDateFormat#getInstance(String, TimeZone, Locale)} or another variation of the factory methods of {@link FastDateFormat} to get a cached
817      * FastDateParser instance.
818      *
819      * @param pattern  non-null {@link java.text.SimpleDateFormat} compatible pattern
820      * @param timeZone non-null time zone to use
821      * @param locale   non-null locale
822      */
823     protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale) {
824         this(pattern, timeZone, locale, null);
825     }
826 
827     /**
828      * Constructs a new FastDateParser.
829      *
830      * @param pattern      non-null {@link java.text.SimpleDateFormat} compatible pattern
831      * @param timeZone     non-null time zone to use
832      * @param locale       locale, null maps to the default Locale.
833      * @param centuryStart The start of the century for 2 digit year parsing
834      * @since 3.5
835      */
836     protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale, final Date centuryStart) {
837         this.pattern = Objects.requireNonNull(pattern, "pattern");
838         this.timeZone = Objects.requireNonNull(timeZone, "timeZone");
839         this.locale = LocaleUtils.toLocale(locale);
840         final Calendar definingCalendar = Calendar.getInstance(timeZone, this.locale);
841         final int centuryStartYear;
842         if (centuryStart != null) {
843             definingCalendar.setTime(centuryStart);
844             centuryStartYear = definingCalendar.get(Calendar.YEAR);
845         } else if (this.locale.equals(JAPANESE_IMPERIAL)) {
846             centuryStartYear = 0;
847         } else {
848             // from 80 years ago to 20 years from now
849             definingCalendar.setTime(new Date());
850             centuryStartYear = definingCalendar.get(Calendar.YEAR) - 80;
851         }
852         century = centuryStartYear / 100 * 100;
853         startYear = centuryStartYear - century;
854         init(definingCalendar);
855     }
856 
857     /**
858      * Adjusts dates to be within appropriate century
859      *
860      * @param twoDigitYear The year to adjust
861      * @return A value between centuryStart(inclusive) to centuryStart+100(exclusive)
862      */
863     private int adjustYear(final int twoDigitYear) {
864         final int trial = century + twoDigitYear;
865         return twoDigitYear >= startYear ? trial : trial + 100;
866     }
867 
868     /**
869      * Compares another object for equality with this object.
870      *
871      * @param obj the object to compare to
872      * @return {@code true}if equal to this instance
873      */
874     @Override
875     public boolean equals(final Object obj) {
876         if (!(obj instanceof FastDateParser)) {
877             return false;
878         }
879         final FastDateParser other = (FastDateParser) obj;
880         return pattern.equals(other.pattern) && timeZone.equals(other.timeZone) && locale.equals(other.locale);
881     }
882 
883     /*
884      * (non-Javadoc)
885      *
886      * @see org.apache.commons.lang3.time.DateParser#getLocale()
887      */
888     @Override
889     public Locale getLocale() {
890         return locale;
891     }
892 
893     /**
894      * Constructs a Strategy that parses a Text field
895      *
896      * @param field            The Calendar field
897      * @param definingCalendar The calendar to obtain the short and long values
898      * @return a TextStrategy for the field and Locale
899      */
900     private Strategy getLocaleSpecificStrategy(final int field, final Calendar definingCalendar) {
901         final ConcurrentMap<Locale, Strategy> cache = getCache(field);
902         return cache.computeIfAbsent(locale,
903                 k -> field == Calendar.ZONE_OFFSET ? new TimeZoneStrategy(locale) : new CaseInsensitiveTextStrategy(field, definingCalendar, locale));
904     }
905 
906     /*
907      * (non-Javadoc)
908      *
909      * @see org.apache.commons.lang3.time.DateParser#getPattern()
910      */
911     @Override
912     public String getPattern() {
913         return pattern;
914     }
915     /**
916      * Gets a Strategy given a field from a SimpleDateFormat pattern
917      *
918      * @param f                A sub-sequence of the SimpleDateFormat pattern
919      * @param width            formatting width
920      * @param definingCalendar The calendar to obtain the short and long values
921      * @return The Strategy that will handle parsing for the field
922      */
923     private Strategy getStrategy(final char f, final int width, final Calendar definingCalendar) {
924         switch (f) {
925         case 'D':
926             return DAY_OF_YEAR_STRATEGY;
927         case 'E':
928             return getLocaleSpecificStrategy(Calendar.DAY_OF_WEEK, definingCalendar);
929         case 'F':
930             return DAY_OF_WEEK_IN_MONTH_STRATEGY;
931         case 'G':
932             return getLocaleSpecificStrategy(Calendar.ERA, definingCalendar);
933         case 'H': // Hour in day (0-23)
934             return HOUR_OF_DAY_STRATEGY;
935         case 'K': // Hour in am/pm (0-11)
936             return HOUR_STRATEGY;
937         case 'M':
938         case 'L':
939             return width >= 3 ? getLocaleSpecificStrategy(Calendar.MONTH, definingCalendar) : NUMBER_MONTH_STRATEGY;
940         case 'S':
941             return MILLISECOND_STRATEGY;
942         case 'W':
943             return WEEK_OF_MONTH_STRATEGY;
944         case 'a':
945             return getLocaleSpecificStrategy(Calendar.AM_PM, definingCalendar);
946         case 'd':
947             return DAY_OF_MONTH_STRATEGY;
948         case 'h': // Hour in am/pm (1-12), i.e. midday/midnight is 12, not 0
949             return HOUR12_STRATEGY;
950         case 'k': // Hour in day (1-24), i.e. midnight is 24, not 0
951             return HOUR24_OF_DAY_STRATEGY;
952         case 'm':
953             return MINUTE_STRATEGY;
954         case 's':
955             return SECOND_STRATEGY;
956         case 'u':
957             return DAY_OF_WEEK_STRATEGY;
958         case 'w':
959             return WEEK_OF_YEAR_STRATEGY;
960         case 'y':
961         case 'Y':
962             return width > 2 ? LITERAL_YEAR_STRATEGY : ABBREVIATED_YEAR_STRATEGY;
963         case 'X':
964             return ISO8601TimeZoneStrategy.getStrategy(width);
965         case 'Z':
966             if (width == 2) {
967                 return ISO8601TimeZoneStrategy.ISO_8601_3_STRATEGY;
968             }
969             // falls-through
970         case 'z':
971             return getLocaleSpecificStrategy(Calendar.ZONE_OFFSET, definingCalendar);
972         default:
973             throw new IllegalArgumentException("Format '" + f + "' not supported");
974         }
975     }
976 
977     /*
978      * (non-Javadoc)
979      *
980      * @see org.apache.commons.lang3.time.DateParser#getTimeZone()
981      */
982     @Override
983     public TimeZone getTimeZone() {
984         return timeZone;
985     }
986 
987     /**
988      * Returns a hash code compatible with equals.
989      *
990      * @return a hash code compatible with equals
991      */
992     @Override
993     public int hashCode() {
994         return pattern.hashCode() + 13 * (timeZone.hashCode() + 13 * locale.hashCode());
995     }
996 
997     /**
998      * Initializes derived fields from defining fields. This is called from constructor and from readObject (de-serialization)
999      *
1000      * @param definingCalendar the {@link java.util.Calendar} instance used to initialize this FastDateParser
1001      */
1002     private void init(final Calendar definingCalendar) {
1003         patterns = new ArrayList<>();
1004 
1005         final StrategyParser strategyParser = new StrategyParser(definingCalendar);
1006         for (;;) {
1007             final StrategyAndWidth field = strategyParser.getNextStrategy();
1008             if (field == null) {
1009                 break;
1010             }
1011             patterns.add(field);
1012         }
1013     }
1014 
1015     /*
1016      * (non-Javadoc)
1017      *
1018      * @see org.apache.commons.lang3.time.DateParser#parse(String)
1019      */
1020     @Override
1021     public Date parse(final String source) throws ParseException {
1022         final ParsePosition pp = new ParsePosition(0);
1023         final Date date = parse(source, pp);
1024         if (date == null) {
1025             // Add a note regarding supported date range
1026             if (locale.equals(JAPANESE_IMPERIAL)) {
1027                 throw new ParseException("(The " + locale + " locale does not support dates before 1868 AD)\nUnparseable date: \"" + source,
1028                         pp.getErrorIndex());
1029             }
1030             throw new ParseException("Unparseable date: " + source, pp.getErrorIndex());
1031         }
1032         return date;
1033     }
1034     /**
1035      * This implementation updates the ParsePosition if the parse succeeds. However, it sets the error index to the position before the failed field unlike the
1036      * method {@link java.text.SimpleDateFormat#parse(String, ParsePosition)} which sets the error index to after the failed field.
1037      * <p>
1038      * To determine if the parse has succeeded, the caller must check if the current parse position given by {@link ParsePosition#getIndex()} has been updated.
1039      * If the input buffer has been fully parsed, then the index will point to just after the end of the input buffer.
1040      * </p>
1041      *
1042      * @see org.apache.commons.lang3.time.DateParser#parse(String, java.text.ParsePosition)
1043      */
1044     @Override
1045     public Date parse(final String source, final ParsePosition pos) {
1046         // timing tests indicate getting new instance is 19% faster than cloning
1047         final Calendar cal = Calendar.getInstance(timeZone, locale);
1048         cal.clear();
1049         return parse(source, pos, cal) ? cal.getTime() : null;
1050     }
1051     /**
1052      * Parses a formatted date string according to the format. Updates the Calendar with parsed fields. Upon success, the ParsePosition index is updated to
1053      * indicate how much of the source text was consumed. Not all source text needs to be consumed. Upon parse failure, ParsePosition error index is updated to
1054      * the offset of the source text which does not match the supplied format.
1055      *
1056      * @param source   The text to parse.
1057      * @param pos      On input, the position in the source to start parsing, on output, updated position.
1058      * @param calendar The calendar into which to set parsed fields.
1059      * @return true, if source has been parsed (pos parsePosition is updated); otherwise false (and pos errorIndex is updated)
1060      * @throws IllegalArgumentException when Calendar has been set to be not lenient, and a parsed field is out of range.
1061      */
1062     @Override
1063     public boolean parse(final String source, final ParsePosition pos, final Calendar calendar) {
1064         final ListIterator<StrategyAndWidth> lt = patterns.listIterator();
1065         while (lt.hasNext()) {
1066             final StrategyAndWidth strategyAndWidth = lt.next();
1067             final int maxWidth = strategyAndWidth.getMaxWidth(lt);
1068             if (!strategyAndWidth.strategy.parse(this, calendar, source, pos, maxWidth)) {
1069                 return false;
1070             }
1071         }
1072         return true;
1073     }
1074 
1075     /*
1076      * (non-Javadoc)
1077      *
1078      * @see org.apache.commons.lang3.time.DateParser#parseObject(String)
1079      */
1080     @Override
1081     public Object parseObject(final String source) throws ParseException {
1082         return parse(source);
1083     }
1084 
1085     /*
1086      * (non-Javadoc)
1087      *
1088      * @see org.apache.commons.lang3.time.DateParser#parseObject(String, java.text.ParsePosition)
1089      */
1090     @Override
1091     public Object parseObject(final String source, final ParsePosition pos) {
1092         return parse(source, pos);
1093     }
1094 
1095     // Serializing
1096     /**
1097      * Creates the object after serialization. This implementation reinitializes the transient properties.
1098      *
1099      * @param in ObjectInputStream from which the object is being deserialized.
1100      * @throws IOException            if there is an IO issue.
1101      * @throws ClassNotFoundException if a class cannot be found.
1102      */
1103     private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
1104         in.defaultReadObject();
1105         final Calendar definingCalendar = Calendar.getInstance(timeZone, locale);
1106         init(definingCalendar);
1107     }
1108 
1109     /**
1110      * Gets a string version of this formatter.
1111      *
1112      * @return a debugging string
1113      */
1114     @Override
1115     public String toString() {
1116         return "FastDateParser[" + pattern + ", " + locale + ", " + timeZone.getID() + "]";
1117     }
1118 
1119     /**
1120      * Converts all state of this instance to a String handy for debugging.
1121      *
1122      * @return a string.
1123      * @since 3.12.0
1124      */
1125     public String toStringAll() {
1126         return "FastDateParser [pattern=" + pattern + ", timeZone=" + timeZone + ", locale=" + locale + ", century=" + century + ", startYear=" + startYear
1127                 + ", patterns=" + patterns + "]";
1128     }
1129 }