View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.lang3.time;
18  
19  import java.io.IOException;
20  import java.io.ObjectInputStream;
21  import java.io.Serializable;
22  import java.text.DateFormatSymbols;
23  import java.text.ParseException;
24  import java.text.ParsePosition;
25  import java.util.ArrayList;
26  import java.util.Calendar;
27  import java.util.Date;
28  import java.util.List;
29  import java.util.Locale;
30  import java.util.Map;
31  import java.util.SortedMap;
32  import java.util.TimeZone;
33  import java.util.TreeMap;
34  import java.util.concurrent.ConcurrentHashMap;
35  import java.util.concurrent.ConcurrentMap;
36  import java.util.regex.Matcher;
37  import java.util.regex.Pattern;
38  
39  /**
40   * <p>FastDateParser is a fast and thread-safe version of
41   * {@link java.text.SimpleDateFormat}.</p>
42   *
43   * <p>This class can be used as a direct replacement for
44   * <code>SimpleDateFormat</code> in most parsing situations.
45   * This class is especially useful in multi-threaded server environments.
46   * <code>SimpleDateFormat</code> is not thread-safe in any JDK version,
47   * nor will it be as Sun has closed the
48   * <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4228335">bug</a>/RFE.
49   * </p>
50   *
51   * <p>Only parsing is supported, but all patterns are compatible with
52   * SimpleDateFormat.</p>
53   *
54   * <p>Timing tests indicate this class is as about as fast as SimpleDateFormat
55   * in single thread applications and about 25% faster in multi-thread applications.</p>
56   *
57   * @version $Id: FastDateParser.java 1572877 2014-02-28 08:42:25Z britter $
58   * @since 3.2
59   */
60  public class FastDateParser implements DateParser, Serializable {
61      /**
62       * Required for serialization support.
63       *
64       * @see java.io.Serializable
65       */
66      private static final long serialVersionUID = 2L;
67  
68      static final Locale JAPANESE_IMPERIAL = new Locale("ja","JP","JP");
69  
70      // defining fields
71      private final String pattern;
72      private final TimeZone timeZone;
73      private final Locale locale;
74      private final int century;
75      private final int startYear;
76  
77      // derived fields
78      private transient Pattern parsePattern;
79      private transient Strategy[] strategies;
80  
81      // dynamic fields to communicate with Strategy
82      private transient String currentFormatField;
83      private transient Strategy nextStrategy;
84  
85      /**
86       * <p>Constructs a new FastDateParser.</p>
87       *
88       * @param pattern non-null {@link java.text.SimpleDateFormat} compatible
89       *  pattern
90       * @param timeZone non-null time zone to use
91       * @param locale non-null locale
92       */
93      protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale) {
94          this(pattern, timeZone, locale, null);
95      }
96  
97      /**
98       * <p>Constructs a new FastDateParser.</p>
99       *
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 }