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}