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