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