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