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