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(String left, 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        }
142        else if(locale.equals(JAPANESE_IMPERIAL)) {
143            centuryStartYear= 0;
144        }
145        else {
146            // from 80 years ago to 20 years from now
147            definingCalendar.setTime(new Date());
148            centuryStartYear= definingCalendar.get(Calendar.YEAR)-80;
149        }
150        century= centuryStartYear / 100 * 100;
151        startYear= centuryStartYear - century;
152
153        init(definingCalendar);
154    }
155
156    /**
157     * Initialize derived fields from defining fields.
158     * This is called from constructor and from readObject (de-serialization)
159     *
160     * @param definingCalendar the {@link java.util.Calendar} instance used to initialize this FastDateParser
161     */
162    private void init(final Calendar definingCalendar) {
163        patterns = new ArrayList<StrategyAndWidth>();
164
165        StrategyParser fm = new StrategyParser(pattern, definingCalendar);
166        for(;;) {
167            StrategyAndWidth field = fm.getNextStrategy();
168            if(field==null) {
169                break;
170            }
171            patterns.add(field);
172        }
173    }
174
175    // helper classes to parse the format string
176    //-----------------------------------------------------------------------
177
178    /**
179     * Holds strategy and field width
180     */
181    private static class StrategyAndWidth {
182        final Strategy strategy;
183        final int width;
184
185        StrategyAndWidth(Strategy strategy, int width) {
186            this.strategy = strategy;
187            this.width = width;
188        }
189
190        int getMaxWidth(ListIterator<StrategyAndWidth> lt) {
191            if(!strategy.isNumber() || !lt.hasNext()) {
192                return 0;
193            }
194            Strategy nextStrategy = lt.next().strategy;
195            lt.previous();
196            return nextStrategy.isNumber() ?width :0;
197       }
198    }
199
200    /**
201     * Parse format into Strategies
202     */
203    private class StrategyParser {
204        final private String pattern;
205        final private Calendar definingCalendar;
206        private int currentIdx;
207
208        StrategyParser(String pattern, Calendar definingCalendar) {
209            this.pattern = pattern;
210            this.definingCalendar = definingCalendar;
211        }
212
213        StrategyAndWidth getNextStrategy() {
214            if (currentIdx >= pattern.length()) {
215                return null;
216            }
217
218            char c = pattern.charAt(currentIdx);
219            if (isFormatLetter(c)) {
220                return letterPattern(c);
221            }
222            return literal();
223        }
224
225        private StrategyAndWidth letterPattern(char c) {
226            int begin = currentIdx;
227            while (++currentIdx < pattern.length()) {
228                if (pattern.charAt(currentIdx) != c) {
229                    break;
230                }
231            }
232
233            int width = currentIdx - begin;
234            return new StrategyAndWidth(getStrategy(c, width, definingCalendar), width);
235        }
236
237        private StrategyAndWidth literal() {
238            boolean activeQuote = false;
239
240            StringBuilder sb = new StringBuilder();
241            while (currentIdx < pattern.length()) {
242                char c = pattern.charAt(currentIdx);
243                if (!activeQuote && isFormatLetter(c)) {
244                    break;
245                } else if (c == '\'' && (++currentIdx == pattern.length() || pattern.charAt(currentIdx) != '\'')) {
246                    activeQuote = !activeQuote;
247                    continue;
248                }
249                ++currentIdx;
250                sb.append(c);
251            }
252
253            if (activeQuote) {
254                throw new IllegalArgumentException("Unterminated quote");
255            }
256
257            String formatField = sb.toString();
258            return new StrategyAndWidth(new CopyQuotedStrategy(formatField), formatField.length());
259        }
260    }
261
262    private static boolean isFormatLetter(char c) {
263        return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z';
264    }
265
266    // Accessors
267    //-----------------------------------------------------------------------
268    /* (non-Javadoc)
269     * @see org.apache.commons.lang3.time.DateParser#getPattern()
270     */
271    @Override
272    public String getPattern() {
273        return pattern;
274    }
275
276    /* (non-Javadoc)
277     * @see org.apache.commons.lang3.time.DateParser#getTimeZone()
278     */
279    @Override
280    public TimeZone getTimeZone() {
281        return timeZone;
282    }
283
284    /* (non-Javadoc)
285     * @see org.apache.commons.lang3.time.DateParser#getLocale()
286     */
287    @Override
288    public Locale getLocale() {
289        return locale;
290    }
291
292
293    // Basics
294    //-----------------------------------------------------------------------
295    /**
296     * <p>Compare another object for equality with this object.</p>
297     *
298     * @param obj  the object to compare to
299     * @return <code>true</code>if equal to this instance
300     */
301    @Override
302    public boolean equals(final Object obj) {
303        if (!(obj instanceof FastDateParser)) {
304            return false;
305        }
306        final FastDateParser other = (FastDateParser) obj;
307        return pattern.equals(other.pattern)
308            && timeZone.equals(other.timeZone)
309            && locale.equals(other.locale);
310    }
311
312    /**
313     * <p>Return a hashcode compatible with equals.</p>
314     *
315     * @return a hashcode compatible with equals
316     */
317    @Override
318    public int hashCode() {
319        return pattern.hashCode() + 13 * (timeZone.hashCode() + 13 * locale.hashCode());
320    }
321
322    /**
323     * <p>Get a string version of this formatter.</p>
324     *
325     * @return a debugging string
326     */
327    @Override
328    public String toString() {
329        return "FastDateParser[" + pattern + "," + locale + "," + timeZone.getID() + "]";
330    }
331
332    // Serializing
333    //-----------------------------------------------------------------------
334    /**
335     * Create the object after serialization. This implementation reinitializes the
336     * transient properties.
337     *
338     * @param in ObjectInputStream from which the object is being deserialized.
339     * @throws IOException if there is an IO issue.
340     * @throws ClassNotFoundException if a class cannot be found.
341     */
342    private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
343        in.defaultReadObject();
344
345        final Calendar definingCalendar = Calendar.getInstance(timeZone, locale);
346        init(definingCalendar);
347    }
348
349    /* (non-Javadoc)
350     * @see org.apache.commons.lang3.time.DateParser#parseObject(java.lang.String)
351     */
352    @Override
353    public Object parseObject(final String source) throws ParseException {
354        return parse(source);
355    }
356
357    /* (non-Javadoc)
358     * @see org.apache.commons.lang3.time.DateParser#parse(java.lang.String)
359     */
360    @Override
361    public Date parse(final String source) throws ParseException {
362        ParsePosition pp = new ParsePosition(0);
363        final Date date= parse(source, pp);
364        if (date == null) {
365            // Add a note re supported date range
366            if (locale.equals(JAPANESE_IMPERIAL)) {
367                throw new ParseException(
368                        "(The " +locale + " locale does not support dates before 1868 AD)\n" +
369                                "Unparseable date: \""+source, pp.getErrorIndex());
370            }
371            throw new ParseException("Unparseable date: "+source, pp.getErrorIndex());
372        }
373        return date;
374    }
375
376    /* (non-Javadoc)
377     * @see org.apache.commons.lang3.time.DateParser#parseObject(java.lang.String, java.text.ParsePosition)
378     */
379    @Override
380    public Object parseObject(final String source, final ParsePosition pos) {
381        return parse(source, pos);
382    }
383
384    /**
385     * This implementation updates the ParsePosition if the parse succeeds.
386     * However, it sets the error index to the position before the failed field unlike 
387     * the method {@link java.text.SimpleDateFormat#parse(String, ParsePosition)} which sets 
388     * the error index to after the failed field.
389     * <p>
390     * To determine if the parse has succeeded, the caller must check if the current parse position
391     * given by {@link ParsePosition#getIndex()} has been updated. If the input buffer has been fully
392     * parsed, then the index will point to just after the end of the input buffer.
393     *
394     * @see org.apache.commons.lang3.time.DateParser#parse(java.lang.String, java.text.ParsePosition)
395     */
396    @Override
397    public Date parse(final String source, final ParsePosition pos) {
398        // timing tests indicate getting new instance is 19% faster than cloning
399        final Calendar cal= Calendar.getInstance(timeZone, locale);
400        cal.clear();
401
402        return parse(source, pos, cal) ? cal.getTime() : null;
403    }
404
405    /**
406     * Parse a formatted date string according to the format.  Updates the Calendar with parsed fields.
407     * Upon success, the ParsePosition index is updated to indicate how much of the source text was consumed.
408     * Not all source text needs to be consumed.  Upon parse failure, ParsePosition error index is updated to
409     * the offset of the source text which does not match the supplied format.
410     * 
411     * @param source The text to parse.
412     * @param pos On input, the position in the source to start parsing, on output, updated position.
413     * @param calendar The calendar into which to set parsed fields.
414     * @return true, if source has been parsed (pos parsePosition is updated); otherwise false (and pos errorIndex is updated)
415     * @throws IllegalArgumentException when Calendar has been set to be not lenient, and a parsed field is
416     * out of range.
417     */
418    @Override
419    public boolean parse(final String source, final ParsePosition pos, final Calendar calendar) {
420        ListIterator<StrategyAndWidth> lt = patterns.listIterator();
421        while (lt.hasNext()) {
422            StrategyAndWidth pattern = lt.next();
423            int maxWidth = pattern.getMaxWidth(lt);
424            if (!pattern.strategy.parse(this, calendar, source, pos, maxWidth)) {
425                return false;
426            }
427        }
428        return true;
429    }
430
431    // Support for strategies
432    //-----------------------------------------------------------------------
433
434    private static StringBuilder simpleQuote(final StringBuilder sb, final String value) {
435        for (int i = 0; i < value.length(); ++i) {
436            char c = value.charAt(i);
437            switch (c) {
438            case '\\':
439            case '^':
440            case '$':
441            case '.':
442            case '|':
443            case '?':
444            case '*':
445            case '+':
446            case '(':
447            case ')':
448            case '[':
449            case '{':
450                sb.append('\\');
451            default:
452                sb.append(c);
453            }
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(Calendar cal, Locale locale, int field, StringBuilder regex) {
467        Map<String, Integer> values = new HashMap<String, Integer>();
468
469        Map<String, Integer> displayNames = cal.getDisplayNames(field, Calendar.ALL_STYLES, locale);
470        TreeSet<String> sorted = new TreeSet<String>(LONGER_FIRST_LOWERCASE);
471        for (Map.Entry<String, Integer> displayName : displayNames.entrySet()) {
472            String key = displayName.getKey().toLowerCase(locale);
473            if (sorted.add(key)) {
474                values.put(key, displayName.getValue());
475            }
476        }
477        for (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 static abstract 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 static abstract class PatternStrategy extends Strategy {
514
515        private Pattern pattern;
516
517        void createPattern(StringBuilder regex) {
518            createPattern(regex.toString());
519        }
520
521        void createPattern(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(FastDateParser parser, Calendar calendar, String source, ParsePosition pos, int maxWidth) {
538            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 formatField 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(char f, 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<Locale, Strategy>(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        final private 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(FastDateParser parser, Calendar calendar, String source, ParsePosition pos, int maxWidth) {
673            for (int idx = 0; idx < formatField.length(); ++idx) {
674                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            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(FastDateParser parser, Calendar cal, String value) {
720            final Integer iVal = lKeyValues.get(value.toLowerCase(locale));
721            cal.set(field, iVal.intValue());
722        }
723    }
724
725
726    /**
727     * A strategy that handles a number field in the parsing pattern
728     */
729    private static class NumberStrategy extends Strategy {
730        private final int field;
731
732        /**
733         * Construct a Strategy that parses a Number field
734         * @param field The Calendar field
735         */
736        NumberStrategy(final int field) {
737             this.field= field;
738        }
739
740        /**
741         * {@inheritDoc}
742         */
743        @Override
744        boolean isNumber() {
745            return true;
746        }
747
748        @Override
749        boolean parse(FastDateParser parser, Calendar calendar, String source, ParsePosition pos, int maxWidth) {
750            int idx = pos.getIndex();
751            int last = source.length();
752
753            if (maxWidth == 0) {
754                // if no maxWidth, strip leading white space
755                for (; idx < last; ++idx) {
756                    char c = source.charAt(idx);
757                    if (!Character.isWhitespace(c)) {
758                        break;
759                    }
760                }
761                pos.setIndex(idx);
762            } else {
763                int end = idx + maxWidth;
764                if (last > end) {
765                    last = end;
766                }
767            }
768
769            for (; idx < last; ++idx) {
770                char c = source.charAt(idx);
771                if (!Character.isDigit(c)) {
772                    break;
773                }
774            }
775
776            if (pos.getIndex() == idx) {
777                pos.setErrorIndex(idx);
778                return false;
779            }
780
781            int value = Integer.parseInt(source.substring(pos.getIndex(), idx));
782            pos.setIndex(idx);
783
784            calendar.set(field, modify(parser, value));
785            return true;
786        }
787
788        /**
789         * Make any modifications to parsed integer
790         * @param parser The parser
791         * @param iValue The parsed integer
792         * @return The modified value
793         */
794        int modify(FastDateParser parser, final int iValue) {
795            return iValue;
796        }
797
798    }
799
800    private static final Strategy ABBREVIATED_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR) {
801        /**
802         * {@inheritDoc}
803         */
804        @Override
805        int modify(FastDateParser parser, final int iValue) {
806            return iValue < 100 ? parser.adjustYear(iValue) : iValue;
807        }
808    };
809
810    /**
811     * A strategy that handles a timezone field in the parsing pattern
812     */
813    static class TimeZoneStrategy extends PatternStrategy {
814        private static final String RFC_822_TIME_ZONE = "[+-]\\d{4}";
815        private static final String GMT_OPTION= "GMT[+-]\\d{1,2}:\\d{2}";
816
817        private final Locale locale;
818        private final Map<String, TzInfo> tzNames= new HashMap<String, TzInfo>();
819
820        private static class TzInfo {
821            TimeZone zone;
822            int dstOffset;
823
824            TzInfo(TimeZone tz, boolean useDst) {
825                zone = tz;
826                dstOffset = useDst ?tz.getDSTSavings() :0;
827            }
828        }
829
830        /**
831         * Index of zone id
832         */
833        private static final int ID = 0;
834
835        /**
836         * Construct a Strategy that parses a TimeZone
837         * @param locale The Locale
838         */
839        TimeZoneStrategy(final Locale locale) {
840            this.locale = locale;
841
842            final StringBuilder sb = new StringBuilder();
843            sb.append("((?iu)" + RFC_822_TIME_ZONE + "|" + GMT_OPTION );
844
845            final Set<String> sorted = new TreeSet<String>(LONGER_FIRST_LOWERCASE);
846
847            final String[][] zones = DateFormatSymbols.getInstance(locale).getZoneStrings();
848            for (final String[] zoneNames : zones) {
849                // offset 0 is the time zone ID and is not localized
850                final String tzId = zoneNames[ID];
851                if (tzId.equalsIgnoreCase("GMT")) {
852                    continue;
853                }
854                final TimeZone tz = TimeZone.getTimeZone(tzId);
855                // offset 1 is long standard name
856                // offset 2 is short standard name
857                TzInfo standard = new TzInfo(tz, false);
858                TzInfo tzInfo = standard;
859                for (int i = 1; i < zoneNames.length; ++i) {
860                    switch (i) {
861                    case 3: // offset 3 is long daylight savings (or summertime) name
862                            // offset 4 is the short summertime name
863                        tzInfo = new TzInfo(tz, true);
864                        break;
865                    case 5: // offset 5 starts additional names, probably standard time
866                        tzInfo = standard;
867                        break;
868                    }
869                    String key = zoneNames[i].toLowerCase(locale);
870                    // ignore the data associated with duplicates supplied in
871                    // the additional names
872                    if (sorted.add(key)) {
873                        tzNames.put(key, tzInfo);
874                    }
875                }
876            }
877            // order the regex alternatives with longer strings first, greedy
878            // match will ensure longest string will be consumed
879            for (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                TimeZone tz = TimeZone.getTimeZone("GMT" + value);
893                cal.setTimeZone(tz);
894            } else if (value.regionMatches(true, 0, "GMT", 0, 3)) {
895                TimeZone tz = TimeZone.getTimeZone(value.toUpperCase());
896                cal.setTimeZone(tz);
897            } else {
898                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(String pattern) {
913            createPattern(pattern);
914        }
915        
916        /**
917         * {@inheritDoc}
918         */
919        @Override
920        void setCalendar(FastDateParser parser, Calendar cal, 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(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(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(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(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(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}