1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.lang3.time;
18
19 import java.io.IOException;
20 import java.io.ObjectInputStream;
21 import java.io.Serializable;
22 import java.text.DateFormatSymbols;
23 import java.text.ParseException;
24 import java.text.ParsePosition;
25 import java.text.SimpleDateFormat;
26 import java.util.ArrayList;
27 import java.util.Calendar;
28 import java.util.Comparator;
29 import java.util.Date;
30 import java.util.GregorianCalendar;
31 import java.util.HashMap;
32 import java.util.List;
33 import java.util.ListIterator;
34 import java.util.Locale;
35 import java.util.Map;
36 import java.util.Objects;
37 import java.util.Set;
38 import java.util.TimeZone;
39 import java.util.TreeMap;
40 import java.util.TreeSet;
41 import java.util.concurrent.ConcurrentHashMap;
42 import java.util.concurrent.ConcurrentMap;
43 import java.util.regex.Matcher;
44 import java.util.regex.Pattern;
45 import java.util.stream.Stream;
46
47 import org.apache.commons.lang3.CharUtils;
48 import org.apache.commons.lang3.LocaleUtils;
49 import org.apache.commons.lang3.SerializationUtils;
50 import org.apache.commons.lang3.StringUtils;
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88 public class FastDateParser implements DateParser, Serializable {
89
90
91
92
93 private static final class CaseInsensitiveTextStrategy extends PatternStrategy {
94
95 private final int field;
96 private final Locale locale;
97 private final Map<String, Integer> lKeyValues;
98
99
100
101
102
103
104
105
106 CaseInsensitiveTextStrategy(final int field, final Calendar definingCalendar, final Locale locale) {
107 this.field = field;
108 this.locale = LocaleUtils.toLocale(locale);
109 final StringBuilder regex = new StringBuilder();
110 regex.append("((?iu)");
111 lKeyValues = appendDisplayNames(definingCalendar, locale, field, regex);
112 regex.setLength(regex.length() - 1);
113 regex.append(")");
114 createPattern(regex);
115 }
116
117
118
119
120 @Override
121 void setCalendar(final FastDateParser parser, final Calendar calendar, final String value) {
122 String lowerCase = value.toLowerCase(locale);
123 Integer iVal = lKeyValues.get(lowerCase);
124 if (iVal == null) {
125
126 iVal = lKeyValues.get(lowerCase + '.');
127 }
128 if (iVal == null) {
129
130
131
132
133 lowerCase = value.toLowerCase(Locale.ROOT);
134 iVal = lKeyValues.get(lowerCase);
135 if (iVal == null) {
136 iVal = lKeyValues.get(lowerCase + '.');
137 }
138 }
139 if (iVal == null) {
140
141
142 throw new IllegalArgumentException("Invalid display name for field " + field + ": '" + value + "'");
143 }
144
145 if (Calendar.AM_PM != this.field || iVal <= 1) {
146 calendar.set(field, iVal.intValue());
147 }
148 }
149
150
151
152
153
154
155 @Override
156 public String toString() {
157 return "CaseInsensitiveTextStrategy [field=" + field + ", locale=" + locale + ", lKeyValues=" + lKeyValues + ", pattern=" + pattern + "]";
158 }
159 }
160
161
162
163
164 private static final class CopyQuotedStrategy extends Strategy {
165
166 private final String formatField;
167
168
169
170
171
172
173 CopyQuotedStrategy(final String formatField) {
174 this.formatField = formatField;
175 }
176
177
178
179
180 @Override
181 boolean isNumber() {
182 return false;
183 }
184
185 @Override
186 boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) {
187 for (int idx = 0; idx < formatField.length(); ++idx) {
188 final int sIdx = idx + pos.getIndex();
189 if (sIdx == source.length() || formatField.charAt(idx) != source.charAt(sIdx)) {
190 pos.setErrorIndex(sIdx);
191 return false;
192 }
193 }
194 pos.setIndex(formatField.length() + pos.getIndex());
195 return true;
196 }
197
198
199
200
201
202
203 @Override
204 public String toString() {
205 return "CopyQuotedStrategy [formatField=" + formatField + "]";
206 }
207 }
208
209 private static final class ISO8601TimeZoneStrategy extends PatternStrategy {
210
211
212 private static final Strategy ISO_8601_1_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-](?:2[0-3]|[01]\\d)))");
213
214 private static final Strategy ISO_8601_2_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-](?:2[0-3]|[01]\\d)[0-5]\\d))");
215
216 private static final Strategy ISO_8601_3_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-](?:2[0-3]|[01]\\d)(?::)[0-5]\\d))");
217
218
219
220
221
222
223
224
225 static Strategy getStrategy(final int tokenLen) {
226 switch (tokenLen) {
227 case 1:
228 return ISO_8601_1_STRATEGY;
229 case 2:
230 return ISO_8601_2_STRATEGY;
231 case 3:
232 return ISO_8601_3_STRATEGY;
233 default:
234 throw new IllegalArgumentException("Invalid number of X");
235 }
236 }
237
238
239
240
241
242
243 ISO8601TimeZoneStrategy(final String pattern) {
244 createPattern(pattern);
245 }
246
247
248
249
250 @Override
251 void setCalendar(final FastDateParser parser, final Calendar calendar, final String value) {
252 calendar.setTimeZone(FastTimeZone.getGmtTimeZone(value));
253 }
254 }
255
256
257
258
259 private static class NumberStrategy extends Strategy {
260
261 private final int field;
262
263
264
265
266
267
268 NumberStrategy(final int field) {
269 this.field = field;
270 }
271
272
273
274
275 @Override
276 boolean isNumber() {
277 return true;
278 }
279
280
281
282
283
284
285
286
287 int modify(final FastDateParser parser, final int iValue) {
288 return iValue;
289 }
290
291 @Override
292 boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) {
293 int idx = pos.getIndex();
294 int last = source.length();
295 if (maxWidth == 0) {
296
297 for (; idx < last; ++idx) {
298 final char c = source.charAt(idx);
299 if (!Character.isWhitespace(c)) {
300 break;
301 }
302 }
303 pos.setIndex(idx);
304 } else {
305 final int end = idx + maxWidth;
306 if (last > end) {
307 last = end;
308 }
309 }
310 for (; idx < last; ++idx) {
311 final char c = source.charAt(idx);
312 if (!Character.isDigit(c)) {
313 break;
314 }
315 }
316 if (pos.getIndex() == idx) {
317 pos.setErrorIndex(idx);
318 return false;
319 }
320 final int value;
321 try {
322 value = Integer.parseInt(source.substring(pos.getIndex(), idx));
323 } catch (final NumberFormatException nfe) {
324
325
326 pos.setErrorIndex(pos.getIndex());
327 return false;
328 }
329 pos.setIndex(idx);
330 calendar.set(field, modify(parser, value));
331 return true;
332 }
333
334
335
336
337
338
339 @Override
340 public String toString() {
341 return getClass().getSimpleName() + " [field=" + field + "]";
342 }
343 }
344
345
346
347
348 private abstract static class PatternStrategy extends Strategy {
349
350 Pattern pattern;
351
352 void createPattern(final String regex) {
353 this.pattern = Pattern.compile(regex);
354 }
355
356 void createPattern(final StringBuilder regex) {
357 createPattern(regex.toString());
358 }
359
360
361
362
363
364
365 @Override
366 boolean isNumber() {
367 return false;
368 }
369
370 @Override
371 boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) {
372 final Matcher matcher = pattern.matcher(source.substring(pos.getIndex()));
373 if (!matcher.lookingAt()) {
374 pos.setErrorIndex(pos.getIndex());
375 return false;
376 }
377 try {
378 setCalendar(parser, calendar, matcher.group(1));
379 } catch (final IllegalArgumentException e) {
380
381
382
383
384 pos.setErrorIndex(pos.getIndex());
385 return false;
386 }
387 pos.setIndex(pos.getIndex() + matcher.end(1));
388 return true;
389 }
390
391 abstract void setCalendar(FastDateParser parser, Calendar calendar, String value);
392
393
394
395
396
397
398 @Override
399 public String toString() {
400 return getClass().getSimpleName() + " [pattern=" + pattern + "]";
401 }
402
403 }
404
405
406
407
408 private abstract static class Strategy {
409
410
411
412
413
414
415 boolean isNumber() {
416 return false;
417 }
418
419 abstract boolean parse(FastDateParser parser, Calendar calendar, String source, ParsePosition pos, int maxWidth);
420 }
421
422
423
424
425 private static final class StrategyAndWidth {
426
427 final Strategy strategy;
428 final int width;
429
430 StrategyAndWidth(final Strategy strategy, final int width) {
431 this.strategy = Objects.requireNonNull(strategy, "strategy");
432 this.width = width;
433 }
434
435 int getMaxWidth(final ListIterator<StrategyAndWidth> lt) {
436 if (!strategy.isNumber() || !lt.hasNext()) {
437 return 0;
438 }
439 final Strategy nextStrategy = lt.next().strategy;
440 lt.previous();
441 return nextStrategy.isNumber() ? width : 0;
442 }
443
444 @Override
445 public String toString() {
446 return "StrategyAndWidth [strategy=" + strategy + ", width=" + width + "]";
447 }
448 }
449
450
451
452
453 private final class StrategyParser {
454 private final Calendar definingCalendar;
455 private int currentIdx;
456
457 StrategyParser(final Calendar definingCalendar) {
458 this.definingCalendar = Objects.requireNonNull(definingCalendar, "definingCalendar");
459 }
460
461 StrategyAndWidth getNextStrategy() {
462 if (currentIdx >= pattern.length()) {
463 return null;
464 }
465 final char c = pattern.charAt(currentIdx);
466 if (CharUtils.isAsciiAlpha(c)) {
467 return letterPattern(c);
468 }
469 return literal();
470 }
471
472 private StrategyAndWidth letterPattern(final char c) {
473 final int begin = currentIdx;
474 while (++currentIdx < pattern.length()) {
475 if (pattern.charAt(currentIdx) != c) {
476 break;
477 }
478 }
479 final int width = currentIdx - begin;
480 return new StrategyAndWidth(getStrategy(c, width, definingCalendar), width);
481 }
482
483 private StrategyAndWidth literal() {
484 boolean activeQuote = false;
485 final StringBuilder sb = new StringBuilder();
486 while (currentIdx < pattern.length()) {
487 final char c = pattern.charAt(currentIdx);
488 if (!activeQuote && CharUtils.isAsciiAlpha(c)) {
489 break;
490 }
491 if (c == '\'' && (++currentIdx == pattern.length() || pattern.charAt(currentIdx) != '\'')) {
492 activeQuote = !activeQuote;
493 continue;
494 }
495 ++currentIdx;
496 sb.append(c);
497 }
498 if (activeQuote) {
499 throw new IllegalArgumentException("Unterminated quote");
500 }
501 final String formatField = sb.toString();
502 return new StrategyAndWidth(new CopyQuotedStrategy(formatField), formatField.length());
503 }
504 }
505
506
507
508
509 static class TimeZoneStrategy extends PatternStrategy {
510
511 private static final class TzInfo {
512 final TimeZone zone;
513 final int dstOffset;
514
515 TzInfo(final TimeZone tz, final boolean useDst) {
516 zone = tz;
517 dstOffset = useDst ? tz.getDSTSavings() : 0;
518 }
519
520 @Override
521 public String toString() {
522 return "TzInfo [zone=" + zone + ", dstOffset=" + dstOffset + "]";
523 }
524 }
525
526 private static final String RFC_822_TIME_ZONE = "[+-](?:2[0-3]|[01]\\d)[0-5]\\d";
527
528 private static final String GMT_OPTION = TimeZones.GMT_ID + "[+-]\\d{1,2}:\\d{2}";
529
530
531
532
533 private static final int ID = 0;
534
535
536
537
538
539
540
541
542
543
544
545
546
547 static boolean skipTimeZone(final String tzId) {
548 return tzId.equalsIgnoreCase(TimeZones.GMT_ID);
549 }
550
551 private final Locale locale;
552
553
554
555
556
557 private final Map<String, TzInfo> tzNames = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
558
559
560
561
562
563
564 TimeZoneStrategy(final Locale locale) {
565 this.locale = LocaleUtils.toLocale(locale);
566 final StringBuilder sb = new StringBuilder();
567 sb.append("((?iu)" + RFC_822_TIME_ZONE + "|" + GMT_OPTION);
568 final Set<String> sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE);
569
570
571 final String[][] zones = DateFormatSymbols.getInstance(locale).getZoneStrings();
572 for (final String[] zoneNames : zones) {
573
574 final String tzId = zoneNames[ID];
575 if (skipTimeZone(tzId)) {
576 continue;
577 }
578 final TimeZone tz = TimeZones.getTimeZone(tzId);
579
580
581 final TzInfo standard = new TzInfo(tz, false);
582 TzInfo tzInfo = standard;
583 for (int i = 1; i < zoneNames.length; ++i) {
584 switch (i) {
585 case 3:
586
587 tzInfo = new TzInfo(tz, true);
588 break;
589 case 5:
590 tzInfo = standard;
591 break;
592 default:
593 break;
594 }
595 final String zoneName = zoneNames[i];
596
597 if (zoneName != null && sorted.add(zoneName)) {
598 tzNames.put(zoneName, tzInfo);
599 }
600 }
601 }
602
603 for (final String tzId : TimeZones.SORTED_AVAILABLE_IDS) {
604 if (skipTimeZone(tzId)) {
605 continue;
606 }
607 final TimeZone tz = TimeZones.getTimeZone(tzId);
608 final String zoneName = tz.getDisplayName(locale);
609 if (sorted.add(zoneName)) {
610 tzNames.put(zoneName, new TzInfo(tz, tz.observesDaylightTime()));
611 }
612 }
613
614
615 sorted.forEach(zoneName -> simpleQuote(sb.append('|'), zoneName));
616 sb.append(")");
617 createPattern(sb);
618 }
619
620
621
622
623 @Override
624 void setCalendar(final FastDateParser parser, final Calendar calendar, final String timeZone) {
625 final TimeZone tz = FastTimeZone.getGmtTimeZone(timeZone);
626 if (tz != null) {
627 calendar.setTimeZone(tz);
628 } else {
629 TzInfo tzInfo = tzNames.get(timeZone);
630 if (tzInfo == null) {
631
632 tzInfo = tzNames.get(timeZone + '.');
633 if (tzInfo == null) {
634
635
636
637 throw new IllegalArgumentException(
638 String.format("Can't find time zone '%s' (%d chars)", timeZone, timeZone.length()));
639 }
640 }
641 calendar.set(Calendar.DST_OFFSET, tzInfo.dstOffset);
642 calendar.set(Calendar.ZONE_OFFSET, tzInfo.zone.getRawOffset());
643 }
644 }
645
646
647
648
649
650
651 @Override
652 public String toString() {
653 return "TimeZoneStrategy [locale=" + locale + ", tzNames=" + tzNames + ", pattern=" + pattern + "]";
654 }
655
656 }
657
658
659
660
661
662
663
664
665 private static final class WeekDateRecorder extends GregorianCalendar {
666
667 private static final long serialVersionUID = 1L;
668
669
670 private final Calendar target;
671
672 private transient int weekYearValue;
673 private transient boolean weekYearSet;
674 private transient int weekOfYearValue;
675 private transient boolean weekOfYearSet;
676 private transient int dayOfWeekValue;
677 private transient boolean dayOfWeekSet;
678
679 WeekDateRecorder(final Calendar target) {
680 this.target = target;
681 }
682
683
684
685
686
687
688
689
690 void applyWeekDate() {
691 if (weekYearSet) {
692 target.setWeekDate(weekYearValue, weekOfYearSet ? weekOfYearValue : 1, dayOfWeekSet ? dayOfWeekValue : target.getFirstDayOfWeek());
693 }
694 }
695
696 @Override
697 public void set(final int field, final int value) {
698 if (target == null) {
699
700 super.set(field, value);
701 return;
702 }
703 switch (field) {
704 case Calendar.YEAR:
705 weekYearValue = value;
706 weekYearSet = true;
707 break;
708 case Calendar.WEEK_OF_YEAR:
709 weekOfYearValue = value;
710 weekOfYearSet = true;
711 break;
712 case Calendar.DAY_OF_WEEK:
713 dayOfWeekValue = value;
714 dayOfWeekSet = true;
715 break;
716 default:
717 break;
718 }
719 target.set(field, value);
720 }
721
722 @Override
723 public void setTimeZone(final TimeZone zone) {
724 if (target == null) {
725
726 super.setTimeZone(zone);
727 return;
728 }
729 target.setTimeZone(zone);
730 }
731 }
732
733
734
735
736
737
738 private static final long serialVersionUID = 3L;
739
740 static final Locale JAPANESE_IMPERIAL = new Locale("ja", "JP", "JP");
741
742
743
744
745
746
747
748 private static final Comparator<String> LONGER_FIRST_LOWERCASE = Comparator.reverseOrder();
749
750 @SuppressWarnings("unchecked")
751 private static final ConcurrentMap<Locale, Strategy>[] CACHES = new ConcurrentMap[Calendar.FIELD_COUNT];
752
753 private static final Strategy ABBREVIATED_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR) {
754
755
756
757
758 @Override
759 int modify(final FastDateParser parser, final int iValue) {
760 return iValue < 100 ? parser.adjustYear(iValue) : iValue;
761 }
762 };
763
764 private static final Strategy NUMBER_MONTH_STRATEGY = new NumberStrategy(Calendar.MONTH) {
765 @Override
766 int modify(final FastDateParser parser, final int iValue) {
767 return iValue - 1;
768 }
769 };
770
771 private static final Strategy LITERAL_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR);
772
773 private static final Strategy WEEK_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_YEAR);
774
775 private static final Strategy WEEK_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_MONTH);
776
777 private static final Strategy DAY_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.DAY_OF_YEAR);
778
779 private static final Strategy DAY_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_MONTH);
780
781 private static final Strategy DAY_OF_WEEK_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK) {
782 @Override
783 int modify(final FastDateParser parser, final int iValue) {
784 return iValue == 7 ? Calendar.SUNDAY : iValue + 1;
785 }
786 };
787
788 private static final Strategy DAY_OF_WEEK_IN_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK_IN_MONTH);
789
790 private static final Strategy HOUR_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY);
791
792 private static final Strategy HOUR24_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY) {
793 @Override
794 int modify(final FastDateParser parser, final int iValue) {
795 return iValue == 24 ? 0 : iValue;
796 }
797 };
798
799 private static final Strategy HOUR12_STRATEGY = new NumberStrategy(Calendar.HOUR) {
800 @Override
801 int modify(final FastDateParser parser, final int iValue) {
802 return iValue == 12 ? 0 : iValue;
803 }
804 };
805
806 private static final Strategy HOUR_STRATEGY = new NumberStrategy(Calendar.HOUR);
807
808 private static final Strategy MINUTE_STRATEGY = new NumberStrategy(Calendar.MINUTE);
809
810 private static final Strategy SECOND_STRATEGY = new NumberStrategy(Calendar.SECOND);
811
812 private static final Strategy MILLISECOND_STRATEGY = new NumberStrategy(Calendar.MILLISECOND);
813
814
815
816
817
818
819
820
821
822
823 private static Map<String, Integer> appendDisplayNames(final Calendar calendar, final Locale locale, final int field, final StringBuilder regex) {
824 Objects.requireNonNull(calendar, "calendar");
825 final Map<String, Integer> values = new HashMap<>();
826 final Locale actualLocale = LocaleUtils.toLocale(locale);
827 final Map<String, Integer> displayNames = calendar.getDisplayNames(field, Calendar.ALL_STYLES, actualLocale);
828 final TreeSet<String> sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE);
829 displayNames.forEach((k, v) -> {
830 final String keyLc = k.toLowerCase(actualLocale);
831 if (sorted.add(keyLc)) {
832 values.put(keyLc, v);
833 }
834 });
835 sorted.forEach(symbol -> simpleQuote(regex, symbol).append('|'));
836 return values;
837 }
838
839
840
841
842 static void clear() {
843 Stream.of(CACHES).filter(Objects::nonNull).forEach(ConcurrentMap::clear);
844 }
845
846
847
848
849
850
851
852 private static ConcurrentMap<Locale, Strategy> getCache(final int field) {
853 synchronized (CACHES) {
854 if (CACHES[field] == null) {
855 CACHES[field] = new ConcurrentHashMap<>(3);
856 }
857 return CACHES[field];
858 }
859 }
860
861 private static StringBuilder simpleQuote(final StringBuilder sb, final String value) {
862 for (int i = 0; i < value.length(); ++i) {
863 final char c = value.charAt(i);
864 switch (c) {
865 case '\\':
866 case '^':
867 case '$':
868 case '.':
869 case '|':
870 case '?':
871 case '*':
872 case '+':
873 case '(':
874 case ')':
875 case '[':
876 case '{':
877 sb.append('\\');
878
879 default:
880 sb.append(c);
881 }
882 }
883 if (sb.charAt(sb.length() - 1) == '.') {
884
885 sb.append('?');
886 }
887 return sb;
888 }
889
890
891 private final String pattern;
892
893
894 private final TimeZone timeZone;
895
896
897 private final Locale locale;
898
899
900
901
902 private final int century;
903
904
905
906
907 private final int startYear;
908
909
910 private transient List<StrategyAndWidth> patterns;
911
912
913
914
915
916 private transient volatile boolean weekYear;
917
918
919
920
921
922
923
924
925
926
927
928 protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale) {
929 this(pattern, timeZone, locale, null);
930 }
931
932
933
934
935
936
937
938
939
940
941 protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale, final Date centuryStart) {
942 this.pattern = Objects.requireNonNull(pattern, "pattern");
943
944 this.timeZone = (TimeZone) Objects.requireNonNull(timeZone, "timeZone").clone();
945 this.locale = LocaleUtils.toLocale(locale);
946 final Calendar definingCalendar = Calendar.getInstance(timeZone, this.locale);
947 final int centuryStartYear;
948 if (centuryStart != null) {
949 definingCalendar.setTime(centuryStart);
950 centuryStartYear = definingCalendar.get(Calendar.YEAR);
951 } else if (this.locale.equals(JAPANESE_IMPERIAL)) {
952 centuryStartYear = 0;
953 } else {
954
955 definingCalendar.setTime(new Date());
956 centuryStartYear = definingCalendar.get(Calendar.YEAR) - 80;
957 }
958 century = centuryStartYear / 100 * 100;
959 startYear = centuryStartYear - century;
960 init(definingCalendar);
961 }
962
963
964
965
966
967
968
969 private int adjustYear(final int twoDigitYear) {
970 final int trial = century + twoDigitYear;
971 return twoDigitYear >= startYear ? trial : trial + 100;
972 }
973
974 private boolean checkLength(final String source, final ParsePosition pos) {
975 final int startIndex = pos.getIndex();
976 if (startIndex > source.length()) {
977 pos.setErrorIndex(startIndex);
978 return false;
979 }
980 return true;
981 }
982
983
984
985
986
987
988
989 @Override
990 public boolean equals(final Object obj) {
991 if (!(obj instanceof FastDateParser)) {
992 return false;
993 }
994 final FastDateParser other = (FastDateParser) obj;
995 return pattern.equals(other.pattern) && timeZone.equals(other.timeZone) && locale.equals(other.locale);
996 }
997
998
999
1000
1001
1002
1003 @Override
1004 public Locale getLocale() {
1005 return locale;
1006 }
1007
1008
1009
1010
1011
1012
1013
1014
1015 private Strategy getLocaleSpecificStrategy(final int field, final Calendar definingCalendar) {
1016 return getCache(field).computeIfAbsent(locale,
1017 k -> field == Calendar.ZONE_OFFSET ? new TimeZoneStrategy(locale) : new CaseInsensitiveTextStrategy(field, definingCalendar, locale));
1018 }
1019
1020
1021
1022
1023
1024
1025 @Override
1026 public String getPattern() {
1027 return pattern;
1028 }
1029
1030 List<StrategyAndWidth> getPatterns() {
1031 return patterns;
1032 }
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042 private Strategy getStrategy(final char f, final int width, final Calendar definingCalendar) {
1043 switch (f) {
1044 case 'D':
1045 return DAY_OF_YEAR_STRATEGY;
1046 case 'E':
1047 return getLocaleSpecificStrategy(Calendar.DAY_OF_WEEK, definingCalendar);
1048 case 'F':
1049 return DAY_OF_WEEK_IN_MONTH_STRATEGY;
1050 case 'G':
1051 return getLocaleSpecificStrategy(Calendar.ERA, definingCalendar);
1052 case 'H':
1053 return HOUR_OF_DAY_STRATEGY;
1054 case 'K':
1055 return HOUR_STRATEGY;
1056 case 'M':
1057 case 'L':
1058 return width >= 3 ? getLocaleSpecificStrategy(Calendar.MONTH, definingCalendar) : NUMBER_MONTH_STRATEGY;
1059 case 'S':
1060 return MILLISECOND_STRATEGY;
1061 case 'W':
1062 return WEEK_OF_MONTH_STRATEGY;
1063 case 'a':
1064 return getLocaleSpecificStrategy(Calendar.AM_PM, definingCalendar);
1065 case 'd':
1066 return DAY_OF_MONTH_STRATEGY;
1067 case 'h':
1068 return HOUR12_STRATEGY;
1069 case 'k':
1070 return HOUR24_OF_DAY_STRATEGY;
1071 case 'm':
1072 return MINUTE_STRATEGY;
1073 case 's':
1074 return SECOND_STRATEGY;
1075 case 'u':
1076 return DAY_OF_WEEK_STRATEGY;
1077 case 'w':
1078 return WEEK_OF_YEAR_STRATEGY;
1079 case 'y':
1080 return width > 2 ? LITERAL_YEAR_STRATEGY : ABBREVIATED_YEAR_STRATEGY;
1081 case 'Y':
1082
1083
1084
1085
1086
1087
1088
1089 weekYear = true;
1090 return width > 2 ? LITERAL_YEAR_STRATEGY : ABBREVIATED_YEAR_STRATEGY;
1091 case 'X':
1092 return ISO8601TimeZoneStrategy.getStrategy(width);
1093 case 'Z':
1094 if (width == 2) {
1095 return ISO8601TimeZoneStrategy.ISO_8601_3_STRATEGY;
1096 }
1097
1098 case 'z':
1099 return getLocaleSpecificStrategy(Calendar.ZONE_OFFSET, definingCalendar);
1100 default:
1101 throw new IllegalArgumentException("Format '" + f + "' not supported");
1102 }
1103 }
1104
1105
1106
1107
1108
1109
1110 @Override
1111 public TimeZone getTimeZone() {
1112 return (TimeZone) timeZone.clone();
1113 }
1114
1115
1116
1117
1118
1119
1120 @Override
1121 public int hashCode() {
1122 return pattern.hashCode() + 13 * (timeZone.hashCode() + 13 * locale.hashCode());
1123 }
1124
1125
1126
1127
1128
1129
1130 private void init(final Calendar definingCalendar) {
1131 patterns = new ArrayList<>();
1132
1133 final StrategyParser strategyParser = new StrategyParser(definingCalendar);
1134 for (;;) {
1135 final StrategyAndWidth field = strategyParser.getNextStrategy();
1136 if (field == null) {
1137 break;
1138 }
1139 patterns.add(field);
1140 }
1141 }
1142
1143
1144
1145
1146
1147
1148 @Override
1149 public Date parse(final String source) throws ParseException {
1150 final ParsePosition pp = new ParsePosition(0);
1151 final Date date = parse(source, pp);
1152 if (date == null) {
1153
1154 final int errorIndex = pp.getErrorIndex();
1155 final String msg = String.format("Unparseable date: '%s', parse position = %s", source, pp);
1156 if (locale.equals(JAPANESE_IMPERIAL)) {
1157 throw new ParseException(String.format("%s; the %s locale does not support dates before 1868-01-01.", msg, locale), errorIndex);
1158 }
1159 throw new ParseException(msg, errorIndex);
1160 }
1161 return date;
1162 }
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174 @Override
1175 public Date parse(final String source, final ParsePosition pos) {
1176 if (!checkLength(source, pos)) {
1177 return null;
1178 }
1179
1180 final Calendar cal = Calendar.getInstance(timeZone, locale);
1181 cal.clear();
1182 return parse(source, pos, cal) ? cal.getTime() : null;
1183 }
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196 @Override
1197 public boolean parse(final String source, final ParsePosition pos, final Calendar calendar) {
1198 if (!checkLength(source, pos)) {
1199 return false;
1200 }
1201 final WeekDateRecorder recorder = weekYear && calendar.isWeekDateSupported() ? new WeekDateRecorder(calendar) : null;
1202 final Calendar sink = recorder != null ? recorder : calendar;
1203 final ListIterator<StrategyAndWidth> lt = patterns.listIterator();
1204 while (lt.hasNext()) {
1205 final StrategyAndWidth strategyAndWidth = lt.next();
1206 final int maxWidth = strategyAndWidth.getMaxWidth(lt);
1207 if (!strategyAndWidth.strategy.parse(this, sink, source, pos, maxWidth)) {
1208 return false;
1209 }
1210 }
1211 if (recorder != null) {
1212 recorder.applyWeekDate();
1213 }
1214 return true;
1215 }
1216
1217
1218
1219
1220
1221
1222 @Override
1223 public Object parseObject(final String source) throws ParseException {
1224 return parse(source);
1225 }
1226
1227
1228
1229
1230
1231
1232 @Override
1233 public Object parseObject(final String source, final ParsePosition pos) {
1234 return parse(source, pos);
1235 }
1236
1237
1238
1239
1240
1241
1242
1243
1244 private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
1245 in.defaultReadObject();
1246 SerializationUtils.requireNonNull(pattern, "pattern null");
1247 SerializationUtils.requireNonNull(timeZone, "timeZone null");
1248 init(Calendar.getInstance(timeZone, locale));
1249 }
1250
1251
1252
1253
1254
1255
1256 @Override
1257 public String toString() {
1258 return "FastDateParser[" + pattern + ", " + locale + ", " + timeZone.getID() + "]";
1259 }
1260
1261
1262
1263
1264
1265
1266
1267
1268 public String toStringAll() {
1269 return "FastDateParser [pattern=" + pattern + ", timeZone=" + timeZone + ", locale=" + locale + ", century=" + century + ", startYear=" + startYear
1270 + ", patterns=" + StringUtils.join(patterns, ", " + System.lineSeparator() + "\t") + "]";
1271 }
1272 }