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