Source code
Description: source: package: org.apache.commons.lang3.time, class: FastDateParser

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