aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/org/apache/commons/lang3/time/FastDateParser.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/org/apache/commons/lang3/time/FastDateParser.java')
-rw-r--r--src/main/java/org/apache/commons/lang3/time/FastDateParser.java1074
1 files changed, 1074 insertions, 0 deletions
diff --git a/src/main/java/org/apache/commons/lang3/time/FastDateParser.java b/src/main/java/org/apache/commons/lang3/time/FastDateParser.java
new file mode 100644
index 000000000..979bf6028
--- /dev/null
+++ b/src/main/java/org/apache/commons/lang3/time/FastDateParser.java
@@ -0,0 +1,1074 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.lang3.time;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.Serializable;
+import java.text.DateFormatSymbols;
+import java.text.ParseException;
+import java.text.ParsePosition;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang3.LocaleUtils;
+
+/**
+ * FastDateParser is a fast and thread-safe version of
+ * {@link java.text.SimpleDateFormat}.
+ *
+ * <p>To obtain a proxy to a FastDateParser, use {@link FastDateFormat#getInstance(String, TimeZone, Locale)}
+ * or another variation of the factory methods of {@link FastDateFormat}.</p>
+ *
+ * <p>Since FastDateParser is thread safe, you can use a static member instance:</p>
+ * <code>
+ * private static final DateParser DATE_PARSER = FastDateFormat.getInstance("yyyy-MM-dd");
+ * </code>
+ *
+ * <p>This class can be used as a direct replacement for
+ * {@link SimpleDateFormat} in most parsing situations.
+ * This class is especially useful in multi-threaded server environments.
+ * {@link SimpleDateFormat} is not thread-safe in any JDK version,
+ * nor will it be as Sun has closed the
+ * <a href="https://bugs.java.com/bugdatabase/view_bug.do?bug_id=4228335">bug</a>/RFE.
+ * </p>
+ *
+ * <p>Only parsing is supported by this class, but all patterns are compatible with
+ * SimpleDateFormat.</p>
+ *
+ * <p>The class operates in lenient mode, so for example a time of 90 minutes is treated as 1 hour 30 minutes.</p>
+ *
+ * <p>Timing tests indicate this class is as about as fast as SimpleDateFormat
+ * in single thread applications and about 25% faster in multi-thread applications.</p>
+ *
+ * @since 3.2
+ * @see FastDatePrinter
+ */
+public class FastDateParser implements DateParser, Serializable {
+
+ /**
+ * Required for serialization support.
+ *
+ * @see java.io.Serializable
+ */
+ private static final long serialVersionUID = 3L;
+
+ static final Locale JAPANESE_IMPERIAL = new Locale("ja", "JP", "JP");
+
+ /** Input pattern. */
+ private final String pattern;
+
+ /** Input TimeZone. */
+ private final TimeZone timeZone;
+
+ /** Input Locale. */
+ private final Locale locale;
+
+ /**
+ * Century from Date.
+ */
+ private final int century;
+
+ /**
+ * Start year from Date.
+ */
+ private final int startYear;
+
+ /** Initialized from Calendar. */
+ private transient List<StrategyAndWidth> patterns;
+
+ /**
+ * comparator used to sort regex alternatives. Alternatives should be ordered longer first, and shorter last.
+ * ('february' before 'feb'). All entries must be lower-case by locale.
+ */
+ private static final Comparator<String> LONGER_FIRST_LOWERCASE = Comparator.reverseOrder();
+
+ /**
+ * Constructs a new FastDateParser.
+ *
+ * Use {@link FastDateFormat#getInstance(String, TimeZone, Locale)} or another variation of the
+ * factory methods of {@link FastDateFormat} to get a cached FastDateParser instance.
+ *
+ * @param pattern non-null {@link java.text.SimpleDateFormat} compatible
+ * pattern
+ * @param timeZone non-null time zone to use
+ * @param locale non-null locale
+ */
+ protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale) {
+ this(pattern, timeZone, locale, null);
+ }
+
+ /**
+ * Constructs a new FastDateParser.
+ *
+ * @param pattern non-null {@link java.text.SimpleDateFormat} compatible
+ * pattern
+ * @param timeZone non-null time zone to use
+ * @param locale non-null locale
+ * @param centuryStart The start of the century for 2 digit year parsing
+ *
+ * @since 3.5
+ */
+ protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale,
+ final Date centuryStart) {
+ this.pattern = pattern;
+ this.timeZone = timeZone;
+ this.locale = LocaleUtils.toLocale(locale);
+
+ final Calendar definingCalendar = Calendar.getInstance(timeZone, this.locale);
+
+ final int centuryStartYear;
+ if (centuryStart != null) {
+ definingCalendar.setTime(centuryStart);
+ centuryStartYear = definingCalendar.get(Calendar.YEAR);
+ } else if (this.locale.equals(JAPANESE_IMPERIAL)) {
+ centuryStartYear = 0;
+ } else {
+ // from 80 years ago to 20 years from now
+ definingCalendar.setTime(new Date());
+ centuryStartYear = definingCalendar.get(Calendar.YEAR) - 80;
+ }
+ century = centuryStartYear / 100 * 100;
+ startYear = centuryStartYear - century;
+
+ init(definingCalendar);
+ }
+
+ /**
+ * Initializes derived fields from defining fields.
+ * This is called from constructor and from readObject (de-serialization)
+ *
+ * @param definingCalendar the {@link java.util.Calendar} instance used to initialize this FastDateParser
+ */
+ private void init(final Calendar definingCalendar) {
+ patterns = new ArrayList<>();
+
+ final StrategyParser fm = new StrategyParser(definingCalendar);
+ for (;;) {
+ final StrategyAndWidth field = fm.getNextStrategy();
+ if (field == null) {
+ break;
+ }
+ patterns.add(field);
+ }
+ }
+
+ // helper classes to parse the format string
+
+ /**
+ * Holds strategy and field width
+ */
+ private static class StrategyAndWidth {
+
+ final Strategy strategy;
+ final int width;
+
+ StrategyAndWidth(final Strategy strategy, final int width) {
+ this.strategy = strategy;
+ this.width = width;
+ }
+
+ int getMaxWidth(final ListIterator<StrategyAndWidth> lt) {
+ if (!strategy.isNumber() || !lt.hasNext()) {
+ return 0;
+ }
+ final Strategy nextStrategy = lt.next().strategy;
+ lt.previous();
+ return nextStrategy.isNumber() ? width : 0;
+ }
+
+ @Override
+ public String toString() {
+ return "StrategyAndWidth [strategy=" + strategy + ", width=" + width + "]";
+ }
+ }
+
+ /**
+ * Parse format into Strategies
+ */
+ private class StrategyParser {
+ private final Calendar definingCalendar;
+ private int currentIdx;
+
+ StrategyParser(final Calendar definingCalendar) {
+ this.definingCalendar = definingCalendar;
+ }
+
+ StrategyAndWidth getNextStrategy() {
+ if (currentIdx >= pattern.length()) {
+ return null;
+ }
+
+ final char c = pattern.charAt(currentIdx);
+ if (isFormatLetter(c)) {
+ return letterPattern(c);
+ }
+ return literal();
+ }
+
+ private StrategyAndWidth letterPattern(final char c) {
+ final int begin = currentIdx;
+ while (++currentIdx < pattern.length()) {
+ if (pattern.charAt(currentIdx) != c) {
+ break;
+ }
+ }
+
+ final int width = currentIdx - begin;
+ return new StrategyAndWidth(getStrategy(c, width, definingCalendar), width);
+ }
+
+ private StrategyAndWidth literal() {
+ boolean activeQuote = false;
+
+ final StringBuilder sb = new StringBuilder();
+ while (currentIdx < pattern.length()) {
+ final char c = pattern.charAt(currentIdx);
+ if (!activeQuote && isFormatLetter(c)) {
+ break;
+ }
+ if (c == '\'' && (++currentIdx == pattern.length() || pattern.charAt(currentIdx) != '\'')) {
+ activeQuote = !activeQuote;
+ continue;
+ }
+ ++currentIdx;
+ sb.append(c);
+ }
+
+ if (activeQuote) {
+ throw new IllegalArgumentException("Unterminated quote");
+ }
+
+ final String formatField = sb.toString();
+ return new StrategyAndWidth(new CopyQuotedStrategy(formatField), formatField.length());
+ }
+ }
+
+ private static boolean isFormatLetter(final char c) {
+ return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z';
+ }
+
+ // Accessors
+ /* (non-Javadoc)
+ * @see org.apache.commons.lang3.time.DateParser#getPattern()
+ */
+ @Override
+ public String getPattern() {
+ return pattern;
+ }
+
+ /* (non-Javadoc)
+ * @see org.apache.commons.lang3.time.DateParser#getTimeZone()
+ */
+ @Override
+ public TimeZone getTimeZone() {
+ return timeZone;
+ }
+
+ /* (non-Javadoc)
+ * @see org.apache.commons.lang3.time.DateParser#getLocale()
+ */
+ @Override
+ public Locale getLocale() {
+ return locale;
+ }
+
+
+ // Basics
+ /**
+ * Compares another object for equality with this object.
+ *
+ * @param obj the object to compare to
+ * @return {@code true}if equal to this instance
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ if (!(obj instanceof FastDateParser)) {
+ return false;
+ }
+ final FastDateParser other = (FastDateParser) obj;
+ return pattern.equals(other.pattern) && timeZone.equals(other.timeZone) && locale.equals(other.locale);
+ }
+
+ /**
+ * Returns a hash code compatible with equals.
+ *
+ * @return a hash code compatible with equals
+ */
+ @Override
+ public int hashCode() {
+ return pattern.hashCode() + 13 * (timeZone.hashCode() + 13 * locale.hashCode());
+ }
+
+ /**
+ * Gets a string version of this formatter.
+ *
+ * @return a debugging string
+ */
+ @Override
+ public String toString() {
+ return "FastDateParser[" + pattern + ", " + locale + ", " + timeZone.getID() + "]";
+ }
+
+ /**
+ * Converts all state of this instance to a String handy for debugging.
+ *
+ * @return a string.
+ * @since 3.12.0
+ */
+ public String toStringAll() {
+ return "FastDateParser [pattern=" + pattern + ", timeZone=" + timeZone + ", locale=" + locale + ", century="
+ + century + ", startYear=" + startYear + ", patterns=" + patterns + "]";
+ }
+
+ // Serializing
+ /**
+ * Creates the object after serialization. This implementation reinitializes the
+ * transient properties.
+ *
+ * @param in ObjectInputStream from which the object is being deserialized.
+ * @throws IOException if there is an IO issue.
+ * @throws ClassNotFoundException if a class cannot be found.
+ */
+ private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
+ in.defaultReadObject();
+
+ final Calendar definingCalendar = Calendar.getInstance(timeZone, locale);
+ init(definingCalendar);
+ }
+
+ /* (non-Javadoc)
+ * @see org.apache.commons.lang3.time.DateParser#parseObject(String)
+ */
+ @Override
+ public Object parseObject(final String source) throws ParseException {
+ return parse(source);
+ }
+
+ /* (non-Javadoc)
+ * @see org.apache.commons.lang3.time.DateParser#parse(String)
+ */
+ @Override
+ public Date parse(final String source) throws ParseException {
+ final ParsePosition pp = new ParsePosition(0);
+ final Date date = parse(source, pp);
+ if (date == null) {
+ // Add a note re supported date range
+ if (locale.equals(JAPANESE_IMPERIAL)) {
+ throw new ParseException("(The " + locale + " locale does not support dates before 1868 AD)\n"
+ + "Unparseable date: \"" + source, pp.getErrorIndex());
+ }
+ throw new ParseException("Unparseable date: " + source, pp.getErrorIndex());
+ }
+ return date;
+ }
+
+ /* (non-Javadoc)
+ * @see org.apache.commons.lang3.time.DateParser#parseObject(String, java.text.ParsePosition)
+ */
+ @Override
+ public Object parseObject(final String source, final ParsePosition pos) {
+ return parse(source, pos);
+ }
+
+ /**
+ * This implementation updates the ParsePosition if the parse succeeds.
+ * However, it sets the error index to the position before the failed field unlike
+ * the method {@link java.text.SimpleDateFormat#parse(String, ParsePosition)} which sets
+ * the error index to after the failed field.
+ * <p>
+ * To determine if the parse has succeeded, the caller must check if the current parse position
+ * given by {@link ParsePosition#getIndex()} has been updated. If the input buffer has been fully
+ * parsed, then the index will point to just after the end of the input buffer.
+ *
+ * @see org.apache.commons.lang3.time.DateParser#parse(String, java.text.ParsePosition)
+ */
+ @Override
+ public Date parse(final String source, final ParsePosition pos) {
+ // timing tests indicate getting new instance is 19% faster than cloning
+ final Calendar cal = Calendar.getInstance(timeZone, locale);
+ cal.clear();
+
+ return parse(source, pos, cal) ? cal.getTime() : null;
+ }
+
+ /**
+ * Parses a formatted date string according to the format. Updates the Calendar with parsed fields.
+ * Upon success, the ParsePosition index is updated to 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
+ * the offset of the source text which does not match the supplied format.
+ *
+ * @param source The text to parse.
+ * @param pos On input, the position in the source to start parsing, on output, updated position.
+ * @param calendar The calendar into which to set parsed fields.
+ * @return true, if source has been parsed (pos parsePosition is updated); otherwise false (and pos errorIndex is updated)
+ * @throws IllegalArgumentException when Calendar has been set to be not lenient, and a parsed field is
+ * out of range.
+ */
+ @Override
+ public boolean parse(final String source, final ParsePosition pos, final Calendar calendar) {
+ final ListIterator<StrategyAndWidth> lt = patterns.listIterator();
+ while (lt.hasNext()) {
+ final StrategyAndWidth strategyAndWidth = lt.next();
+ final int maxWidth = strategyAndWidth.getMaxWidth(lt);
+ if (!strategyAndWidth.strategy.parse(this, calendar, source, pos, maxWidth)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // Support for strategies
+
+ private static StringBuilder simpleQuote(final StringBuilder sb, final String value) {
+ for (int i = 0; i < value.length(); ++i) {
+ final char c = value.charAt(i);
+ switch (c) {
+ case '\\':
+ case '^':
+ case '$':
+ case '.':
+ case '|':
+ case '?':
+ case '*':
+ case '+':
+ case '(':
+ case ')':
+ case '[':
+ case '{':
+ sb.append('\\');
+ default:
+ sb.append(c);
+ }
+ }
+ if (sb.charAt(sb.length() - 1) == '.') {
+ // trailing '.' is optional
+ sb.append('?');
+ }
+ return sb;
+ }
+
+ /**
+ * Gets the short and long values displayed for a field
+ * @param calendar The calendar to obtain the short and long values
+ * @param locale The locale of display names
+ * @param field The field of interest
+ * @param regex The regular expression to build
+ * @return The map of string display names to field values
+ */
+ private static Map<String, Integer> appendDisplayNames(final Calendar calendar, final Locale locale, final int field,
+ final StringBuilder regex) {
+ final Map<String, Integer> values = new HashMap<>();
+ final Locale actualLocale = LocaleUtils.toLocale(locale);
+ final Map<String, Integer> displayNames = calendar.getDisplayNames(field, Calendar.ALL_STYLES, actualLocale);
+ final TreeSet<String> sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE);
+ displayNames.forEach((k, v) -> {
+ final String keyLc = k.toLowerCase(actualLocale);
+ if (sorted.add(keyLc)) {
+ values.put(keyLc, v);
+ }
+ });
+ sorted.forEach(symbol -> simpleQuote(regex, symbol).append('|'));
+ return values;
+ }
+
+ /**
+ * Adjusts dates to be within appropriate century
+ * @param twoDigitYear The year to adjust
+ * @return A value between centuryStart(inclusive) to centuryStart+100(exclusive)
+ */
+ private int adjustYear(final int twoDigitYear) {
+ final int trial = century + twoDigitYear;
+ return twoDigitYear >= startYear ? trial : trial + 100;
+ }
+
+ /**
+ * A strategy to parse a single field from the parsing pattern
+ */
+ private abstract static class Strategy {
+
+ /**
+ * Is this field a number? The default implementation returns false.
+ *
+ * @return true, if field is a number
+ */
+ boolean isNumber() {
+ return false;
+ }
+
+ abstract boolean parse(FastDateParser parser, Calendar calendar, String source, ParsePosition pos,
+ int maxWidth);
+ }
+
+ /**
+ * A strategy to parse a single field from the parsing pattern
+ */
+ private abstract static class PatternStrategy extends Strategy {
+
+ Pattern pattern;
+
+ void createPattern(final StringBuilder regex) {
+ createPattern(regex.toString());
+ }
+
+ void createPattern(final String regex) {
+ this.pattern = Pattern.compile(regex);
+ }
+
+ /**
+ * Is this field a number? The default implementation returns false.
+ *
+ * @return true, if field is a number
+ */
+ @Override
+ boolean isNumber() {
+ return false;
+ }
+
+ @Override
+ boolean parse(final FastDateParser parser, final Calendar calendar, final String source,
+ final ParsePosition pos, final int maxWidth) {
+ final Matcher matcher = pattern.matcher(source.substring(pos.getIndex()));
+ if (!matcher.lookingAt()) {
+ pos.setErrorIndex(pos.getIndex());
+ return false;
+ }
+ pos.setIndex(pos.getIndex() + matcher.end(1));
+ setCalendar(parser, calendar, matcher.group(1));
+ return true;
+ }
+
+ abstract void setCalendar(FastDateParser parser, Calendar calendar, String value);
+
+ /**
+ * Converts this instance to a handy debug string.
+ *
+ * @since 3.12.0
+ */
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + " [pattern=" + pattern + "]";
+ }
+
+}
+
+ /**
+ * Gets a Strategy given a field from a SimpleDateFormat pattern
+ * @param f A sub-sequence of the SimpleDateFormat pattern
+ * @param width formatting width
+ * @param definingCalendar The calendar to obtain the short and long values
+ * @return The Strategy that will handle parsing for the field
+ */
+ private Strategy getStrategy(final char f, final int width, final Calendar definingCalendar) {
+ switch (f) {
+ default:
+ throw new IllegalArgumentException("Format '" + f + "' not supported");
+ case 'D':
+ return DAY_OF_YEAR_STRATEGY;
+ case 'E':
+ return getLocaleSpecificStrategy(Calendar.DAY_OF_WEEK, definingCalendar);
+ case 'F':
+ return DAY_OF_WEEK_IN_MONTH_STRATEGY;
+ case 'G':
+ return getLocaleSpecificStrategy(Calendar.ERA, definingCalendar);
+ case 'H': // Hour in day (0-23)
+ return HOUR_OF_DAY_STRATEGY;
+ case 'K': // Hour in am/pm (0-11)
+ return HOUR_STRATEGY;
+ case 'M':
+ case 'L':
+ return width >= 3 ? getLocaleSpecificStrategy(Calendar.MONTH, definingCalendar) : NUMBER_MONTH_STRATEGY;
+ case 'S':
+ return MILLISECOND_STRATEGY;
+ case 'W':
+ return WEEK_OF_MONTH_STRATEGY;
+ case 'a':
+ return getLocaleSpecificStrategy(Calendar.AM_PM, definingCalendar);
+ case 'd':
+ return DAY_OF_MONTH_STRATEGY;
+ case 'h': // Hour in am/pm (1-12), i.e. midday/midnight is 12, not 0
+ return HOUR12_STRATEGY;
+ case 'k': // Hour in day (1-24), i.e. midnight is 24, not 0
+ return HOUR24_OF_DAY_STRATEGY;
+ case 'm':
+ return MINUTE_STRATEGY;
+ case 's':
+ return SECOND_STRATEGY;
+ case 'u':
+ return DAY_OF_WEEK_STRATEGY;
+ case 'w':
+ return WEEK_OF_YEAR_STRATEGY;
+ case 'y':
+ case 'Y':
+ return width > 2 ? LITERAL_YEAR_STRATEGY : ABBREVIATED_YEAR_STRATEGY;
+ case 'X':
+ return ISO8601TimeZoneStrategy.getStrategy(width);
+ case 'Z':
+ if (width == 2) {
+ return ISO8601TimeZoneStrategy.ISO_8601_3_STRATEGY;
+ }
+ //$FALL-THROUGH$
+ case 'z':
+ return getLocaleSpecificStrategy(Calendar.ZONE_OFFSET, definingCalendar);
+ }
+ }
+
+ @SuppressWarnings("unchecked") // OK because we are creating an array with no entries
+ private static final ConcurrentMap<Locale, Strategy>[] caches = new ConcurrentMap[Calendar.FIELD_COUNT];
+
+ /**
+ * Gets a cache of Strategies for a particular field
+ * @param field The Calendar field
+ * @return a cache of Locale to Strategy
+ */
+ private static ConcurrentMap<Locale, Strategy> getCache(final int field) {
+ synchronized (caches) {
+ if (caches[field] == null) {
+ caches[field] = new ConcurrentHashMap<>(3);
+ }
+ return caches[field];
+ }
+ }
+
+ /**
+ * Constructs a Strategy that parses a Text field
+ * @param field The Calendar field
+ * @param definingCalendar The calendar to obtain the short and long values
+ * @return a TextStrategy for the field and Locale
+ */
+ private Strategy getLocaleSpecificStrategy(final int field, final Calendar definingCalendar) {
+ final ConcurrentMap<Locale, Strategy> cache = getCache(field);
+ return cache.computeIfAbsent(locale, k -> field == Calendar.ZONE_OFFSET ? new TimeZoneStrategy(locale) : new CaseInsensitiveTextStrategy(field, definingCalendar, locale));
+ }
+
+ /**
+ * A strategy that copies the static or quoted field in the parsing pattern
+ */
+ private static class CopyQuotedStrategy extends Strategy {
+
+ private final String formatField;
+
+ /**
+ * Constructs a Strategy that ensures the formatField has literal text
+ *
+ * @param formatField The literal text to match
+ */
+ CopyQuotedStrategy(final String formatField) {
+ this.formatField = formatField;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ boolean isNumber() {
+ return false;
+ }
+
+ @Override
+ boolean parse(final FastDateParser parser, final Calendar calendar, final String source,
+ final ParsePosition pos, final int maxWidth) {
+ for (int idx = 0; idx < formatField.length(); ++idx) {
+ final int sIdx = idx + pos.getIndex();
+ if (sIdx == source.length()) {
+ pos.setErrorIndex(sIdx);
+ return false;
+ }
+ if (formatField.charAt(idx) != source.charAt(sIdx)) {
+ pos.setErrorIndex(sIdx);
+ return false;
+ }
+ }
+ pos.setIndex(formatField.length() + pos.getIndex());
+ return true;
+ }
+
+ /**
+ * Converts this instance to a handy debug string.
+ *
+ * @since 3.12.0
+ */
+ @Override
+ public String toString() {
+ return "CopyQuotedStrategy [formatField=" + formatField + "]";
+ }
+ }
+
+ /**
+ * A strategy that handles a text field in the parsing pattern
+ */
+ private static class CaseInsensitiveTextStrategy extends PatternStrategy {
+ private final int field;
+ final Locale locale;
+ private final Map<String, Integer> lKeyValues;
+
+ /**
+ * Constructs a Strategy that parses a Text field
+ *
+ * @param field The Calendar field
+ * @param definingCalendar The Calendar to use
+ * @param locale The Locale to use
+ */
+ CaseInsensitiveTextStrategy(final int field, final Calendar definingCalendar, final Locale locale) {
+ this.field = field;
+ this.locale = LocaleUtils.toLocale(locale);
+
+ final StringBuilder regex = new StringBuilder();
+ regex.append("((?iu)");
+ lKeyValues = appendDisplayNames(definingCalendar, locale, field, regex);
+ regex.setLength(regex.length() - 1);
+ regex.append(")");
+ createPattern(regex);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ void setCalendar(final FastDateParser parser, final Calendar calendar, final String value) {
+ final String lowerCase = value.toLowerCase(locale);
+ Integer iVal = lKeyValues.get(lowerCase);
+ if (iVal == null) {
+ // match missing the optional trailing period
+ iVal = lKeyValues.get(lowerCase + '.');
+ }
+ //LANG-1669: Mimic fix done in OpenJDK 17 to resolve issue with parsing newly supported day periods added in OpenJDK 16
+ if (Calendar.AM_PM != this.field || iVal <= 1) {
+ calendar.set(field, iVal.intValue());
+ }
+ }
+
+ /**
+ * Converts this instance to a handy debug string.
+ *
+ * @since 3.12.0
+ */
+ @Override
+ public String toString() {
+ return "CaseInsensitiveTextStrategy [field=" + field + ", locale=" + locale + ", lKeyValues=" + lKeyValues
+ + ", pattern=" + pattern + "]";
+ }
+ }
+
+
+ /**
+ * A strategy that handles a number field in the parsing pattern
+ */
+ private static class NumberStrategy extends Strategy {
+
+ private final int field;
+
+ /**
+ * Constructs a Strategy that parses a Number field
+ *
+ * @param field The Calendar field
+ */
+ NumberStrategy(final int field) {
+ this.field = field;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ boolean isNumber() {
+ return true;
+ }
+
+ @Override
+ boolean parse(final FastDateParser parser, final Calendar calendar, final String source,
+ final ParsePosition pos, final int maxWidth) {
+ int idx = pos.getIndex();
+ int last = source.length();
+
+ if (maxWidth == 0) {
+ // if no maxWidth, strip leading white space
+ for (; idx < last; ++idx) {
+ final char c = source.charAt(idx);
+ if (!Character.isWhitespace(c)) {
+ break;
+ }
+ }
+ pos.setIndex(idx);
+ } else {
+ final int end = idx + maxWidth;
+ if (last > end) {
+ last = end;
+ }
+ }
+
+ for (; idx < last; ++idx) {
+ final char c = source.charAt(idx);
+ if (!Character.isDigit(c)) {
+ break;
+ }
+ }
+
+ if (pos.getIndex() == idx) {
+ pos.setErrorIndex(idx);
+ return false;
+ }
+
+ final int value = Integer.parseInt(source.substring(pos.getIndex(), idx));
+ pos.setIndex(idx);
+
+ calendar.set(field, modify(parser, value));
+ return true;
+ }
+
+ /**
+ * Make any modifications to parsed integer
+ *
+ * @param parser The parser
+ * @param iValue The parsed integer
+ * @return The modified value
+ */
+ int modify(final FastDateParser parser, final int iValue) {
+ return iValue;
+ }
+
+ /**
+ * Converts this instance to a handy debug string.
+ *
+ * @since 3.12.0
+ */
+ @Override
+ public String toString() {
+ return "NumberStrategy [field=" + field + "]";
+ }
+ }
+
+ private static final Strategy ABBREVIATED_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR) {
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ int modify(final FastDateParser parser, final int iValue) {
+ return iValue < 100 ? parser.adjustYear(iValue) : iValue;
+ }
+ };
+
+ /**
+ * A strategy that handles a time zone field in the parsing pattern
+ */
+ static class TimeZoneStrategy extends PatternStrategy {
+ private static final String RFC_822_TIME_ZONE = "[+-]\\d{4}";
+ private static final String GMT_OPTION = TimeZones.GMT_ID + "[+-]\\d{1,2}:\\d{2}";
+
+ private final Locale locale;
+ private final Map<String, TzInfo> tzNames = new HashMap<>();
+
+ private static class TzInfo {
+ final TimeZone zone;
+ final int dstOffset;
+
+ TzInfo(final TimeZone tz, final boolean useDst) {
+ zone = tz;
+ dstOffset = useDst ? tz.getDSTSavings() : 0;
+ }
+ }
+
+ /**
+ * Index of zone id
+ */
+ private static final int ID = 0;
+
+ /**
+ * Constructs a Strategy that parses a TimeZone
+ *
+ * @param locale The Locale
+ */
+ TimeZoneStrategy(final Locale locale) {
+ this.locale = LocaleUtils.toLocale(locale);
+
+ final StringBuilder sb = new StringBuilder();
+ sb.append("((?iu)" + RFC_822_TIME_ZONE + "|" + GMT_OPTION);
+
+ final Set<String> sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE);
+
+ final String[][] zones = DateFormatSymbols.getInstance(locale).getZoneStrings();
+ for (final String[] zoneNames : zones) {
+ // offset 0 is the time zone ID and is not localized
+ final String tzId = zoneNames[ID];
+ if (tzId.equalsIgnoreCase(TimeZones.GMT_ID)) {
+ continue;
+ }
+ final TimeZone tz = TimeZone.getTimeZone(tzId);
+ // offset 1 is long standard name
+ // offset 2 is short standard name
+ final TzInfo standard = new TzInfo(tz, false);
+ TzInfo tzInfo = standard;
+ for (int i = 1; i < zoneNames.length; ++i) {
+ switch (i) {
+ case 3: // offset 3 is long daylight savings (or summertime) name
+ // offset 4 is the short summertime name
+ tzInfo = new TzInfo(tz, true);
+ break;
+ case 5: // offset 5 starts additional names, probably standard time
+ tzInfo = standard;
+ break;
+ default:
+ break;
+ }
+ if (zoneNames[i] != null) {
+ final String key = zoneNames[i].toLowerCase(locale);
+ // ignore the data associated with duplicates supplied in
+ // the additional names
+ if (sorted.add(key)) {
+ tzNames.put(key, tzInfo);
+ }
+ }
+ }
+ }
+ // order the regex alternatives with longer strings first, greedy
+ // match will ensure the longest string will be consumed
+ sorted.forEach(zoneName -> simpleQuote(sb.append('|'), zoneName));
+ sb.append(")");
+ createPattern(sb);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ void setCalendar(final FastDateParser parser, final Calendar calendar, final String timeZone) {
+ final TimeZone tz = FastTimeZone.getGmtTimeZone(timeZone);
+ if (tz != null) {
+ calendar.setTimeZone(tz);
+ } else {
+ final String lowerCase = timeZone.toLowerCase(locale);
+ TzInfo tzInfo = tzNames.get(lowerCase);
+ if (tzInfo == null) {
+ // match missing the optional trailing period
+ tzInfo = tzNames.get(lowerCase + '.');
+ }
+ calendar.set(Calendar.DST_OFFSET, tzInfo.dstOffset);
+ calendar.set(Calendar.ZONE_OFFSET, tzInfo.zone.getRawOffset());
+ }
+ }
+
+ /**
+ * Converts this instance to a handy debug string.
+ *
+ * @since 3.12.0
+ */
+ @Override
+ public String toString() {
+ return "TimeZoneStrategy [locale=" + locale + ", tzNames=" + tzNames + ", pattern=" + pattern + "]";
+ }
+
+ }
+
+ private static class ISO8601TimeZoneStrategy extends PatternStrategy {
+ // Z, +hh, -hh, +hhmm, -hhmm, +hh:mm or -hh:mm
+
+ /**
+ * Constructs a Strategy that parses a TimeZone
+ * @param pattern The Pattern
+ */
+ ISO8601TimeZoneStrategy(final String pattern) {
+ createPattern(pattern);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ void setCalendar(final FastDateParser parser, final Calendar calendar, final String value) {
+ calendar.setTimeZone(FastTimeZone.getGmtTimeZone(value));
+ }
+
+ private static final Strategy ISO_8601_1_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}))");
+ private static final Strategy ISO_8601_2_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}\\d{2}))");
+ private static final Strategy ISO_8601_3_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}(?::)\\d{2}))");
+
+ /**
+ * Factory method for ISO8601TimeZoneStrategies.
+ *
+ * @param tokenLen a token indicating the length of the TimeZone String to be formatted.
+ * @return a ISO8601TimeZoneStrategy that can format TimeZone String of length {@code tokenLen}. If no such
+ * strategy exists, an IllegalArgumentException will be thrown.
+ */
+ static Strategy getStrategy(final int tokenLen) {
+ switch(tokenLen) {
+ case 1:
+ return ISO_8601_1_STRATEGY;
+ case 2:
+ return ISO_8601_2_STRATEGY;
+ case 3:
+ return ISO_8601_3_STRATEGY;
+ default:
+ throw new IllegalArgumentException("invalid number of X");
+ }
+ }
+ }
+
+ private static final Strategy NUMBER_MONTH_STRATEGY = new NumberStrategy(Calendar.MONTH) {
+ @Override
+ int modify(final FastDateParser parser, final int iValue) {
+ return iValue-1;
+ }
+ };
+
+ private static final Strategy LITERAL_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR);
+ private static final Strategy WEEK_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_YEAR);
+ private static final Strategy WEEK_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_MONTH);
+ private static final Strategy DAY_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.DAY_OF_YEAR);
+ private static final Strategy DAY_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_MONTH);
+ private static final Strategy DAY_OF_WEEK_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK) {
+ @Override
+ int modify(final FastDateParser parser, final int iValue) {
+ return iValue == 7 ? Calendar.SUNDAY : iValue + 1;
+ }
+ };
+
+ private static final Strategy DAY_OF_WEEK_IN_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK_IN_MONTH);
+ private static final Strategy HOUR_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY);
+ private static final Strategy HOUR24_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY) {
+ @Override
+ int modify(final FastDateParser parser, final int iValue) {
+ return iValue == 24 ? 0 : iValue;
+ }
+ };
+
+ private static final Strategy HOUR12_STRATEGY = new NumberStrategy(Calendar.HOUR) {
+ @Override
+ int modify(final FastDateParser parser, final int iValue) {
+ return iValue == 12 ? 0 : iValue;
+ }
+ };
+
+ private static final Strategy HOUR_STRATEGY = new NumberStrategy(Calendar.HOUR);
+ private static final Strategy MINUTE_STRATEGY = new NumberStrategy(Calendar.MINUTE);
+ private static final Strategy SECOND_STRATEGY = new NumberStrategy(Calendar.SECOND);
+ private static final Strategy MILLISECOND_STRATEGY = new NumberStrategy(Calendar.MILLISECOND);
+}