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        return sb;
452    }
453
454    /**
455     * Get the short and long values displayed for a field
456     * @param cal The calendar to obtain the short and long values
457     * @param locale The locale of display names
458     * @param field The field of interest
459     * @param regex The regular expression to build
460     * @return The map of string display names to field values
461     */
462    private static Map<String, Integer> appendDisplayNames(final Calendar cal, final Locale locale, final int field, final StringBuilder regex) {
463        final Map<String, Integer> values = new HashMap<>();
464
465        final Map<String, Integer> displayNames = cal.getDisplayNames(field, Calendar.ALL_STYLES, locale);
466        final TreeSet<String> sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE);
467        for (final Map.Entry<String, Integer> displayName : displayNames.entrySet()) {
468            final String key = displayName.getKey().toLowerCase(locale);
469            if (sorted.add(key)) {
470                values.put(key, displayName.getValue());
471            }
472        }
473        for (final String symbol : sorted) {
474            simpleQuote(regex, symbol).append('|');
475        }
476        return values;
477    }
478
479    /**
480     * Adjust dates to be within appropriate century
481     * @param twoDigitYear The year to adjust
482     * @return A value between centuryStart(inclusive) to centuryStart+100(exclusive)
483     */
484    private int adjustYear(final int twoDigitYear) {
485        final int trial = century + twoDigitYear;
486        return twoDigitYear >= startYear ? trial : trial + 100;
487    }
488
489    /**
490     * A strategy to parse a single field from the parsing pattern
491     */
492    private abstract static class Strategy {
493        /**
494         * Is this field a number?
495         * The default implementation returns false.
496         *
497         * @return true, if field is a number
498         */
499        boolean isNumber() {
500            return false;
501        }
502
503        abstract boolean parse(FastDateParser parser, Calendar calendar, String source, ParsePosition pos, int maxWidth);
504    }
505
506    /**
507     * A strategy to parse a single field from the parsing pattern
508     */
509    private abstract static class PatternStrategy extends Strategy {
510
511        private Pattern pattern;
512
513        void createPattern(final StringBuilder regex) {
514            createPattern(regex.toString());
515        }
516
517        void createPattern(final String regex) {
518            this.pattern = Pattern.compile(regex);
519        }
520
521        /**
522         * Is this field a number?
523         * The default implementation returns false.
524         *
525         * @return true, if field is a number
526         */
527        @Override
528        boolean isNumber() {
529            return false;
530        }
531
532        @Override
533        boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) {
534            final Matcher matcher = pattern.matcher(source.substring(pos.getIndex()));
535            if (!matcher.lookingAt()) {
536                pos.setErrorIndex(pos.getIndex());
537                return false;
538            }
539            pos.setIndex(pos.getIndex() + matcher.end(1));
540            setCalendar(parser, calendar, matcher.group(1));
541            return true;
542        }
543
544        abstract void setCalendar(FastDateParser parser, Calendar cal, String value);
545    }
546
547    /**
548     * Obtain a Strategy given a field from a SimpleDateFormat pattern
549     * @param f A sub-sequence of the SimpleDateFormat pattern
550     * @param definingCalendar The calendar to obtain the short and long values
551     * @return The Strategy that will handle parsing for the field
552     */
553    private Strategy getStrategy(final char f, final int width, final Calendar definingCalendar) {
554        switch(f) {
555        default:
556            throw new IllegalArgumentException("Format '"+f+"' not supported");
557        case 'D':
558            return DAY_OF_YEAR_STRATEGY;
559        case 'E':
560            return getLocaleSpecificStrategy(Calendar.DAY_OF_WEEK, definingCalendar);
561        case 'F':
562            return DAY_OF_WEEK_IN_MONTH_STRATEGY;
563        case 'G':
564            return getLocaleSpecificStrategy(Calendar.ERA, definingCalendar);
565        case 'H':  // Hour in day (0-23)
566            return HOUR_OF_DAY_STRATEGY;
567        case 'K':  // Hour in am/pm (0-11)
568            return HOUR_STRATEGY;
569        case 'M':
570            return width>=3 ?getLocaleSpecificStrategy(Calendar.MONTH, definingCalendar) :NUMBER_MONTH_STRATEGY;
571        case 'S':
572            return MILLISECOND_STRATEGY;
573        case 'W':
574            return WEEK_OF_MONTH_STRATEGY;
575        case 'a':
576            return getLocaleSpecificStrategy(Calendar.AM_PM, definingCalendar);
577        case 'd':
578            return DAY_OF_MONTH_STRATEGY;
579        case 'h':  // Hour in am/pm (1-12), i.e. midday/midnight is 12, not 0
580            return HOUR12_STRATEGY;
581        case 'k':  // Hour in day (1-24), i.e. midnight is 24, not 0
582            return HOUR24_OF_DAY_STRATEGY;
583        case 'm':
584            return MINUTE_STRATEGY;
585        case 's':
586            return SECOND_STRATEGY;
587        case 'u':
588            return DAY_OF_WEEK_STRATEGY;
589        case 'w':
590            return WEEK_OF_YEAR_STRATEGY;
591        case 'y':
592        case 'Y':
593            return width>2 ?LITERAL_YEAR_STRATEGY :ABBREVIATED_YEAR_STRATEGY;
594        case 'X':
595            return ISO8601TimeZoneStrategy.getStrategy(width);
596        case 'Z':
597            if (width==2) {
598                return ISO8601TimeZoneStrategy.ISO_8601_3_STRATEGY;
599            }
600            //$FALL-THROUGH$
601        case 'z':
602            return getLocaleSpecificStrategy(Calendar.ZONE_OFFSET, definingCalendar);
603        }
604    }
605
606    @SuppressWarnings("unchecked") // OK because we are creating an array with no entries
607    private static final ConcurrentMap<Locale, Strategy>[] caches = new ConcurrentMap[Calendar.FIELD_COUNT];
608
609    /**
610     * Get a cache of Strategies for a particular field
611     * @param field The Calendar field
612     * @return a cache of Locale to Strategy
613     */
614    private static ConcurrentMap<Locale, Strategy> getCache(final int field) {
615        synchronized (caches) {
616            if (caches[field] == null) {
617                caches[field] = new ConcurrentHashMap<>(3);
618            }
619            return caches[field];
620        }
621    }
622
623    /**
624     * Construct a Strategy that parses a Text field
625     * @param field The Calendar field
626     * @param definingCalendar The calendar to obtain the short and long values
627     * @return a TextStrategy for the field and Locale
628     */
629    private Strategy getLocaleSpecificStrategy(final int field, final Calendar definingCalendar) {
630        final ConcurrentMap<Locale, Strategy> cache = getCache(field);
631        Strategy strategy = cache.get(locale);
632        if (strategy == null) {
633            strategy = field == Calendar.ZONE_OFFSET
634                    ? new TimeZoneStrategy(locale)
635                    : new CaseInsensitiveTextStrategy(field, definingCalendar, locale);
636            final Strategy inCache = cache.putIfAbsent(locale, strategy);
637            if (inCache != null) {
638                return inCache;
639            }
640        }
641        return strategy;
642    }
643
644    /**
645     * A strategy that copies the static or quoted field in the parsing pattern
646     */
647    private static class CopyQuotedStrategy extends Strategy {
648
649        private final String formatField;
650
651        /**
652         * Construct a Strategy that ensures the formatField has literal text
653         * @param formatField The literal text to match
654         */
655        CopyQuotedStrategy(final String formatField) {
656            this.formatField = formatField;
657        }
658
659        /**
660         * {@inheritDoc}
661         */
662        @Override
663        boolean isNumber() {
664            return false;
665        }
666
667        @Override
668        boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) {
669            for (int idx = 0; idx < formatField.length(); ++idx) {
670                final int sIdx = idx + pos.getIndex();
671                if (sIdx == source.length()) {
672                    pos.setErrorIndex(sIdx);
673                    return false;
674                }
675                if (formatField.charAt(idx) != source.charAt(sIdx)) {
676                    pos.setErrorIndex(sIdx);
677                    return false;
678                }
679            }
680            pos.setIndex(formatField.length() + pos.getIndex());
681            return true;
682        }
683    }
684
685    /**
686     * A strategy that handles a text field in the parsing pattern
687     */
688     private static class CaseInsensitiveTextStrategy extends PatternStrategy {
689        private final int field;
690        final Locale locale;
691        private final Map<String, Integer> lKeyValues;
692
693        /**
694         * Construct a Strategy that parses a Text field
695         * @param field  The Calendar field
696         * @param definingCalendar  The Calendar to use
697         * @param locale  The Locale to use
698         */
699        CaseInsensitiveTextStrategy(final int field, final Calendar definingCalendar, final Locale locale) {
700            this.field = field;
701            this.locale = locale;
702
703            final StringBuilder regex = new StringBuilder();
704            regex.append("((?iu)");
705            lKeyValues = appendDisplayNames(definingCalendar, locale, field, regex);
706            regex.setLength(regex.length()-1);
707            regex.append(")");
708            createPattern(regex);
709        }
710
711        /**
712         * {@inheritDoc}
713         */
714        @Override
715        void setCalendar(final FastDateParser parser, final Calendar cal, final String value) {
716            final Integer iVal = lKeyValues.get(value.toLowerCase(locale));
717            cal.set(field, iVal.intValue());
718        }
719    }
720
721
722    /**
723     * A strategy that handles a number field in the parsing pattern
724     */
725    private static class NumberStrategy extends Strategy {
726        private final int field;
727
728        /**
729         * Construct a Strategy that parses a Number field
730         * @param field The Calendar field
731         */
732        NumberStrategy(final int field) {
733             this.field= field;
734        }
735
736        /**
737         * {@inheritDoc}
738         */
739        @Override
740        boolean isNumber() {
741            return true;
742        }
743
744        @Override
745        boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) {
746            int idx = pos.getIndex();
747            int last = source.length();
748
749            if (maxWidth == 0) {
750                // if no maxWidth, strip leading white space
751                for (; idx < last; ++idx) {
752                    final char c = source.charAt(idx);
753                    if (!Character.isWhitespace(c)) {
754                        break;
755                    }
756                }
757                pos.setIndex(idx);
758            } else {
759                final int end = idx + maxWidth;
760                if (last > end) {
761                    last = end;
762                }
763            }
764
765            for (; idx < last; ++idx) {
766                final char c = source.charAt(idx);
767                if (!Character.isDigit(c)) {
768                    break;
769                }
770            }
771
772            if (pos.getIndex() == idx) {
773                pos.setErrorIndex(idx);
774                return false;
775            }
776
777            final int value = Integer.parseInt(source.substring(pos.getIndex(), idx));
778            pos.setIndex(idx);
779
780            calendar.set(field, modify(parser, value));
781            return true;
782        }
783
784        /**
785         * Make any modifications to parsed integer
786         * @param parser The parser
787         * @param iValue The parsed integer
788         * @return The modified value
789         */
790        int modify(final FastDateParser parser, final int iValue) {
791            return iValue;
792        }
793
794    }
795
796    private static final Strategy ABBREVIATED_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR) {
797        /**
798         * {@inheritDoc}
799         */
800        @Override
801        int modify(final FastDateParser parser, final int iValue) {
802            return iValue < 100 ? parser.adjustYear(iValue) : iValue;
803        }
804    };
805
806    /**
807     * A strategy that handles a timezone field in the parsing pattern
808     */
809    static class TimeZoneStrategy extends PatternStrategy {
810        private static final String RFC_822_TIME_ZONE = "[+-]\\d{4}";
811        private static final String GMT_OPTION= "GMT[+-]\\d{1,2}:\\d{2}";
812
813        private final Locale locale;
814        private final Map<String, TzInfo> tzNames= new HashMap<>();
815
816        private static class TzInfo {
817            TimeZone zone;
818            int dstOffset;
819
820            TzInfo(final TimeZone tz, final boolean useDst) {
821                zone = tz;
822                dstOffset = useDst ?tz.getDSTSavings() :0;
823            }
824        }
825
826        /**
827         * Index of zone id
828         */
829        private static final int ID = 0;
830
831        /**
832         * Construct a Strategy that parses a TimeZone
833         * @param locale The Locale
834         */
835        TimeZoneStrategy(final Locale locale) {
836            this.locale = locale;
837
838            final StringBuilder sb = new StringBuilder();
839            sb.append("((?iu)" + RFC_822_TIME_ZONE + "|" + GMT_OPTION );
840
841            final Set<String> sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE);
842
843            final String[][] zones = DateFormatSymbols.getInstance(locale).getZoneStrings();
844            for (final String[] zoneNames : zones) {
845                // offset 0 is the time zone ID and is not localized
846                final String tzId = zoneNames[ID];
847                if (tzId.equalsIgnoreCase("GMT")) {
848                    continue;
849                }
850                final TimeZone tz = TimeZone.getTimeZone(tzId);
851                // offset 1 is long standard name
852                // offset 2 is short standard name
853                final TzInfo standard = new TzInfo(tz, false);
854                TzInfo tzInfo = standard;
855                for (int i = 1; i < zoneNames.length; ++i) {
856                    switch (i) {
857                    case 3: // offset 3 is long daylight savings (or summertime) name
858                            // offset 4 is the short summertime name
859                        tzInfo = new TzInfo(tz, true);
860                        break;
861                    case 5: // offset 5 starts additional names, probably standard time
862                        tzInfo = standard;
863                        break;
864                    default:
865                        break;
866                    }
867                    if (zoneNames[i] != null) {
868                        final String key = zoneNames[i].toLowerCase(locale);
869                        // ignore the data associated with duplicates supplied in
870                        // the additional names
871                        if (sorted.add(key)) {
872                            tzNames.put(key, tzInfo);
873                        }
874                    }
875                }
876            }
877            // order the regex alternatives with longer strings first, greedy
878            // match will ensure longest string will be consumed
879            for (final String zoneName : sorted) {
880                simpleQuote(sb.append('|'), zoneName);
881            }
882            sb.append(")");
883            createPattern(sb);
884        }
885
886        /**
887         * {@inheritDoc}
888         */
889        @Override
890        void setCalendar(final FastDateParser parser, final Calendar cal, final String value) {
891            if (value.charAt(0) == '+' || value.charAt(0) == '-') {
892                final TimeZone tz = TimeZone.getTimeZone("GMT" + value);
893                cal.setTimeZone(tz);
894            } else if (value.regionMatches(true, 0, "GMT", 0, 3)) {
895                final TimeZone tz = TimeZone.getTimeZone(value.toUpperCase());
896                cal.setTimeZone(tz);
897            } else {
898                final TzInfo tzInfo = tzNames.get(value.toLowerCase(locale));
899                cal.set(Calendar.DST_OFFSET, tzInfo.dstOffset);
900                cal.set(Calendar.ZONE_OFFSET, tzInfo.zone.getRawOffset());
901            }
902        }
903    }
904
905    private static class ISO8601TimeZoneStrategy extends PatternStrategy {
906        // Z, +hh, -hh, +hhmm, -hhmm, +hh:mm or -hh:mm
907
908        /**
909         * Construct a Strategy that parses a TimeZone
910         * @param pattern The Pattern
911         */
912        ISO8601TimeZoneStrategy(final String pattern) {
913            createPattern(pattern);
914        }
915
916        /**
917         * {@inheritDoc}
918         */
919        @Override
920        void setCalendar(final FastDateParser parser, final Calendar cal, final String value) {
921            if (value.equals("Z")) {
922                cal.setTimeZone(TimeZone.getTimeZone("UTC"));
923            } else {
924                cal.setTimeZone(TimeZone.getTimeZone("GMT" + value));
925            }
926        }
927
928        private static final Strategy ISO_8601_1_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}))");
929        private static final Strategy ISO_8601_2_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}\\d{2}))");
930        private static final Strategy ISO_8601_3_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}(?::)\\d{2}))");
931
932        /**
933         * Factory method for ISO8601TimeZoneStrategies.
934         *
935         * @param tokenLen a token indicating the length of the TimeZone String to be formatted.
936         * @return a ISO8601TimeZoneStrategy that can format TimeZone String of length {@code tokenLen}. If no such
937         *          strategy exists, an IllegalArgumentException will be thrown.
938         */
939        static Strategy getStrategy(final int tokenLen) {
940            switch(tokenLen) {
941            case 1:
942                return ISO_8601_1_STRATEGY;
943            case 2:
944                return ISO_8601_2_STRATEGY;
945            case 3:
946                return ISO_8601_3_STRATEGY;
947            default:
948                throw new IllegalArgumentException("invalid number of X");
949            }
950        }
951    }
952
953    private static final Strategy NUMBER_MONTH_STRATEGY = new NumberStrategy(Calendar.MONTH) {
954        @Override
955        int modify(final FastDateParser parser, final int iValue) {
956            return iValue-1;
957        }
958    };
959    private static final Strategy LITERAL_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR);
960    private static final Strategy WEEK_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_YEAR);
961    private static final Strategy WEEK_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_MONTH);
962    private static final Strategy DAY_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.DAY_OF_YEAR);
963    private static final Strategy DAY_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_MONTH);
964    private static final Strategy DAY_OF_WEEK_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK) {
965        @Override
966        int modify(final FastDateParser parser, final int iValue) {
967            return iValue != 7 ? iValue + 1 : Calendar.SUNDAY;
968        }
969    };
970    private static final Strategy DAY_OF_WEEK_IN_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK_IN_MONTH);
971    private static final Strategy HOUR_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY);
972    private static final Strategy HOUR24_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY) {
973        @Override
974        int modify(final FastDateParser parser, final int iValue) {
975            return iValue == 24 ? 0 : iValue;
976        }
977    };
978    private static final Strategy HOUR12_STRATEGY = new NumberStrategy(Calendar.HOUR) {
979        @Override
980        int modify(final FastDateParser parser, final int iValue) {
981            return iValue == 12 ? 0 : iValue;
982        }
983    };
984    private static final Strategy HOUR_STRATEGY = new NumberStrategy(Calendar.HOUR);
985    private static final Strategy MINUTE_STRATEGY = new NumberStrategy(Calendar.MINUTE);
986    private static final Strategy SECOND_STRATEGY = new NumberStrategy(Calendar.SECOND);
987    private static final Strategy MILLISECOND_STRATEGY = new NumberStrategy(Calendar.MILLISECOND);
988}