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