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