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