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