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