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