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