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