1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.logging.log4j.core.util.datetime;
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.util.ArrayList;
26 import java.util.Calendar;
27 import java.util.Comparator;
28 import java.util.Date;
29 import java.util.HashMap;
30 import java.util.List;
31 import java.util.ListIterator;
32 import java.util.Locale;
33 import java.util.Map;
34 import java.util.Set;
35 import java.util.TimeZone;
36 import java.util.TreeSet;
37 import java.util.concurrent.ConcurrentHashMap;
38 import java.util.concurrent.ConcurrentMap;
39 import java.util.regex.Matcher;
40 import java.util.regex.Pattern;
41
42
43
44
45
46
47
48
49
50
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 public class FastDateParser implements DateParser, Serializable {
78
79
80
81
82
83
84 private static final long serialVersionUID = 3L;
85
86 static final Locale JAPANESE_IMPERIAL = new Locale("ja","JP","JP");
87
88
89 private final String pattern;
90 private final TimeZone timeZone;
91 private final Locale locale;
92 private final int century;
93 private final int startYear;
94
95
96 private transient List<StrategyAndWidth> patterns;
97
98
99
100
101 private static final Comparator<String> LONGER_FIRST_LOWERCASE = new Comparator<String>() {
102 @Override
103 public int compare(final String left, final String right) {
104 return right.compareTo(left);
105 }
106 };
107
108
109
110
111
112
113
114
115
116
117
118
119 protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale) {
120 this(pattern, timeZone, locale, null);
121 }
122
123
124
125
126
127
128
129
130
131
132
133
134 protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale, final Date centuryStart) {
135 this.pattern = pattern;
136 this.timeZone = timeZone;
137 this.locale = locale;
138
139 final Calendar definingCalendar = Calendar.getInstance(timeZone, locale);
140
141 int centuryStartYear;
142 if(centuryStart!=null) {
143 definingCalendar.setTime(centuryStart);
144 centuryStartYear= definingCalendar.get(Calendar.YEAR);
145 }
146 else if(locale.equals(JAPANESE_IMPERIAL)) {
147 centuryStartYear= 0;
148 }
149 else {
150
151 definingCalendar.setTime(new Date());
152 centuryStartYear= definingCalendar.get(Calendar.YEAR)-80;
153 }
154 century= centuryStartYear / 100 * 100;
155 startYear= centuryStartYear - century;
156
157 init(definingCalendar);
158 }
159
160
161
162
163
164
165
166 private void init(final Calendar definingCalendar) {
167 patterns = new ArrayList<>();
168
169 final StrategyParser fm = new StrategyParser(definingCalendar);
170 for(;;) {
171 final StrategyAndWidth field = fm.getNextStrategy();
172 if(field==null) {
173 break;
174 }
175 patterns.add(field);
176 }
177 }
178
179
180
181
182
183
184
185 private static class StrategyAndWidth {
186 final Strategy strategy;
187 final int width;
188
189 StrategyAndWidth(final Strategy strategy, final int width) {
190 this.strategy = strategy;
191 this.width = width;
192 }
193
194 int getMaxWidth(final ListIterator<StrategyAndWidth> lt) {
195 if(!strategy.isNumber() || !lt.hasNext()) {
196 return 0;
197 }
198 final Strategy nextStrategy = lt.next().strategy;
199 lt.previous();
200 return nextStrategy.isNumber() ?width :0;
201 }
202 }
203
204
205
206
207 private class StrategyParser {
208 final private Calendar definingCalendar;
209 private int currentIdx;
210
211 StrategyParser(final Calendar definingCalendar) {
212 this.definingCalendar = definingCalendar;
213 }
214
215 StrategyAndWidth getNextStrategy() {
216 if (currentIdx >= pattern.length()) {
217 return null;
218 }
219
220 final char c = pattern.charAt(currentIdx);
221 if (isFormatLetter(c)) {
222 return letterPattern(c);
223 }
224 return literal();
225 }
226
227 private StrategyAndWidth letterPattern(final char c) {
228 final int begin = currentIdx;
229 while (++currentIdx < pattern.length()) {
230 if (pattern.charAt(currentIdx) != c) {
231 break;
232 }
233 }
234
235 final int width = currentIdx - begin;
236 return new StrategyAndWidth(getStrategy(c, width, definingCalendar), width);
237 }
238
239 private StrategyAndWidth literal() {
240 boolean activeQuote = false;
241
242 final StringBuilder sb = new StringBuilder();
243 while (currentIdx < pattern.length()) {
244 final char c = pattern.charAt(currentIdx);
245 if (!activeQuote && isFormatLetter(c)) {
246 break;
247 } else if (c == '\'' && (++currentIdx == pattern.length() || pattern.charAt(currentIdx) != '\'')) {
248 activeQuote = !activeQuote;
249 continue;
250 }
251 ++currentIdx;
252 sb.append(c);
253 }
254
255 if (activeQuote) {
256 throw new IllegalArgumentException("Unterminated quote");
257 }
258
259 final String formatField = sb.toString();
260 return new StrategyAndWidth(new CopyQuotedStrategy(formatField), formatField.length());
261 }
262 }
263
264 private static boolean isFormatLetter(final char c) {
265 return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z';
266 }
267
268
269
270
271
272
273 @Override
274 public String getPattern() {
275 return pattern;
276 }
277
278
279
280
281 @Override
282 public TimeZone getTimeZone() {
283 return timeZone;
284 }
285
286
287
288
289 @Override
290 public Locale getLocale() {
291 return locale;
292 }
293
294
295
296
297
298
299
300
301
302
303 @Override
304 public boolean equals(final Object obj) {
305 if (!(obj instanceof FastDateParser)) {
306 return false;
307 }
308 final FastDateParser other = (FastDateParser) obj;
309 return pattern.equals(other.pattern)
310 && timeZone.equals(other.timeZone)
311 && locale.equals(other.locale);
312 }
313
314
315
316
317
318
319 @Override
320 public int hashCode() {
321 return pattern.hashCode() + 13 * (timeZone.hashCode() + 13 * locale.hashCode());
322 }
323
324
325
326
327
328
329 @Override
330 public String toString() {
331 return "FastDateParser[" + pattern + "," + locale + "," + timeZone.getID() + "]";
332 }
333
334
335
336
337
338
339
340
341
342
343
344 private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
345 in.defaultReadObject();
346
347 final Calendar definingCalendar = Calendar.getInstance(timeZone, locale);
348 init(definingCalendar);
349 }
350
351
352
353
354 @Override
355 public Object parseObject(final String source) throws ParseException {
356 return parse(source);
357 }
358
359
360
361
362 @Override
363 public Date parse(final String source) throws ParseException {
364 final ParsePosition pp = new ParsePosition(0);
365 final Date date= parse(source, pp);
366 if (date == null) {
367
368 if (locale.equals(JAPANESE_IMPERIAL)) {
369 throw new ParseException(
370 "(The " +locale + " locale does not support dates before 1868 AD)\n" +
371 "Unparseable date: \""+source, pp.getErrorIndex());
372 }
373 throw new ParseException("Unparseable date: "+source, pp.getErrorIndex());
374 }
375 return date;
376 }
377
378
379
380
381 @Override
382 public Object parseObject(final String source, final ParsePosition pos) {
383 return parse(source, pos);
384 }
385
386
387
388
389
390
391
392
393
394
395
396
397
398 @Override
399 public Date parse(final String source, final ParsePosition pos) {
400
401 final Calendar cal= Calendar.getInstance(timeZone, locale);
402 cal.clear();
403
404 return parse(source, pos, cal) ? cal.getTime() : null;
405 }
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420 @Override
421 public boolean parse(final String source, final ParsePosition pos, final Calendar calendar) {
422 final ListIterator<StrategyAndWidth> lt = patterns.listIterator();
423 while (lt.hasNext()) {
424 final StrategyAndWidth strategyAndWidth = lt.next();
425 final int maxWidth = strategyAndWidth.getMaxWidth(lt);
426 if (!strategyAndWidth.strategy.parse(this, calendar, source, pos, maxWidth)) {
427 return false;
428 }
429 }
430 return true;
431 }
432
433
434
435
436 private static StringBuilder simpleQuote(final StringBuilder sb, final String value) {
437 for (int i = 0; i < value.length(); ++i) {
438 final char c = value.charAt(i);
439 switch (c) {
440 case '\\':
441 case '^':
442 case '$':
443 case '.':
444 case '|':
445 case '?':
446 case '*':
447 case '+':
448 case '(':
449 case ')':
450 case '[':
451 case '{':
452 sb.append('\\');
453 default:
454 sb.append(c);
455 }
456 }
457 return sb;
458 }
459
460
461
462
463
464
465
466
467
468 private static Map<String, Integer> appendDisplayNames(final Calendar cal, final Locale locale, final int field, final StringBuilder regex) {
469 final Map<String, Integer> values = new HashMap<>();
470
471 final Map<String, Integer> displayNames = cal.getDisplayNames(field, Calendar.ALL_STYLES, locale);
472 final TreeSet<String> sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE);
473 for (final Map.Entry<String, Integer> displayName : displayNames.entrySet()) {
474 final String key = displayName.getKey().toLowerCase(locale);
475 if (sorted.add(key)) {
476 values.put(key, displayName.getValue());
477 }
478 }
479 for (final String symbol : sorted) {
480 simpleQuote(regex, symbol).append('|');
481 }
482 return values;
483 }
484
485
486
487
488
489
490 private int adjustYear(final int twoDigitYear) {
491 final int trial = century + twoDigitYear;
492 return twoDigitYear >= startYear ? trial : trial + 100;
493 }
494
495
496
497
498 private static abstract class Strategy {
499
500
501
502
503
504
505 boolean isNumber() {
506 return false;
507 }
508
509 abstract boolean parse(FastDateParser parser, Calendar calendar, String source, ParsePosition pos, int maxWidth);
510 }
511
512
513
514
515 private static abstract class PatternStrategy extends Strategy {
516
517 private Pattern pattern;
518
519 void createPattern(final StringBuilder regex) {
520 createPattern(regex.toString());
521 }
522
523 void createPattern(final String regex) {
524 this.pattern = Pattern.compile(regex);
525 }
526
527
528
529
530
531
532
533 @Override
534 boolean isNumber() {
535 return false;
536 }
537
538 @Override
539 boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) {
540 final Matcher matcher = pattern.matcher(source.substring(pos.getIndex()));
541 if (!matcher.lookingAt()) {
542 pos.setErrorIndex(pos.getIndex());
543 return false;
544 }
545 pos.setIndex(pos.getIndex() + matcher.end(1));
546 setCalendar(parser, calendar, matcher.group(1));
547 return true;
548 }
549
550 abstract void setCalendar(FastDateParser parser, Calendar cal, String value);
551 }
552
553
554
555
556
557
558
559 private Strategy getStrategy(final char f, final int width, final Calendar definingCalendar) {
560 switch(f) {
561 default:
562 throw new IllegalArgumentException("Format '"+f+"' not supported");
563 case 'D':
564 return DAY_OF_YEAR_STRATEGY;
565 case 'E':
566 return getLocaleSpecificStrategy(Calendar.DAY_OF_WEEK, definingCalendar);
567 case 'F':
568 return DAY_OF_WEEK_IN_MONTH_STRATEGY;
569 case 'G':
570 return getLocaleSpecificStrategy(Calendar.ERA, definingCalendar);
571 case 'H':
572 return HOUR_OF_DAY_STRATEGY;
573 case 'K':
574 return HOUR_STRATEGY;
575 case 'M':
576 return width >= 3 ? getLocaleSpecificStrategy(Calendar.MONTH, definingCalendar) : NUMBER_MONTH_STRATEGY;
577 case 'S':
578 return MILLISECOND_STRATEGY;
579 case 'W':
580 return WEEK_OF_MONTH_STRATEGY;
581 case 'a':
582 return getLocaleSpecificStrategy(Calendar.AM_PM, definingCalendar);
583 case 'd':
584 return DAY_OF_MONTH_STRATEGY;
585 case 'h':
586 return HOUR12_STRATEGY;
587 case 'k':
588 return HOUR24_OF_DAY_STRATEGY;
589 case 'm':
590 return MINUTE_STRATEGY;
591 case 's':
592 return SECOND_STRATEGY;
593 case 'u':
594 return DAY_OF_WEEK_STRATEGY;
595 case 'w':
596 return WEEK_OF_YEAR_STRATEGY;
597 case 'y':
598 case 'Y':
599 return width > 2 ? LITERAL_YEAR_STRATEGY : ABBREVIATED_YEAR_STRATEGY;
600 case 'X':
601 return ISO8601TimeZoneStrategy.getStrategy(width);
602 case 'Z':
603 if (width==2) {
604 return ISO8601TimeZoneStrategy.ISO_8601_3_STRATEGY;
605 }
606
607 case 'z':
608 return getLocaleSpecificStrategy(Calendar.ZONE_OFFSET, definingCalendar);
609 }
610 }
611
612 @SuppressWarnings("unchecked")
613 private static final ConcurrentMap<Locale, Strategy>[] caches = new ConcurrentMap[Calendar.FIELD_COUNT];
614
615
616
617
618
619
620 private static ConcurrentMap<Locale, Strategy> getCache(final int field) {
621 synchronized (caches) {
622 if (caches[field] == null) {
623 caches[field] = new ConcurrentHashMap<>(3);
624 }
625 return caches[field];
626 }
627 }
628
629
630
631
632
633
634
635 private Strategy getLocaleSpecificStrategy(final int field, final Calendar definingCalendar) {
636 final ConcurrentMap<Locale, Strategy> cache = getCache(field);
637 Strategy strategy = cache.get(locale);
638 if (strategy == null) {
639 strategy = field == Calendar.ZONE_OFFSET
640 ? new TimeZoneStrategy(locale)
641 : new CaseInsensitiveTextStrategy(field, definingCalendar, locale);
642 final Strategy inCache = cache.putIfAbsent(locale, strategy);
643 if (inCache != null) {
644 return inCache;
645 }
646 }
647 return strategy;
648 }
649
650
651
652
653 private static class CopyQuotedStrategy extends Strategy {
654
655 final private String formatField;
656
657
658
659
660
661 CopyQuotedStrategy(final String formatField) {
662 this.formatField = formatField;
663 }
664
665
666
667
668 @Override
669 boolean isNumber() {
670 return false;
671 }
672
673 @Override
674 boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) {
675 for (int idx = 0; idx < formatField.length(); ++idx) {
676 final int sIdx = idx + pos.getIndex();
677 if (sIdx == source.length()) {
678 pos.setErrorIndex(sIdx);
679 return false;
680 }
681 if (formatField.charAt(idx) != source.charAt(sIdx)) {
682 pos.setErrorIndex(sIdx);
683 return false;
684 }
685 }
686 pos.setIndex(formatField.length() + pos.getIndex());
687 return true;
688 }
689 }
690
691
692
693
694 private static class CaseInsensitiveTextStrategy extends PatternStrategy {
695 private final int field;
696 final Locale locale;
697 private final Map<String, Integer> lKeyValues;
698
699
700
701
702
703
704
705 CaseInsensitiveTextStrategy(final int field, final Calendar definingCalendar, final Locale locale) {
706 this.field = field;
707 this.locale = locale;
708
709 final StringBuilder regex = new StringBuilder();
710 regex.append("((?iu)");
711 lKeyValues = appendDisplayNames(definingCalendar, locale, field, regex);
712 regex.setLength(regex.length()-1);
713 regex.append(")");
714 createPattern(regex);
715 }
716
717
718
719
720 @Override
721 void setCalendar(final FastDateParser parser, final Calendar cal, final String value) {
722 final Integer iVal = lKeyValues.get(value.toLowerCase(locale));
723 cal.set(field, iVal.intValue());
724 }
725 }
726
727
728
729
730
731 private static class NumberStrategy extends Strategy {
732 private final int field;
733
734
735
736
737
738 NumberStrategy(final int field) {
739 this.field= field;
740 }
741
742
743
744
745 @Override
746 boolean isNumber() {
747 return true;
748 }
749
750 @Override
751 boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) {
752 int idx = pos.getIndex();
753 int last = source.length();
754
755 if (maxWidth == 0) {
756
757 for (; idx < last; ++idx) {
758 final char c = source.charAt(idx);
759 if (!Character.isWhitespace(c)) {
760 break;
761 }
762 }
763 pos.setIndex(idx);
764 } else {
765 final int end = idx + maxWidth;
766 if (last > end) {
767 last = end;
768 }
769 }
770
771 for (; idx < last; ++idx) {
772 final char c = source.charAt(idx);
773 if (!Character.isDigit(c)) {
774 break;
775 }
776 }
777
778 if (pos.getIndex() == idx) {
779 pos.setErrorIndex(idx);
780 return false;
781 }
782
783 final int value = Integer.parseInt(source.substring(pos.getIndex(), idx));
784 pos.setIndex(idx);
785
786 calendar.set(field, modify(parser, value));
787 return true;
788 }
789
790
791
792
793
794
795
796 int modify(final FastDateParser parser, final int iValue) {
797 return iValue;
798 }
799
800 }
801
802 private static final Strategy ABBREVIATED_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR) {
803
804
805
806 @Override
807 int modify(final FastDateParser parser, final int iValue) {
808 return iValue < 100 ? parser.adjustYear(iValue) : iValue;
809 }
810 };
811
812
813
814
815 static class TimeZoneStrategy extends PatternStrategy {
816 private static final String RFC_822_TIME_ZONE = "[+-]\\d{4}";
817 private static final String GMT_OPTION= "GMT[+-]\\d{1,2}:\\d{2}";
818
819 private final Locale locale;
820 private final Map<String, TzInfo> tzNames= new HashMap<>();
821
822 private static class TzInfo {
823 TimeZone zone;
824 int dstOffset;
825
826 TzInfo(final TimeZone tz, final boolean useDst) {
827 zone = tz;
828 dstOffset = useDst ?tz.getDSTSavings() :0;
829 }
830 }
831
832
833
834
835 private static final int ID = 0;
836
837
838
839
840
841 TimeZoneStrategy(final Locale locale) {
842 this.locale = locale;
843
844 final StringBuilder sb = new StringBuilder();
845 sb.append("((?iu)" + RFC_822_TIME_ZONE + "|" + GMT_OPTION );
846
847 final Set<String> sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE);
848
849 final String[][] zones = DateFormatSymbols.getInstance(locale).getZoneStrings();
850 for (final String[] zoneNames : zones) {
851
852 final String tzId = zoneNames[ID];
853 if (tzId.equalsIgnoreCase("GMT")) {
854 continue;
855 }
856 final TimeZone tz = TimeZone.getTimeZone(tzId);
857
858
859 final TzInfo standard = new TzInfo(tz, false);
860 TzInfo tzInfo = standard;
861 for (int i = 1; i < zoneNames.length; ++i) {
862 switch (i) {
863 case 3:
864
865 tzInfo = new TzInfo(tz, true);
866 break;
867 case 5:
868 tzInfo = standard;
869 break;
870 }
871 if (zoneNames[i] != null) {
872 final String key = zoneNames[i].toLowerCase(locale);
873
874
875 if (sorted.add(key)) {
876 tzNames.put(key, tzInfo);
877 }
878 }
879 }
880 }
881
882
883 for (final String zoneName : sorted) {
884 simpleQuote(sb.append('|'), zoneName);
885 }
886 sb.append(")");
887 createPattern(sb);
888 }
889
890
891
892
893 @Override
894 void setCalendar(final FastDateParser parser, final Calendar cal, final String value) {
895 if (value.charAt(0) == '+' || value.charAt(0) == '-') {
896 final TimeZone tz = TimeZone.getTimeZone("GMT" + value);
897 cal.setTimeZone(tz);
898 } else if (value.regionMatches(true, 0, "GMT", 0, 3)) {
899 final TimeZone tz = TimeZone.getTimeZone(value.toUpperCase());
900 cal.setTimeZone(tz);
901 } else {
902 final TzInfo tzInfo = tzNames.get(value.toLowerCase(locale));
903 cal.set(Calendar.DST_OFFSET, tzInfo.dstOffset);
904 cal.set(Calendar.ZONE_OFFSET, tzInfo.zone.getRawOffset());
905 }
906 }
907 }
908
909 private static class ISO8601TimeZoneStrategy extends PatternStrategy {
910
911
912
913
914
915
916 ISO8601TimeZoneStrategy(final String pattern) {
917 createPattern(pattern);
918 }
919
920
921
922
923 @Override
924 void setCalendar(final FastDateParser parser, final Calendar cal, final String value) {
925 if (value.equals("Z")) {
926 cal.setTimeZone(TimeZone.getTimeZone("UTC"));
927 } else {
928 cal.setTimeZone(TimeZone.getTimeZone("GMT" + value));
929 }
930 }
931
932 private static final Strategy ISO_8601_1_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}))");
933 private static final Strategy ISO_8601_2_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}\\d{2}))");
934 private static final Strategy ISO_8601_3_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}(?::)\\d{2}))");
935
936
937
938
939
940
941
942
943 static Strategy getStrategy(final int tokenLen) {
944 switch(tokenLen) {
945 case 1:
946 return ISO_8601_1_STRATEGY;
947 case 2:
948 return ISO_8601_2_STRATEGY;
949 case 3:
950 return ISO_8601_3_STRATEGY;
951 default:
952 throw new IllegalArgumentException("invalid number of X");
953 }
954 }
955 }
956
957 private static final Strategy NUMBER_MONTH_STRATEGY = new NumberStrategy(Calendar.MONTH) {
958 @Override
959 int modify(final FastDateParser parser, final int iValue) {
960 return iValue-1;
961 }
962 };
963 private static final Strategy LITERAL_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR);
964 private static final Strategy WEEK_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_YEAR);
965 private static final Strategy WEEK_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_MONTH);
966 private static final Strategy DAY_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.DAY_OF_YEAR);
967 private static final Strategy DAY_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_MONTH);
968 private static final Strategy DAY_OF_WEEK_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK) {
969 @Override
970 int modify(final FastDateParser parser, final int iValue) {
971 return iValue != 7 ? iValue + 1 : Calendar.SUNDAY;
972 }
973 };
974 private static final Strategy DAY_OF_WEEK_IN_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK_IN_MONTH);
975 private static final Strategy HOUR_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY);
976 private static final Strategy HOUR24_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY) {
977 @Override
978 int modify(final FastDateParser parser, final int iValue) {
979 return iValue == 24 ? 0 : iValue;
980 }
981 };
982 private static final Strategy HOUR12_STRATEGY = new NumberStrategy(Calendar.HOUR) {
983 @Override
984 int modify(final FastDateParser parser, final int iValue) {
985 return iValue == 12 ? 0 : iValue;
986 }
987 };
988 private static final Strategy HOUR_STRATEGY = new NumberStrategy(Calendar.HOUR);
989 private static final Strategy MINUTE_STRATEGY = new NumberStrategy(Calendar.MINUTE);
990 private static final Strategy SECOND_STRATEGY = new NumberStrategy(Calendar.SECOND);
991 private static final Strategy MILLISECOND_STRATEGY = new NumberStrategy(Calendar.MILLISECOND);
992 }