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