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