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 *      https://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.GregorianCalendar;
031import java.util.HashMap;
032import java.util.List;
033import java.util.ListIterator;
034import java.util.Locale;
035import java.util.Map;
036import java.util.Objects;
037import java.util.Set;
038import java.util.TimeZone;
039import java.util.TreeMap;
040import java.util.TreeSet;
041import java.util.concurrent.ConcurrentHashMap;
042import java.util.concurrent.ConcurrentMap;
043import java.util.regex.Matcher;
044import java.util.regex.Pattern;
045import java.util.stream.Stream;
046
047import org.apache.commons.lang3.CharUtils;
048import org.apache.commons.lang3.LocaleUtils;
049import org.apache.commons.lang3.SerializationUtils;
050import org.apache.commons.lang3.StringUtils;
051
052/**
053 * FastDateParser is a fast and thread-safe version of {@link java.text.SimpleDateFormat}.
054 *
055 * <p>
056 * To obtain a proxy to a FastDateParser, use {@link FastDateFormat#getInstance(String, TimeZone, Locale)} or another variation of the factory methods of
057 * {@link FastDateFormat}.
058 * </p>
059 *
060 * <p>
061 * Since FastDateParser is thread safe, you can use a static member instance:
062 * </p>
063 * {@code
064 *     private static final DateParser DATE_PARSER = FastDateFormat.getInstance("yyyy-MM-dd");
065 * }
066 *
067 * <p>
068 * This class can be used as a direct replacement for {@link SimpleDateFormat} in most parsing situations. This class is especially useful in multi-threaded
069 * server environments. {@link SimpleDateFormat} is not thread-safe in any JDK version, nor will it be as Sun has closed the
070 * <a href="https://bugs.openjdk.org/browse/JDK-4228335">bug</a>/RFE.
071 * </p>
072 *
073 * <p>
074 * Only parsing is supported by this class. Patterns are compatible with {@link SimpleDateFormat}, except for time zone extensions described below.
075 * </p>
076 *
077 * <p>
078 * The pattern {@code 'ZZ'} accepts {@code Z} for a zero offset or an ISO 8601 extended numeric offset such as {@code +08:00}. Three or more {@code 'Z'}
079 * letters accept colon-separated numeric offsets in addition to the RFC822 offsets, GMT offsets, and localized time zone names accepted by a single
080 * {@code 'Z'}. The patterns {@code 'z'} and a single {@code 'Z'} do not accept bare colon-separated numeric offsets.
081 * </p>
082 *
083 * <p>
084 * The class operates in lenient mode, so for example a time of 90 minutes is treated as 1 hour 30 minutes.
085 * </p>
086 *
087 * <p>
088 * Timing tests indicate this class is as about as fast as SimpleDateFormat in single thread applications and about 25% faster in multi-thread applications.
089 * </p>
090 *
091 * @since 3.2
092 * @see FastDatePrinter
093 */
094public class FastDateParser implements DateParser, Serializable {
095
096    /**
097     * A strategy that handles a text field in the parsing pattern
098     */
099    private static final class CaseInsensitiveTextStrategy extends PatternStrategy {
100
101        private final int field;
102        private final Locale locale;
103        private final Map<String, Integer> lKeyValues;
104
105        /**
106         * Constructs a Strategy that parses a Text field
107         *
108         * @param field            The Calendar field
109         * @param definingCalendar The Calendar to use
110         * @param locale           The Locale to use
111         */
112        CaseInsensitiveTextStrategy(final int field, final Calendar definingCalendar, final Locale locale) {
113            this.field = field;
114            this.locale = LocaleUtils.toLocale(locale);
115            final StringBuilder regex = new StringBuilder();
116            regex.append("((?iu)");
117            lKeyValues = appendDisplayNames(definingCalendar, locale, field, regex);
118            regex.setLength(regex.length() - 1);
119            regex.append(")");
120            createPattern(regex);
121        }
122
123        /**
124         * {@inheritDoc}
125         */
126        @Override
127        void setCalendar(final FastDateParser parser, final Calendar calendar, final String value) {
128            String lowerCase = value.toLowerCase(locale);
129            Integer iVal = lKeyValues.get(lowerCase);
130            if (iVal == null) {
131                // match missing the optional trailing period
132                iVal = lKeyValues.get(lowerCase + '.');
133            }
134            if (iVal == null) {
135                // The regex matches case-insensitively via Unicode case folding ("(?iu)"), which is a
136                // wider equivalence than the toLowerCase(locale) fold used to build the key map; retry
137                // with the root-locale fold so that, for example, ASCII input under locales with
138                // special casing rules still resolves to the same key.
139                lowerCase = value.toLowerCase(Locale.ROOT);
140                iVal = lKeyValues.get(lowerCase);
141                if (iVal == null) {
142                    iVal = lKeyValues.get(lowerCase + '.');
143                }
144            }
145            if (iVal == null) {
146                // Converted to a parse failure by PatternStrategy.parse instead of surfacing as an
147                // undeclared NullPointerException.
148                throw new IllegalArgumentException("Invalid display name for field " + field + ": '" + value + "'");
149            }
150            // LANG-1669: Mimic fix done in OpenJDK 17 to resolve issue with parsing newly supported day periods added in OpenJDK 16
151            if (Calendar.AM_PM != this.field || iVal <= 1) {
152                calendar.set(field, iVal.intValue());
153            }
154        }
155
156        /**
157         * Converts this instance to a handy debug string.
158         *
159         * @since 3.12.0
160         */
161        @Override
162        public String toString() {
163            return "CaseInsensitiveTextStrategy [field=" + field + ", locale=" + locale + ", lKeyValues=" + lKeyValues + ", pattern=" + pattern + "]";
164        }
165    }
166
167    /**
168     * A strategy that copies the static or quoted field in the parsing pattern
169     */
170    private static final class CopyQuotedStrategy extends Strategy {
171
172        private final String formatField;
173
174        /**
175         * Constructs a Strategy that ensures the formatField has literal text
176         *
177         * @param formatField The literal text to match
178         */
179        CopyQuotedStrategy(final String formatField) {
180            this.formatField = formatField;
181        }
182
183        /**
184         * {@inheritDoc}
185         */
186        @Override
187        boolean isNumber() {
188            return false;
189        }
190
191        @Override
192        boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) {
193            for (int idx = 0; idx < formatField.length(); ++idx) {
194                final int sIdx = idx + pos.getIndex();
195                if (sIdx == source.length() || formatField.charAt(idx) != source.charAt(sIdx)) {
196                    pos.setErrorIndex(sIdx);
197                    return false;
198                }
199            }
200            pos.setIndex(formatField.length() + pos.getIndex());
201            return true;
202        }
203
204        /**
205         * Converts this instance to a handy debug string.
206         *
207         * @since 3.12.0
208         */
209        @Override
210        public String toString() {
211            return "CopyQuotedStrategy [formatField=" + formatField + "]";
212        }
213    }
214
215    private static final class ISO8601TimeZoneStrategy extends PatternStrategy {
216        // Z, +hh, -hh, +hhmm, -hhmm, +hh:mm or -hh:mm
217
218        private static final Strategy ISO_8601_1_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-](?:2[0-3]|[01]\\d)))");
219
220        private static final Strategy ISO_8601_2_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-](?:2[0-3]|[01]\\d)[0-5]\\d))");
221
222        private static final Strategy ISO_8601_3_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-](?:2[0-3]|[01]\\d)(?::)[0-5]\\d))");
223
224        /**
225         * Gets the ISO 8601 time zone strategy.
226         *
227         * @param tokenLen A token indicating the length of the TimeZone String to be formatted.
228         * @return A ISO8601TimeZoneStrategy that can format TimeZone String of length {@code tokenLen}. If no such strategy exists, an IllegalArgumentException
229         *         will be thrown.
230         */
231        static Strategy getStrategy(final int tokenLen) {
232            switch (tokenLen) {
233            case 1:
234                return ISO_8601_1_STRATEGY;
235            case 2:
236                return ISO_8601_2_STRATEGY;
237            case 3:
238                return ISO_8601_3_STRATEGY;
239            default:
240                throw new IllegalArgumentException("Invalid number of X");
241            }
242        }
243
244        /**
245         * Constructs a Strategy that parses a TimeZone
246         *
247         * @param pattern The Pattern
248         */
249        ISO8601TimeZoneStrategy(final String pattern) {
250            createPattern(pattern);
251        }
252
253        /**
254         * {@inheritDoc}
255         */
256        @Override
257        void setCalendar(final FastDateParser parser, final Calendar calendar, final String value) {
258            calendar.setTimeZone(FastTimeZone.getGmtTimeZone(value));
259        }
260    }
261
262    /**
263     * A strategy that handles a number field in the parsing pattern
264     */
265    private static class NumberStrategy extends Strategy {
266
267        private final int field;
268
269        /**
270         * Constructs a Strategy that parses a Number field
271         *
272         * @param field The Calendar field
273         */
274        NumberStrategy(final int field) {
275            this.field = field;
276        }
277
278        /**
279         * {@inheritDoc}
280         */
281        @Override
282        boolean isNumber() {
283            return true;
284        }
285
286        /**
287         * Make any modifications to parsed integer
288         *
289         * @param parser The parser
290         * @param iValue The parsed integer
291         * @return The modified value
292         */
293        int modify(final FastDateParser parser, final int iValue) {
294            return iValue;
295        }
296
297        @Override
298        boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) {
299            int idx = pos.getIndex();
300            int last = source.length();
301            if (maxWidth == 0) {
302                // if no maxWidth, strip leading white space
303                for (; idx < last; ++idx) {
304                    final char c = source.charAt(idx);
305                    if (!Character.isWhitespace(c)) {
306                        break;
307                    }
308                }
309                pos.setIndex(idx);
310            } else {
311                final int end = idx + maxWidth;
312                if (last > end) {
313                    last = end;
314                }
315            }
316            for (; idx < last; ++idx) {
317                final char c = source.charAt(idx);
318                if (!Character.isDigit(c)) {
319                    break;
320                }
321            }
322            if (pos.getIndex() == idx) {
323                pos.setErrorIndex(idx);
324                return false;
325            }
326            final int value;
327            try {
328                value = Integer.parseInt(source.substring(pos.getIndex(), idx));
329            } catch (final NumberFormatException nfe) {
330                // A run of digits that overflows int cannot be represented by this field; signal a parse failure
331                // rather than letting NumberFormatException escape the ParsePosition-based parse methods.
332                pos.setErrorIndex(pos.getIndex());
333                return false;
334            }
335            pos.setIndex(idx);
336            calendar.set(field, modify(parser, value));
337            return true;
338        }
339
340        /**
341         * Converts this instance to a handy debug string.
342         *
343         * @since 3.12.0
344         */
345        @Override
346        public String toString() {
347            return getClass().getSimpleName() + " [field=" + field + "]";
348        }
349    }
350
351    /**
352     * A strategy to parse a single field from the parsing pattern
353     */
354    private abstract static class PatternStrategy extends Strategy {
355
356        Pattern pattern;
357
358        void createPattern(final String regex) {
359            this.pattern = Pattern.compile(regex);
360        }
361
362        void createPattern(final StringBuilder regex) {
363            createPattern(regex.toString());
364        }
365
366        /**
367         * Tests whether this field is numeric. The default implementation returns false.
368         *
369         * @return true, if field is a number
370         */
371        @Override
372        boolean isNumber() {
373            return false;
374        }
375
376        @Override
377        boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) {
378            final Matcher matcher = pattern.matcher(source.substring(pos.getIndex()));
379            if (!matcher.lookingAt()) {
380                pos.setErrorIndex(pos.getIndex());
381                return false;
382            }
383            try {
384                setCalendar(parser, calendar, matcher.group(1));
385            } catch (final IllegalArgumentException e) {
386                // A matched field whose value cannot be interpreted (for example an out-of-range GMT
387                // offset or a display name the key map cannot resolve) is a parse failure, reported
388                // through the ParsePosition error index, not an undeclared runtime exception:
389                // the public parse methods declare only ParseException.
390                pos.setErrorIndex(pos.getIndex());
391                return false;
392            }
393            pos.setIndex(pos.getIndex() + matcher.end(1));
394            return true;
395        }
396
397        abstract void setCalendar(FastDateParser parser, Calendar calendar, String value);
398
399        /**
400         * Converts this instance to a handy debug string.
401         *
402         * @since 3.12.0
403         */
404        @Override
405        public String toString() {
406            return getClass().getSimpleName() + " [pattern=" + pattern + "]";
407        }
408
409    }
410
411    /**
412     * A strategy to parse a single field from the parsing pattern
413     */
414    private abstract static class Strategy {
415
416        /**
417         * Tests whether this field is numeric. The default implementation returns false.
418         *
419         * @return true, if field is a number
420         */
421        boolean isNumber() {
422            return false;
423        }
424
425        abstract boolean parse(FastDateParser parser, Calendar calendar, String source, ParsePosition pos, int maxWidth);
426    }
427
428    /**
429     * Holds strategy and field width
430     */
431    private static final class StrategyAndWidth {
432
433        final Strategy strategy;
434        final int width;
435
436        StrategyAndWidth(final Strategy strategy, final int width) {
437            this.strategy = Objects.requireNonNull(strategy, "strategy");
438            this.width = width;
439        }
440
441        int getMaxWidth(final ListIterator<StrategyAndWidth> lt) {
442            if (!strategy.isNumber() || !lt.hasNext()) {
443                return 0;
444            }
445            final Strategy nextStrategy = lt.next().strategy;
446            lt.previous();
447            return nextStrategy.isNumber() ? width : 0;
448        }
449
450        @Override
451        public String toString() {
452            return "StrategyAndWidth [strategy=" + strategy + ", width=" + width + "]";
453        }
454    }
455
456    /**
457     * Parses the format into strategies.
458     *
459     * <p>
460     * Instances are confined to a single {@link #init(Calendar)} invocation and never escape that invocation.
461     * </p>
462     */
463    private final class StrategyParser {
464        private final Calendar definingCalendar;
465        private int currentIdx;
466
467        StrategyParser(final Calendar definingCalendar) {
468            this.definingCalendar = Objects.requireNonNull(definingCalendar, "definingCalendar");
469        }
470
471        StrategyAndWidth getNextStrategy() {
472            if (currentIdx >= pattern.length()) {
473                return null;
474            }
475            final char c = pattern.charAt(currentIdx);
476            if (CharUtils.isAsciiAlpha(c)) {
477                return letterPattern(c);
478            }
479            return literal();
480        }
481
482        private StrategyAndWidth letterPattern(final char c) {
483            final int begin = currentIdx;
484            while (++currentIdx < pattern.length()) {
485                if (pattern.charAt(currentIdx) != c) {
486                    break;
487                }
488            }
489            final int width = currentIdx - begin;
490            return new StrategyAndWidth(getStrategy(c, width, definingCalendar), width);
491        }
492
493        private StrategyAndWidth literal() {
494            boolean activeQuote = false;
495            final StringBuilder sb = new StringBuilder();
496            while (currentIdx < pattern.length()) {
497                final char c = pattern.charAt(currentIdx);
498                if (!activeQuote && CharUtils.isAsciiAlpha(c)) {
499                    break;
500                }
501                if (c == '\'' && (++currentIdx == pattern.length() || pattern.charAt(currentIdx) != '\'')) {
502                    activeQuote = !activeQuote;
503                    continue;
504                }
505                ++currentIdx;
506                sb.append(c);
507            }
508            if (activeQuote) {
509                throw new IllegalArgumentException("Unterminated quote");
510            }
511            final String formatField = sb.toString();
512            return new StrategyAndWidth(new CopyQuotedStrategy(formatField), formatField.length());
513        }
514    }
515
516    /**
517     * A strategy that handles a time zone field in the parsing pattern
518     */
519    static class TimeZoneStrategy extends PatternStrategy {
520
521        private static final class TzInfo {
522            final TimeZone zone;
523            final int dstOffset;
524
525            TzInfo(final TimeZone tz, final boolean useDst) {
526                zone = tz;
527                dstOffset = useDst ? tz.getDSTSavings() : 0;
528            }
529
530            @Override
531            public String toString() {
532                return "TzInfo [zone=" + zone + ", dstOffset=" + dstOffset + "]";
533            }
534        }
535
536        // The RFC 822 numeric offset form "+hhmm"; SimpleDateFormat rejects "+hh:mm" here, and so must 'z'.
537        private static final String RFC_822_TIME_ZONE = "[+-](?:2[0-3]|[01]\\d)[0-5]\\d";
538
539        /**
540         * Same as {@link #RFC_822_TIME_ZONE} but also accepts the ISO 8601 extended form {@code "+hh:mm"}.
541         */
542        private static final String ISO_8601_EXTENDED_TIME_ZONE = "[+-](?:2[0-3]|[01]\\d):?[0-5]\\d";
543
544        /**
545         * Cache of {@link TimeZoneStrategy} instances that accept the ISO 8601 extended form, keyed by locale. Kept separate from the field caches because 'z'
546         * (and 'Z' with a single letter) must keep exact SimpleDateFormat parity and reject the extended form.
547         */
548        private static final ConcurrentMap<Locale, Strategy> ISO_EXTENDED_ZONE_CACHE = new ConcurrentHashMap<>(3);
549
550        private static final String GMT_OPTION = TimeZones.GMT_ID + "[+-]\\d{1,2}:\\d{2}";
551
552        /**
553         * Index of zone id from {@link DateFormatSymbols#getZoneStrings()}.
554         */
555        private static final int ID = 0;
556
557        static void clearIsoExtendedCache() {
558            ISO_EXTENDED_ZONE_CACHE.clear();
559        }
560
561        /**
562         * Gets a {@link TimeZoneStrategy} that also parses the ISO 8601 extended offset form {@code "+hh:mm"}.
563         *
564         * @param locale The Locale.
565         * @return A shared strategy instance for the given Locale.
566         */
567        static Strategy getIsoExtendedStrategy(final Locale locale) {
568            return ISO_EXTENDED_ZONE_CACHE.computeIfAbsent(LocaleUtils.toLocale(locale), k -> new TimeZoneStrategy(k, true));
569        }
570
571        /**
572         * Tests whether to skip the given time zone, true if TimeZone.getTimeZone().
573         * <p>
574         * On Java 25 and up, skips short IDs if {@code ignoreTimeZoneShortIDs} is true.
575         * </p>
576         * <p>
577         * This method is package private only for testing.
578         * </p>
579         *
580         * @param tzId The ID to test.
581         * @return Whether to skip the given time zone ID.
582         */
583        static boolean skipTimeZone(final String tzId) {
584            return tzId.equalsIgnoreCase(TimeZones.GMT_ID);
585        }
586
587        private final Locale locale;
588
589        /**
590         * Using lower case only or upper case only will cause problems with some Locales like Turkey, Armenia, Colognian and also depending on the Java
591         * version. For details, see https://garygregory.wordpress.com/2015/11/03/java-lowercase-conversion-turkey/
592         */
593        private final Map<String, TzInfo> tzNames = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
594
595        /**
596         * Constructs a Strategy that parses a TimeZone.
597         *
598         * @param locale           The Locale.
599         * @param iso8601Extended Whether to also accept the ISO 8601 extended offset form {@code "+hh:mm"} that FastDatePrinter emits for three or more 'Z'
600         *                        pattern letters.
601         */
602        TimeZoneStrategy(final Locale locale, final boolean iso8601Extended) {
603            this.locale = LocaleUtils.toLocale(locale);
604            final StringBuilder sb = new StringBuilder();
605            sb.append("((?iu)" + (iso8601Extended ? ISO_8601_EXTENDED_TIME_ZONE : RFC_822_TIME_ZONE) + "|" + GMT_OPTION);
606            final Set<String> sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE);
607            // Order is undefined.
608            // TODO Use of getZoneStrings() is discouraged per its Javadoc.
609            final String[][] zones = DateFormatSymbols.getInstance(locale).getZoneStrings();
610            for (final String[] zoneNames : zones) {
611                // offset 0 is the time zone ID and is not localized
612                final String tzId = zoneNames[ID];
613                if (skipTimeZone(tzId)) {
614                    continue;
615                }
616                final TimeZone tz = TimeZones.getTimeZone(tzId);
617                // offset 1 is long standard name
618                // offset 2 is short standard name
619                final TzInfo standard = new TzInfo(tz, false);
620                TzInfo tzInfo = standard;
621                for (int i = 1; i < zoneNames.length; ++i) {
622                    switch (i) {
623                    case 3: // offset 3 is long daylight savings (or summertime) name
624                            // offset 4 is the short summertime name
625                        tzInfo = new TzInfo(tz, true);
626                        break;
627                    case 5: // offset 5 starts additional names, probably standard time
628                        tzInfo = standard;
629                        break;
630                    default:
631                        break;
632                    }
633                    final String zoneName = zoneNames[i];
634                    // ignore the data associated with duplicates supplied in the additional names
635                    if (zoneName != null && sorted.add(zoneName)) {
636                        tzNames.put(zoneName, tzInfo);
637                    }
638                }
639            }
640            // Order is undefined.
641            for (final String tzId : TimeZones.SORTED_AVAILABLE_IDS) {
642                if (skipTimeZone(tzId)) {
643                    continue;
644                }
645                final TimeZone tz = TimeZones.getTimeZone(tzId);
646                final String zoneName = tz.getDisplayName(locale);
647                if (sorted.add(zoneName)) {
648                    tzNames.put(zoneName, new TzInfo(tz, tz.observesDaylightTime()));
649                }
650            }
651            // order the regex alternatives with longer strings first, greedy
652            // match will ensure the longest string will be consumed
653            sorted.forEach(zoneName -> simpleQuote(sb.append('|'), zoneName));
654            sb.append(")");
655            createPattern(sb);
656        }
657
658        /**
659         * {@inheritDoc}
660         */
661        @Override
662        void setCalendar(final FastDateParser parser, final Calendar calendar, final String timeZone) {
663            final TimeZone tz = FastTimeZone.getGmtTimeZone(timeZone);
664            if (tz != null) {
665                calendar.setTimeZone(tz);
666            } else {
667                TzInfo tzInfo = tzNames.get(timeZone);
668                if (tzInfo == null) {
669                    // match missing the optional trailing period
670                    tzInfo = tzNames.get(timeZone + '.');
671                    if (tzInfo == null) {
672                        // Converted to a parse failure by PatternStrategy.parse instead of surfacing as an
673                        // undeclared IllegalStateException; the message is bounded by the matched input
674                        // (no dump of the entire time zone name table).
675                        throw new IllegalArgumentException(
676                                String.format("Can't find time zone '%s' (%d chars)", timeZone, timeZone.length()));
677                    }
678                }
679                calendar.set(Calendar.DST_OFFSET, tzInfo.dstOffset);
680                calendar.set(Calendar.ZONE_OFFSET, tzInfo.zone.getRawOffset());
681            }
682        }
683
684        /**
685         * Converts this instance to a handy debug string.
686         *
687         * @since 3.12.0
688         */
689        @Override
690        public String toString() {
691            return "TimeZoneStrategy [locale=" + locale + ", tzNames=" + tzNames + ", pattern=" + pattern + "]";
692        }
693
694    }
695
696    /**
697     * A write-through recorder used while parsing a pattern that contains a week year ('Y'). Every mutation is delegated to the real target calendar
698     * unchanged, and the raw values assigned to the three week-date fields are additionally captured, so that after all fields are parsed the week date can
699     * be resolved from exactly what was parsed - mirroring {@code java.text.CalendarBuilder}, which {@link java.text.SimpleDateFormat} uses for the same
700     * purpose. (Reading the values back from the calendar instead would normalize them: {@link Calendar#get(int)} resolves the complete date, so a parsed
701     * week 53 read back through a calendar-year interpretation can roll the year and land a full year away.)
702     */
703    private static final class WeekDateRecorder extends GregorianCalendar {
704
705        private static final long serialVersionUID = 1L;
706
707        /** The calendar every mutation is delegated to. */
708        private final Calendar target;
709
710        private transient int weekYearValue;
711        private transient boolean weekYearSet;
712        private transient int weekOfYearValue;
713        private transient boolean weekOfYearSet;
714        private transient int dayOfWeekValue;
715        private transient boolean dayOfWeekSet;
716
717        WeekDateRecorder(final Calendar target) {
718            this.target = target;
719        }
720
721        /**
722         * Resolves the recorded week year through the target calendar's week-date machinery. The parsed 'Y' value was delegated into {@link Calendar#YEAR}
723         * by the number strategy; {@link Calendar#setWeekDate(int, int, int)} reinterprets it as a week year together with the parsed week of year and day
724         * of week, defaulting to week 1 and the calendar's first day-of-week when the pattern did not contain them (the same defaults as
725         * {@code java.text.CalendarBuilder}). The fields set by {@code setWeekDate} take precedence over any month/day fields parsed earlier, which matches
726         * {@link java.text.SimpleDateFormat}.
727         */
728        void applyWeekDate() {
729            if (!weekYearSet) {
730                return;
731            }
732            int weekOfYear = weekOfYearSet ? weekOfYearValue : 1;
733            int dayOfWeek = dayOfWeekSet ? dayOfWeekValue : target.getFirstDayOfWeek();
734            if (!isValidDayOfWeek(dayOfWeek) && target.isLenient()) {
735                // Calendar.setWeekDate rejects a day of week outside SUNDAY..SATURDAY even on a lenient calendar, so fold the overflow of an
736                // out-of-range 'u' value into the week number first, exactly as java.text.CalendarBuilder does for SimpleDateFormat.
737                if (dayOfWeek >= 8) {
738                    dayOfWeek--;
739                    weekOfYear += dayOfWeek / 7;
740                    dayOfWeek = dayOfWeek % 7 + 1;
741                } else {
742                    while (dayOfWeek <= 0) {
743                        dayOfWeek += 7;
744                        weekOfYear--;
745                    }
746                }
747                dayOfWeek = toCalendarDayOfWeek(dayOfWeek);
748            }
749            target.setWeekDate(weekYearValue, weekOfYear, dayOfWeek);
750        }
751
752        @Override
753        public void set(final int field, final int value) {
754            if (target == null) {
755                // Callers from the superclass constructors, before this recorder is fully constructed.
756                super.set(field, value);
757                return;
758            }
759            switch (field) {
760            case Calendar.YEAR:
761                weekYearValue = value;
762                weekYearSet = true;
763                break;
764            case Calendar.WEEK_OF_YEAR:
765                weekOfYearValue = value;
766                weekOfYearSet = true;
767                break;
768            case Calendar.DAY_OF_WEEK:
769                dayOfWeekValue = value;
770                dayOfWeekSet = true;
771                break;
772            default:
773                break;
774            }
775            target.set(field, value);
776        }
777
778        @Override
779        public void setTimeZone(final TimeZone zone) {
780            if (target == null) {
781                // Callers from the superclass constructors, before this recorder is fully constructed.
782                super.setTimeZone(zone);
783                return;
784            }
785            target.setTimeZone(zone);
786        }
787    }
788
789    /**
790     * Required for serialization support.
791     *
792     * @see java.io.Serializable
793     */
794    private static final long serialVersionUID = 3L;
795
796    static final Locale JAPANESE_IMPERIAL = new Locale("ja", "JP", "JP");
797
798    // helper classes to parse the format string
799
800    /**
801     * comparator used to sort regex alternatives. Alternatives should be ordered longer first, and shorter last. ('february' before 'feb'). All entries must be
802     * lower-case by locale.
803     */
804    private static final Comparator<String> LONGER_FIRST_LOWERCASE = Comparator.reverseOrder();
805
806    @SuppressWarnings("unchecked") // OK because we are creating an array with no entries
807    private static final ConcurrentMap<Locale, Strategy>[] CACHES = new ConcurrentMap[Calendar.FIELD_COUNT];
808
809    private static final Strategy ABBREVIATED_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR) {
810
811        /**
812         * {@inheritDoc}
813         */
814        @Override
815        int modify(final FastDateParser parser, final int iValue) {
816            return iValue < 100 ? parser.adjustYear(iValue) : iValue;
817        }
818    };
819
820    private static final Strategy NUMBER_MONTH_STRATEGY = new NumberStrategy(Calendar.MONTH) {
821        @Override
822        int modify(final FastDateParser parser, final int iValue) {
823            return iValue - 1;
824        }
825    };
826
827    private static final Strategy LITERAL_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR);
828
829    private static final Strategy WEEK_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_YEAR);
830
831    private static final Strategy WEEK_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_MONTH);
832
833    private static final Strategy DAY_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.DAY_OF_YEAR);
834
835    private static final Strategy DAY_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_MONTH);
836
837    private static final Strategy DAY_OF_WEEK_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK) {
838        @Override
839        int modify(final FastDateParser parser, final int iValue) {
840            return toCalendarDayOfWeek(iValue);
841        }
842    };
843
844    private static final Strategy DAY_OF_WEEK_IN_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK_IN_MONTH);
845
846    private static final Strategy HOUR_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY);
847
848    private static final Strategy HOUR24_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY) {
849        @Override
850        int modify(final FastDateParser parser, final int iValue) {
851            return iValue == 24 ? 0 : iValue;
852        }
853    };
854
855    private static final Strategy HOUR12_STRATEGY = new NumberStrategy(Calendar.HOUR) {
856        @Override
857        int modify(final FastDateParser parser, final int iValue) {
858            return iValue == 12 ? 0 : iValue;
859        }
860    };
861
862    private static final Strategy HOUR_STRATEGY = new NumberStrategy(Calendar.HOUR);
863
864    private static final Strategy MINUTE_STRATEGY = new NumberStrategy(Calendar.MINUTE);
865
866    private static final Strategy SECOND_STRATEGY = new NumberStrategy(Calendar.SECOND);
867
868    private static final Strategy MILLISECOND_STRATEGY = new NumberStrategy(Calendar.MILLISECOND);
869
870    /**
871     * Gets the short and long values displayed for a field
872     *
873     * @param calendar The calendar to obtain the short and long values
874     * @param locale   The locale of display names
875     * @param field    The field of interest
876     * @param regex    The regular expression to build
877     * @return The map of string display names to field values
878     */
879    private static Map<String, Integer> appendDisplayNames(final Calendar calendar, final Locale locale, final int field, final StringBuilder regex) {
880        Objects.requireNonNull(calendar, "calendar");
881        final Map<String, Integer> values = new HashMap<>();
882        final Locale actualLocale = LocaleUtils.toLocale(locale);
883        final Map<String, Integer> displayNames = calendar.getDisplayNames(field, Calendar.ALL_STYLES, actualLocale);
884        final TreeSet<String> sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE);
885        displayNames.forEach((k, v) -> {
886            final String keyLc = k.toLowerCase(actualLocale);
887            if (sorted.add(keyLc)) {
888                values.put(keyLc, v);
889            }
890        });
891        sorted.forEach(symbol -> simpleQuote(regex, symbol).append('|'));
892        return values;
893    }
894
895    /**
896     * Clears the cache.
897     */
898    static void clear() {
899        Stream.of(CACHES).filter(Objects::nonNull).forEach(ConcurrentMap::clear);
900        TimeZoneStrategy.clearIsoExtendedCache();
901    }
902
903    /**
904     * Gets a cache of Strategies for a particular field
905     *
906     * @param field The Calendar field
907     * @return A cache of Locale to Strategy
908     */
909    private static ConcurrentMap<Locale, Strategy> getCache(final int field) {
910        synchronized (CACHES) {
911            if (CACHES[field] == null) {
912                CACHES[field] = new ConcurrentHashMap<>(3);
913            }
914            return CACHES[field];
915        }
916    }
917
918    /**
919     * Tests whether a value is a valid {@link Calendar#DAY_OF_WEEK} value, in the range {@link Calendar#SUNDAY} to {@link Calendar#SATURDAY}.
920     *
921     * @param dayOfWeek the value to test.
922     * @return true if the value is in range.
923     */
924    private static boolean isValidDayOfWeek(final int dayOfWeek) {
925        return dayOfWeek >= Calendar.SUNDAY && dayOfWeek <= Calendar.SATURDAY;
926    }
927
928    private static StringBuilder simpleQuote(final StringBuilder sb, final String value) {
929        for (int i = 0; i < value.length(); ++i) {
930            final char c = value.charAt(i);
931            switch (c) {
932            case '\\':
933            case '^':
934            case '$':
935            case '.':
936            case '|':
937            case '?':
938            case '*':
939            case '+':
940            case '(':
941            case ')':
942            case '[':
943            case '{':
944                sb.append('\\');
945                // falls-through
946            default:
947                sb.append(c);
948            }
949        }
950        if (sb.charAt(sb.length() - 1) == '.') {
951            // trailing '.' is optional
952            sb.append('?');
953        }
954        return sb;
955    }
956
957    /**
958     * Converts an ISO 8601 day number of week ('u', Monday = 1 to Sunday = 7) to a {@link Calendar#DAY_OF_WEEK} value.
959     *
960     * <p>
961     * An out-of-range value is returned unchanged so a lenient calendar can adjust it, as {@code java.text.CalendarBuilder} does for
962     * {@link java.text.SimpleDateFormat}.
963     * </p>
964     *
965     * @param isoDayOfWeek the parsed ISO day number of week.
966     * @return the {@link Calendar#DAY_OF_WEEK} value, or the input when it is out of range.
967     */
968    private static int toCalendarDayOfWeek(final int isoDayOfWeek) {
969        if (!isValidDayOfWeek(isoDayOfWeek)) {
970            return isoDayOfWeek;
971        }
972        return isoDayOfWeek == 7 ? Calendar.SUNDAY : isoDayOfWeek + 1;
973    }
974
975    /** Input pattern. */
976    private final String pattern;
977
978    /** Input TimeZone. */
979    private final TimeZone timeZone;
980
981    /** Input Locale. */
982    private final Locale locale;
983
984    /**
985     * Century from Date.
986     */
987    private final int century;
988
989    /**
990     * Start year from Date.
991     */
992    private final int startYear;
993
994    /** Initialized from Calendar. */
995    private transient List<StrategyAndWidth> patterns;
996
997    /**
998     * Whether the pattern contains a week-year field ('Y'). Derived from the pattern in {@link #init(Calendar)} (called from the constructor and from
999     * readObject), so it does not need to be serialized.
1000     */
1001    private transient volatile boolean weekYear;
1002
1003    /**
1004     * Constructs a new FastDateParser.
1005     *
1006     * Use {@link FastDateFormat#getInstance(String, TimeZone, Locale)} or another variation of the factory methods of {@link FastDateFormat} to get a cached
1007     * FastDateParser instance.
1008     *
1009     * @param pattern  non-null {@link java.text.SimpleDateFormat} compatible pattern
1010     * @param timeZone non-null time zone to use
1011     * @param locale   non-null locale
1012     */
1013    protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale) {
1014        this(pattern, timeZone, locale, null);
1015    }
1016
1017    /**
1018     * Constructs a new FastDateParser.
1019     *
1020     * @param pattern      non-null {@link java.text.SimpleDateFormat} compatible pattern
1021     * @param timeZone     non-null time zone to use
1022     * @param locale       locale, null maps to the default Locale.
1023     * @param centuryStart The start of the century for 2 digit year parsing
1024     * @since 3.5
1025     */
1026    protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale, final Date centuryStart) {
1027        this.pattern = Objects.requireNonNull(pattern, "pattern");
1028        // TimeZone is mutable and instances are shared through the FastDateFormat cache.
1029        this.timeZone = (TimeZone) Objects.requireNonNull(timeZone, "timeZone").clone();
1030        this.locale = LocaleUtils.toLocale(locale);
1031        final Calendar definingCalendar = Calendar.getInstance(timeZone, this.locale);
1032        final int centuryStartYear;
1033        if (centuryStart != null) {
1034            definingCalendar.setTime(centuryStart);
1035            centuryStartYear = definingCalendar.get(Calendar.YEAR);
1036        } else if (this.locale.equals(JAPANESE_IMPERIAL)) {
1037            centuryStartYear = 0;
1038        } else {
1039            // from 80 years ago to 20 years from now
1040            definingCalendar.setTime(new Date());
1041            centuryStartYear = definingCalendar.get(Calendar.YEAR) - 80;
1042        }
1043        century = centuryStartYear / 100 * 100;
1044        startYear = centuryStartYear - century;
1045        init(definingCalendar);
1046    }
1047
1048    /**
1049     * Adjusts dates to be within appropriate century
1050     *
1051     * @param twoDigitYear The year to adjust
1052     * @return A value between centuryStart(inclusive) to centuryStart+100(exclusive)
1053     */
1054    private int adjustYear(final int twoDigitYear) {
1055        final int trial = century + twoDigitYear;
1056        return twoDigitYear >= startYear ? trial : trial + 100;
1057    }
1058
1059    private boolean checkLength(final String source, final ParsePosition pos) {
1060        final int startIndex = pos.getIndex();
1061        if (startIndex > source.length()) {
1062            pos.setErrorIndex(startIndex);
1063            return false;
1064        }
1065        return true;
1066    }
1067
1068    /**
1069     * Compares another object for equality with this object.
1070     *
1071     * @param obj The object to compare to
1072     * @return {@code true}if equal to this instance
1073     */
1074    @Override
1075    public boolean equals(final Object obj) {
1076        if (!(obj instanceof FastDateParser)) {
1077            return false;
1078        }
1079        final FastDateParser other = (FastDateParser) obj;
1080        return pattern.equals(other.pattern) && timeZone.equals(other.timeZone) && locale.equals(other.locale);
1081    }
1082
1083    /*
1084     * (non-Javadoc)
1085     *
1086     * @see org.apache.commons.lang3.time.DateParser#getLocale()
1087     */
1088    @Override
1089    public Locale getLocale() {
1090        return locale;
1091    }
1092
1093    /**
1094     * Gets a strategy that parses a text field.
1095     *
1096     * @param field            The Calendar field
1097     * @param definingCalendar The calendar to obtain the short and long values
1098     * @return A TextStrategy for the field and Locale
1099     */
1100    private Strategy getLocaleSpecificStrategy(final int field, final Calendar definingCalendar) {
1101        return getCache(field).computeIfAbsent(locale,
1102                k -> field == Calendar.ZONE_OFFSET ? new TimeZoneStrategy(locale, false)
1103                        : new CaseInsensitiveTextStrategy(field, definingCalendar, locale));
1104    }
1105
1106    /*
1107     * (non-Javadoc)
1108     *
1109     * @see org.apache.commons.lang3.time.DateParser#getPattern()
1110     */
1111    @Override
1112    public String getPattern() {
1113        return pattern;
1114    }
1115
1116    List<StrategyAndWidth> getPatterns() {
1117        return patterns;
1118    }
1119
1120    /**
1121     * Gets a Strategy given a field from a SimpleDateFormat pattern
1122     *
1123     * @param f                A sub-sequence of the SimpleDateFormat pattern
1124     * @param width            formatting width
1125     * @param definingCalendar The calendar to obtain the short and long values
1126     * @return The Strategy that will handle parsing for the field
1127     */
1128    private Strategy getStrategy(final char f, final int width, final Calendar definingCalendar) {
1129        switch (f) {
1130        case 'D':
1131            return DAY_OF_YEAR_STRATEGY;
1132        case 'E':
1133            return getLocaleSpecificStrategy(Calendar.DAY_OF_WEEK, definingCalendar);
1134        case 'F':
1135            return DAY_OF_WEEK_IN_MONTH_STRATEGY;
1136        case 'G':
1137            return getLocaleSpecificStrategy(Calendar.ERA, definingCalendar);
1138        case 'H': // Hour in day (0-23)
1139            return HOUR_OF_DAY_STRATEGY;
1140        case 'K': // Hour in am/pm (0-11)
1141            return HOUR_STRATEGY;
1142        case 'M':
1143        case 'L':
1144            return width >= 3 ? getLocaleSpecificStrategy(Calendar.MONTH, definingCalendar) : NUMBER_MONTH_STRATEGY;
1145        case 'S':
1146            return MILLISECOND_STRATEGY;
1147        case 'W':
1148            return WEEK_OF_MONTH_STRATEGY;
1149        case 'a':
1150            return getLocaleSpecificStrategy(Calendar.AM_PM, definingCalendar);
1151        case 'd':
1152            return DAY_OF_MONTH_STRATEGY;
1153        case 'h': // Hour in am/pm (1-12), i.e. midday/midnight is 12, not 0
1154            return HOUR12_STRATEGY;
1155        case 'k': // Hour in day (1-24), i.e. midnight is 24, not 0
1156            return HOUR24_OF_DAY_STRATEGY;
1157        case 'm':
1158            return MINUTE_STRATEGY;
1159        case 's':
1160            return SECOND_STRATEGY;
1161        case 'u':
1162            return DAY_OF_WEEK_STRATEGY;
1163        case 'w':
1164            return WEEK_OF_YEAR_STRATEGY;
1165        case 'y':
1166            return width > 2 ? LITERAL_YEAR_STRATEGY : ABBREVIATED_YEAR_STRATEGY;
1167        case 'Y':
1168            // Week year: the number is parsed like a year (including the two-digit-century adjustment,
1169            // as SimpleDateFormat does for 'YY'), but it must be resolved through the calendar's
1170            // week-date machinery rather than Calendar.YEAR. Record that this pattern contains a week
1171            // year; parse(String, ParsePosition, Calendar) re-resolves the date via setWeekDate,
1172            // mirroring FastDatePrinter's WeekYear rule and java.text.CalendarBuilder. When the
1173            // calendar does not support week dates, the value falls back to Calendar.YEAR, exactly
1174            // like FastDatePrinter's fallback.
1175            weekYear = true;
1176            return width > 2 ? LITERAL_YEAR_STRATEGY : ABBREVIATED_YEAR_STRATEGY;
1177        case 'X':
1178            return ISO8601TimeZoneStrategy.getStrategy(width);
1179        case 'Z':
1180            if (width == 2) {
1181                return ISO8601TimeZoneStrategy.ISO_8601_3_STRATEGY;
1182            }
1183            if (width >= 3) {
1184                // FastDatePrinter formats three or more 'Z' as the ISO 8601 extended offset "+hh:mm", so parsing
1185                // 'Z' must accept that form as well. 'z' keeps exact SimpleDateFormat parity and rejects it.
1186                return TimeZoneStrategy.getIsoExtendedStrategy(locale);
1187            }
1188            // falls-through
1189        case 'z':
1190            return getLocaleSpecificStrategy(Calendar.ZONE_OFFSET, definingCalendar);
1191        default:
1192            throw new IllegalArgumentException("Format '" + f + "' not supported");
1193        }
1194    }
1195
1196    /*
1197     * (non-Javadoc)
1198     *
1199     * @see org.apache.commons.lang3.time.DateParser#getTimeZone()
1200     */
1201    @Override
1202    public TimeZone getTimeZone() {
1203        return (TimeZone) timeZone.clone();
1204    }
1205
1206    /**
1207     * Returns a hash code compatible with equals.
1208     *
1209     * @return A hash code compatible with equals
1210     */
1211    @Override
1212    public int hashCode() {
1213        return pattern.hashCode() + 13 * (timeZone.hashCode() + 13 * locale.hashCode());
1214    }
1215
1216    /**
1217     * Initializes derived fields from defining fields. This is called from constructor and from readObject (de-serialization)
1218     *
1219     * @param definingCalendar The {@link java.util.Calendar} instance used to initialize this FastDateParser
1220     */
1221    private void init(final Calendar definingCalendar) {
1222        patterns = new ArrayList<>();
1223
1224        final StrategyParser strategyParser = new StrategyParser(definingCalendar);
1225        for (;;) {
1226            final StrategyAndWidth field = strategyParser.getNextStrategy();
1227            if (field == null) {
1228                break;
1229            }
1230            patterns.add(field);
1231        }
1232    }
1233
1234    /*
1235     * (non-Javadoc)
1236     *
1237     * @see org.apache.commons.lang3.time.DateParser#parse(String)
1238     */
1239    @Override
1240    public Date parse(final String source) throws ParseException {
1241        final ParsePosition pp = new ParsePosition(0);
1242        final Date date = parse(source, pp);
1243        if (date == null) {
1244            // Add a note regarding supported date range
1245            final int errorIndex = pp.getErrorIndex();
1246            final String msg = String.format("Unparseable date: '%s', parse position = %s", source, pp);
1247            if (locale.equals(JAPANESE_IMPERIAL)) {
1248                throw new ParseException(String.format("%s; the %s locale does not support dates before 1868-01-01.", msg, locale), errorIndex);
1249            }
1250            throw new ParseException(msg, errorIndex);
1251        }
1252        return date;
1253    }
1254
1255    /**
1256     * This implementation updates the ParsePosition if the parse succeeds. However, it sets the error index to the position before the failed field unlike the
1257     * method {@link java.text.SimpleDateFormat#parse(String, ParsePosition)} which sets the error index to after the failed field.
1258     * <p>
1259     * To determine if the parse has succeeded, the caller must check if the current parse position given by {@link ParsePosition#getIndex()} has been updated.
1260     * If the input buffer has been fully parsed, then the index will point to just after the end of the input buffer.
1261     * </p>
1262     *
1263     * @see org.apache.commons.lang3.time.DateParser#parse(String, java.text.ParsePosition)
1264     */
1265    @Override
1266    public Date parse(final String source, final ParsePosition pos) {
1267        if (!checkLength(source, pos)) {
1268            return null;
1269        }
1270        // timing tests indicate getting new instance is 19% faster than cloning
1271        final Calendar cal = Calendar.getInstance(timeZone, locale);
1272        cal.clear();
1273        return parse(source, pos, cal) ? cal.getTime() : null;
1274    }
1275
1276    /**
1277     * Parses a formatted date string according to the format. Updates the Calendar with parsed fields. Upon success, the ParsePosition index is updated to
1278     * 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
1279     * the offset of the source text which does not match the supplied format.
1280     *
1281     * @param source   The text to parse.
1282     * @param pos      On input, the position in the source to start parsing, on output, updated position.
1283     * @param calendar The calendar into which to set parsed fields.
1284     * @return true, if source has been parsed (pos parsePosition is updated); otherwise false (and pos errorIndex is updated)
1285     * @throws IllegalArgumentException Thrown when Calendar has been set to be not lenient, and a parsed field is out of range.
1286     */
1287    @Override
1288    public boolean parse(final String source, final ParsePosition pos, final Calendar calendar) {
1289        if (!checkLength(source, pos)) {
1290            return false;
1291        }
1292        final WeekDateRecorder recorder = weekYear && calendar.isWeekDateSupported() ? new WeekDateRecorder(calendar) : null;
1293        final Calendar sink = recorder != null ? recorder : calendar;
1294        final ListIterator<StrategyAndWidth> lt = patterns.listIterator();
1295        while (lt.hasNext()) {
1296            final StrategyAndWidth strategyAndWidth = lt.next();
1297            final int maxWidth = strategyAndWidth.getMaxWidth(lt);
1298            if (!strategyAndWidth.strategy.parse(this, sink, source, pos, maxWidth)) {
1299                return false;
1300            }
1301        }
1302        if (recorder != null) {
1303            recorder.applyWeekDate();
1304        }
1305        return true;
1306    }
1307
1308    /*
1309     * (non-Javadoc)
1310     *
1311     * @see org.apache.commons.lang3.time.DateParser#parseObject(String)
1312     */
1313    @Override
1314    public Object parseObject(final String source) throws ParseException {
1315        return parse(source);
1316    }
1317
1318    /*
1319     * (non-Javadoc)
1320     *
1321     * @see org.apache.commons.lang3.time.DateParser#parseObject(String, java.text.ParsePosition)
1322     */
1323    @Override
1324    public Object parseObject(final String source, final ParsePosition pos) {
1325        return parse(source, pos);
1326    }
1327
1328    /**
1329     * Creates the object after serialization. This implementation reinitializes the transient properties.
1330     *
1331     * @param in ObjectInputStream from which the object is being deserialized.
1332     * @throws IOException            Thrown if there is an IO issue.
1333     * @throws ClassNotFoundException Thrown if a class cannot be found.
1334     */
1335    private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
1336        in.defaultReadObject();
1337        SerializationUtils.requireNonNull(pattern, "pattern null");
1338        SerializationUtils.requireNonNull(timeZone, "timeZone null");
1339        init(Calendar.getInstance(timeZone, locale));
1340    }
1341
1342    /**
1343     * Gets a string version of this formatter.
1344     *
1345     * @return A debugging string
1346     */
1347    @Override
1348    public String toString() {
1349        return "FastDateParser[" + pattern + ", " + locale + ", " + timeZone.getID() + "]";
1350    }
1351
1352
1353    /**
1354     * Converts all state of this instance to a String handy for debugging.
1355     *
1356     * @return A string.
1357     * @since 3.12.0
1358     */
1359    public String toStringAll() {
1360        return "FastDateParser [pattern=" + pattern + ", timeZone=" + timeZone + ", locale=" + locale + ", century=" + century + ", startYear=" + startYear
1361                + ", patterns=" + StringUtils.join(patterns, ", " + System.lineSeparator() + "\t") + "]";
1362    }
1363}