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 static org.apache.commons.lang3.LangAssertions.assertIllegalArgumentException;
20 import static org.junit.jupiter.api.Assertions.assertEquals;
21 import static org.junit.jupiter.api.Assertions.assertFalse;
22 import static org.junit.jupiter.api.Assertions.assertNotEquals;
23 import static org.junit.jupiter.api.Assertions.assertNotNull;
24 import static org.junit.jupiter.api.Assertions.assertNull;
25 import static org.junit.jupiter.api.Assertions.assertThrows;
26 import static org.junit.jupiter.api.Assertions.assertTrue;
27 import static org.junit.jupiter.api.Assertions.fail;
28
29 import java.io.Serializable;
30 import java.lang.reflect.Constructor;
31 import java.text.ParseException;
32 import java.text.ParsePosition;
33 import java.text.SimpleDateFormat;
34 import java.time.Instant;
35 import java.util.Calendar;
36 import java.util.Date;
37 import java.util.GregorianCalendar;
38 import java.util.HashMap;
39 import java.util.List;
40 import java.util.Locale;
41 import java.util.Map;
42 import java.util.TimeZone;
43 import java.util.stream.Stream;
44
45 import org.apache.commons.lang3.AbstractLangTest;
46 import org.apache.commons.lang3.LocaleProblems;
47 import org.apache.commons.lang3.LocaleUtils;
48 import org.apache.commons.lang3.SerializationUtils;
49 import org.apache.commons.lang3.SystemUtils;
50 import org.apache.commons.lang3.function.TriFunction;
51 import org.apache.commons.lang3.reflect.FieldUtils;
52 import org.junit.jupiter.api.AfterEach;
53 import org.junit.jupiter.api.BeforeEach;
54 import org.junit.jupiter.api.Test;
55 import org.junit.jupiter.params.ParameterizedTest;
56 import org.junit.jupiter.params.provider.Arguments;
57 import org.junit.jupiter.params.provider.CsvSource;
58 import org.junit.jupiter.params.provider.MethodSource;
59 import org.junitpioneer.jupiter.DefaultLocale;
60 import org.junitpioneer.jupiter.DefaultTimeZone;
61 import org.junitpioneer.jupiter.ReadsDefaultLocale;
62 import org.junitpioneer.jupiter.ReadsDefaultTimeZone;
63 import org.junitpioneer.jupiter.cartesian.ArgumentSets;
64 import org.junitpioneer.jupiter.cartesian.CartesianTest;
65 import org.opentest4j.AssertionFailedError;
66
67
68
69
70 @DefaultLocale(language = "en")
71 @DefaultTimeZone(TimeZones.GMT_ID)
72 @ReadsDefaultLocale
73 @ReadsDefaultTimeZone
74 class FastDateParserTest extends AbstractLangTest {
75
76 private enum Expected1806 {
77
78
79 India(INDIA, "+05", "+0530", "+05:30", true),
80 Greenwich(TimeZones.GMT, "Z", "Z", "Z", false),
81 NewYork(NEW_YORK, "-05", "-0500", "-05:00", false);
82
83
84 final TimeZone zone;
85 final String one;
86 final String two;
87 final String three;
88 final long offset;
89
90 Expected1806(final TimeZone zone, final String one, final String two, final String three,
91 final boolean hasHalfHourOffset) {
92 this.zone = zone;
93 this.one = one;
94 this.two = two;
95 this.three = three;
96 this.offset = hasHalfHourOffset ? 30 * 60 * 1000 : 0;
97 }
98 }
99
100 static final String DATE_PARSER_PARAMETERS = "dateParserParameters";
101
102 static final String SHORT_FORMAT_NOERA = "y/M/d/h/a/m/s/E";
103
104 static final String LONG_FORMAT_NOERA = "yyyy/MMMM/dddd/hhhh/mmmm/ss/aaaa/EEEE";
105 static final String SHORT_FORMAT = "G/" + SHORT_FORMAT_NOERA;
106 static final String LONG_FORMAT = "GGGG/" + LONG_FORMAT_NOERA;
107
108 private static final String yMdHmsSZ = "yyyy-MM-dd'T'HH:mm:ss.SSS Z";
109 private static final String DMY_DOT = "dd.MM.yyyy";
110 private static final String YMD_SLASH = "yyyy/MM/dd";
111 private static final String MDY_DASH = "MM-DD-yyyy";
112 private static final String MDY_SLASH = "MM/DD/yyyy";
113
114 private static final TimeZone REYKJAVIK = TimeZones.getTimeZone("Atlantic/Reykjavik");
115 private static final TimeZone NEW_YORK = TimeZones.getTimeZone("America/New_York");
116 private static final TimeZone INDIA = TimeZones.getTimeZone("Asia/Calcutta");
117
118 private static final Locale SWEDEN = new Locale("sv", "SE");
119
120 private static void assertParseFailure(final DateParser parser, final String source, final int errorIndex) {
121 final ParseException exception = assertThrows(ParseException.class, () -> parser.parse(source), source);
122 assertEquals(errorIndex, exception.getErrorOffset(), source);
123 for (final int startIndex : new int[] { 0, 2 }) {
124 final String input = startIndex == 0 ? source : "##" + source;
125 final ParsePosition datePosition = new ParsePosition(startIndex);
126 assertNull(parser.parse(input, datePosition), input);
127 assertEquals(startIndex + errorIndex, datePosition.getIndex(), input);
128 assertEquals(startIndex + errorIndex, datePosition.getErrorIndex(), input);
129 final ParsePosition calendarPosition = new ParsePosition(startIndex);
130 final Calendar calendar = Calendar.getInstance(TimeZones.GMT, Locale.US);
131 calendar.clear();
132 assertFalse(parser.parse(input, calendarPosition, calendar), input);
133 assertEquals(startIndex + errorIndex, calendarPosition.getIndex(), input);
134 assertEquals(startIndex + errorIndex, calendarPosition.getErrorIndex(), input);
135 }
136 }
137
138 static void checkParse(final Locale locale, final Calendar cal, final SimpleDateFormat simpleDateFormat,
139 final DateParser dateParser) {
140 final String formattedDate = simpleDateFormat.format(cal.getTime());
141 checkParse(locale, simpleDateFormat, dateParser, formattedDate, formattedDate);
142 checkParse(locale, simpleDateFormat, dateParser, formattedDate.toLowerCase(locale), formattedDate);
143 checkParse(locale, simpleDateFormat, dateParser, formattedDate.toUpperCase(locale), formattedDate);
144 }
145
146 static void checkParse(final Locale locale, final SimpleDateFormat simpleDateFormat, final DateParser dateParser,
147 final String formattedDate, final String originalFormattedDate) {
148 try {
149 final Date expectedTime = simpleDateFormat.parse(formattedDate);
150 final Date actualTime = dateParser.parse(formattedDate);
151 assertEquals(expectedTime, actualTime,
152 "locale: " + locale + ", formattedDate: '" + formattedDate + "', originalFormattedDate: '"
153 + originalFormattedDate + ", simpleDateFormat.pattern: '" + simpleDateFormat + "', Java: "
154 + SystemUtils.JAVA_RUNTIME_VERSION + "\n");
155 } catch (final Exception e) {
156 fail("locale: " + locale + ", formattedDate: '" + formattedDate + "', error : " + e + "\n", e);
157 }
158 }
159
160 static Stream<Arguments> dateParserParameters() {
161 return Stream.of(
162
163 Arguments.of((TriFunction<String, TimeZone, Locale, DateParser>) (format, timeZone, locale)
164 -> new FastDateParser(format, timeZone, locale, null)),
165 Arguments.of((TriFunction<String, TimeZone, Locale, DateParser>) FastDateFormat::getInstance)
166
167 );
168 }
169
170 private static Calendar initializeCalendar(final TimeZone timeZone) {
171 final Calendar cal = Calendar.getInstance(timeZone);
172 cal.set(Calendar.YEAR, 2001);
173 cal.set(Calendar.MONTH, 1);
174 cal.set(Calendar.DAY_OF_MONTH, 4);
175 cal.set(Calendar.HOUR_OF_DAY, 12);
176 cal.set(Calendar.MINUTE, 8);
177 cal.set(Calendar.SECOND, 56);
178 cal.set(Calendar.MILLISECOND, 235);
179 return cal;
180 }
181
182 static ArgumentSets testParsesFactory() {
183
184 return ArgumentSets
185 .argumentsForFirstParameter(LONG_FORMAT, SHORT_FORMAT)
186 .argumentsForNextParameter(LocaleUtils.availableLocaleList())
187 .argumentsForNextParameter(NEW_YORK, REYKJAVIK, TimeZones.GMT)
188 .argumentsForNextParameter(2003, 1940, 1868, 1867, 1, -1, -1940);
189
190 }
191
192 private final TriFunction<String, TimeZone, Locale, DateParser> dateParserProvider = (format, timeZone, locale) -> new FastDateParser(format, timeZone,
193 locale, null);
194
195 @BeforeEach
196 @AfterEach
197 void clear() {
198 AbstractFormatCache.clear();
199 FastDateFormat.clear();
200 FastDateParser.clear();
201 FastDatePrinter.clear();
202 }
203
204 private DateParser getDateInstance(final int dateStyle, final Locale locale) {
205 return getInstance(null, AbstractFormatCache.getPatternForStyle(Integer.valueOf(dateStyle), null, locale), TimeZone.getDefault(), Locale.getDefault());
206 }
207
208 private Calendar getEraStart(int year, final TimeZone zone, final Locale locale) {
209 final Calendar cal = Calendar.getInstance(zone, locale);
210 cal.clear();
211
212 if (locale.equals(FastDateParser.JAPANESE_IMPERIAL)) {
213 if (year < 1868) {
214 cal.set(Calendar.ERA, 0);
215 cal.set(Calendar.YEAR, 1868 - year);
216 }
217 } else {
218 if (year < 0) {
219 cal.set(Calendar.ERA, GregorianCalendar.BC);
220 year = -year;
221 }
222 cal.set(Calendar.YEAR, year / 100 * 100);
223 }
224 return cal;
225 }
226
227 DateParser getInstance(final String format) {
228 return getInstance(null, format, TimeZone.getDefault(), Locale.getDefault());
229 }
230
231 DateParser getInstance(final String format, final Locale locale) {
232 return getInstance(null, format, TimeZone.getDefault(), locale);
233 }
234
235 private DateParser getInstance(final String format, final TimeZone timeZone) {
236 return getInstance(null, format, timeZone, Locale.getDefault());
237 }
238
239
240
241
242
243
244
245
246
247
248 protected DateParser getInstance(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider,
249 final String format, final TimeZone timeZone, final Locale locale) {
250 return (dpProvider == null ? this.dateParserProvider : dpProvider).apply(format, timeZone, locale);
251 }
252
253 @ParameterizedTest
254 @MethodSource(DATE_PARSER_PARAMETERS)
255 void test_Equality_Hash(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider) {
256
257 final DateParser[] parsers = {
258 getInstance(dpProvider, yMdHmsSZ, NEW_YORK, Locale.US),
259 getInstance(dpProvider, DMY_DOT, NEW_YORK, Locale.US),
260 getInstance(dpProvider, YMD_SLASH, NEW_YORK, Locale.US),
261 getInstance(dpProvider, MDY_DASH, NEW_YORK, Locale.US),
262 getInstance(dpProvider, MDY_SLASH, NEW_YORK, Locale.US),
263 getInstance(dpProvider, MDY_SLASH, REYKJAVIK, Locale.US),
264 getInstance(dpProvider, MDY_SLASH, REYKJAVIK, SWEDEN)
265 };
266
267 final Map<DateParser, Integer> map = new HashMap<>();
268 int i = 0;
269 for (final DateParser parser : parsers) {
270 map.put(parser, Integer.valueOf(i++));
271 }
272 i = 0;
273 for (final DateParser parser : parsers) {
274 assertEquals(i++, map.get(parser).intValue());
275 }
276 }
277
278 @Test
279 @ReadsDefaultLocale
280 void test1806() throws ParseException {
281 final String formatStub = "yyyy-MM-dd'T'HH:mm:ss.SSS";
282 final String dateStub = "2001-02-04T12:08:56.235";
283 for (final Expected1806 trial : Expected1806.values()) {
284 final Calendar cal = initializeCalendar(trial.zone);
285 final String message = trial.zone.getDisplayName() + ";";
286 DateParser parser = getInstance(formatStub + "X", trial.zone);
287 assertEquals(cal.getTime().getTime(), parser.parse(dateStub + trial.one).getTime() - trial.offset, message + trial.one);
288 parser = getInstance(formatStub + "XX", trial.zone);
289 assertEquals(cal.getTime(), parser.parse(dateStub + trial.two), message + trial.two);
290 parser = getInstance(formatStub + "XXX", trial.zone);
291 assertEquals(cal.getTime(), parser.parse(dateStub + trial.three), message + trial.three);
292 }
293 }
294
295 @Test
296 void test1806Argument() {
297 assertIllegalArgumentException(() -> getInstance("XXXX"));
298 }
299
300 @ParameterizedTest
301 @MethodSource(DATE_PARSER_PARAMETERS)
302 void testAmPm(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider) throws ParseException {
303 final Calendar cal = Calendar.getInstance(NEW_YORK, Locale.US);
304 cal.clear();
305
306 final DateParser h = getInstance(dpProvider, "yyyy-MM-dd hh a mm:ss", NEW_YORK, Locale.US);
307 final DateParser K = getInstance(dpProvider, "yyyy-MM-dd KK a mm:ss", NEW_YORK, Locale.US);
308 final DateParser k = getInstance(dpProvider, "yyyy-MM-dd kk:mm:ss", NEW_YORK, Locale.US);
309 final DateParser H = getInstance(dpProvider, "yyyy-MM-dd HH:mm:ss", NEW_YORK, Locale.US);
310
311 cal.set(2010, Calendar.AUGUST, 1, 0, 33, 20);
312 assertEquals(cal.getTime(), h.parse("2010-08-01 12 AM 33:20"));
313 assertEquals(cal.getTime(), K.parse("2010-08-01 0 AM 33:20"));
314 assertEquals(cal.getTime(), k.parse("2010-08-01 00:33:20"));
315 assertEquals(cal.getTime(), H.parse("2010-08-01 00:33:20"));
316
317 cal.set(2010, Calendar.AUGUST, 1, 3, 33, 20);
318 assertEquals(cal.getTime(), h.parse("2010-08-01 3 AM 33:20"));
319 assertEquals(cal.getTime(), K.parse("2010-08-01 3 AM 33:20"));
320 assertEquals(cal.getTime(), k.parse("2010-08-01 03:33:20"));
321 assertEquals(cal.getTime(), H.parse("2010-08-01 03:33:20"));
322
323 cal.set(2010, Calendar.AUGUST, 1, 15, 33, 20);
324 assertEquals(cal.getTime(), h.parse("2010-08-01 3 PM 33:20"));
325 assertEquals(cal.getTime(), K.parse("2010-08-01 3 PM 33:20"));
326 assertEquals(cal.getTime(), k.parse("2010-08-01 15:33:20"));
327 assertEquals(cal.getTime(), H.parse("2010-08-01 15:33:20"));
328
329 cal.set(2010, Calendar.AUGUST, 1, 12, 33, 20);
330 assertEquals(cal.getTime(), h.parse("2010-08-01 12 PM 33:20"));
331 assertEquals(cal.getTime(), K.parse("2010-08-01 0 PM 33:20"));
332 assertEquals(cal.getTime(), k.parse("2010-08-01 12:33:20"));
333 assertEquals(cal.getTime(), H.parse("2010-08-01 12:33:20"));
334 }
335
336 @Test
337 void testDayNumberOfWeek() throws ParseException {
338 final DateParser parser = getInstance("u");
339 final Calendar calendar = Calendar.getInstance();
340
341 calendar.setTime(parser.parse("1"));
342 assertEquals(Calendar.MONDAY, calendar.get(Calendar.DAY_OF_WEEK));
343
344 calendar.setTime(parser.parse("6"));
345 assertEquals(Calendar.SATURDAY, calendar.get(Calendar.DAY_OF_WEEK));
346
347 calendar.setTime(parser.parse("7"));
348 assertEquals(Calendar.SUNDAY, calendar.get(Calendar.DAY_OF_WEEK));
349 }
350
351 @ParameterizedTest
352 @MethodSource(DATE_PARSER_PARAMETERS)
353 void testDayOf(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider) throws ParseException {
354 final Calendar cal = Calendar.getInstance(NEW_YORK, Locale.US);
355 cal.clear();
356 cal.set(2003, Calendar.FEBRUARY, 10);
357
358 final DateParser fdf = getInstance(dpProvider, "W w F D y", NEW_YORK, Locale.US);
359 assertEquals(cal.getTime(), fdf.parse("3 7 2 41 03"));
360 }
361
362 @Test
363 void testEquals() {
364 final DateParser parser1 = getInstance(YMD_SLASH);
365 final DateParser parser2 = getInstance(YMD_SLASH);
366
367 assertEquals(parser1, parser2);
368 assertEquals(parser1.hashCode(), parser2.hashCode());
369
370 assertNotEquals(parser1, new Object());
371 }
372
373 @Test
374 void testISO8601TimeZoneVariants() throws Exception {
375 final Date date = Date.from(Instant.parse("2026-01-17T04:30:00Z"));
376 final TimeZone timeZone = TimeZone.getTimeZone("UTC");
377 assertEquals(date, new FastDateParser("yyyy-MM-dd'T'HH:mm:ssXXX", timeZone, Locale.US).parse("2026-01-17T10:00:00+05:30"));
378 assertEquals(date, new FastDateParser("yyyy-MM-dd'T'HH:mm:ssXX", timeZone, Locale.US).parse("2026-01-17T10:00:00+0530"));
379 }
380
381 @Test
382 void testJpLocales() {
383
384 final Calendar cal = Calendar.getInstance(TimeZones.GMT);
385 cal.clear();
386 cal.set(2003, Calendar.FEBRUARY, 10);
387 cal.set(Calendar.ERA, GregorianCalendar.BC);
388
389 final Locale locale = LocaleUtils.toLocale("zh");
390
391
392 final SimpleDateFormat sdf = new SimpleDateFormat(LONG_FORMAT, locale);
393 final DateParser fdf = getInstance(LONG_FORMAT, locale);
394
395
396 checkParse(locale, cal, sdf, fdf);
397 }
398
399 @ParameterizedTest
400 @MethodSource(DATE_PARSER_PARAMETERS)
401 void testLANG_831(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider) throws Exception {
402 testSdfAndFdp(dpProvider, "M E", "3 Tue", true);
403 }
404
405 @ParameterizedTest
406 @MethodSource(DATE_PARSER_PARAMETERS)
407 void testLANG_832(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider) throws Exception {
408 testSdfAndFdp(dpProvider, "'d'd", "d3", false);
409 testSdfAndFdp(dpProvider, "'d'd'", "d3", true);
410 }
411
412 @ParameterizedTest
413 @MethodSource(DATE_PARSER_PARAMETERS)
414 void testLang1121(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider) throws ParseException {
415 final TimeZone kst = TimeZones.getTimeZone("KST");
416 final DateParser fdp = getInstance(dpProvider, "yyyyMMdd", kst, Locale.KOREA);
417
418 assertThrows(ParseException.class, () -> fdp.parse("2015"));
419
420
421 Date actual = fdp.parse("20150429");
422 final Calendar cal = Calendar.getInstance(kst, Locale.KOREA);
423 cal.clear();
424 cal.set(2015, Calendar.APRIL, 29);
425 Date expected = cal.getTime();
426 assertEquals(expected, actual);
427
428 final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd", Locale.KOREA);
429 sdf.setTimeZone(kst);
430 expected = sdf.parse("20150429113100");
431
432
433 actual = fdp.parse("20150429113100");
434 assertEquals(expected, actual);
435 }
436
437 @ParameterizedTest
438 @MethodSource(DATE_PARSER_PARAMETERS)
439 void testLang1359(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider) {
440
441
442 final DateParser fdp = getInstance(dpProvider, "yyyy", TimeZones.GMT, Locale.US);
443 final String overflow = "99999999999";
444 assertThrows(ParseException.class, () -> fdp.parse(overflow));
445 final ParsePosition pos = new ParsePosition(0);
446 assertNull(fdp.parseObject(overflow, pos));
447 assertTrue(pos.getErrorIndex() >= 0);
448 assertEquals(0, pos.getIndex());
449 }
450
451 @ParameterizedTest
452 @MethodSource(DATE_PARSER_PARAMETERS)
453 void testLang1380(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider) throws ParseException {
454 final Calendar expected = Calendar.getInstance(TimeZones.GMT, Locale.FRANCE);
455 expected.clear();
456 expected.set(2014, Calendar.APRIL, 14);
457
458 final DateParser fdp = getInstance(dpProvider, "dd MMM yyyy", TimeZones.GMT, Locale.FRANCE);
459 assertEquals(expected.getTime(), fdp.parse("14 avril 2014"));
460 assertEquals(expected.getTime(), fdp.parse("14 avr. 2014"));
461 assertEquals(expected.getTime(), fdp.parse("14 avr 2014"));
462 }
463
464 @Test
465 void testLang303() throws ParseException {
466 DateParser parser = getInstance(YMD_SLASH);
467 final Calendar cal = Calendar.getInstance();
468 cal.set(2004, Calendar.DECEMBER, 31);
469
470 final Date date = parser.parse("2004/11/31");
471
472 parser = SerializationUtils.deserialize(SerializationUtils.serialize((Serializable) parser));
473 assertEquals(date, parser.parse("2004/11/31"));
474 }
475
476 @Test
477 @ReadsDefaultLocale
478 void testLang538() throws ParseException {
479 final DateParser parser = getInstance("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", TimeZones.GMT);
480 final Calendar cal = Calendar.getInstance(TimeZones.getTimeZone("GMT-8"));
481 cal.clear();
482 cal.set(2009, Calendar.OCTOBER, 16, 8, 42, 16);
483 assertEquals(cal.getTime(), parser.parse("2009-10-16T16:42:16.000Z"));
484 }
485
486 @ParameterizedTest
487 @MethodSource(DATE_PARSER_PARAMETERS)
488 void testLang996(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider) throws ParseException {
489 final Calendar expected = Calendar.getInstance(NEW_YORK, Locale.US);
490 expected.clear();
491 expected.set(2014, Calendar.MAY, 14);
492
493 final DateParser fdp = getInstance(dpProvider, "ddMMMyyyy", NEW_YORK, Locale.US);
494 assertEquals(expected.getTime(), fdp.parse("14may2014"));
495 assertEquals(expected.getTime(), fdp.parse("14MAY2014"));
496 assertEquals(expected.getTime(), fdp.parse("14May2014"));
497 }
498
499 @Test
500 void testLocaleMatches() {
501 final DateParser parser = getInstance(yMdHmsSZ, SWEDEN);
502 assertEquals(SWEDEN, parser.getLocale());
503 }
504
505
506
507
508
509
510 @Test
511 void testLowYearPadding() throws ParseException {
512 final DateParser parser = getInstance(YMD_SLASH);
513 final Calendar cal = Calendar.getInstance();
514 cal.clear();
515
516 cal.set(1, Calendar.JANUARY, 1);
517 assertEquals(cal.getTime(), parser.parse("0001/01/01"));
518 cal.set(10, Calendar.JANUARY, 1);
519 assertEquals(cal.getTime(), parser.parse("0010/01/01"));
520 cal.set(100, Calendar.JANUARY, 1);
521 assertEquals(cal.getTime(), parser.parse("0100/01/01"));
522 cal.set(999, Calendar.JANUARY, 1);
523 assertEquals(cal.getTime(), parser.parse("0999/01/01"));
524 }
525
526 @Test
527 void testMilleniumBug() throws ParseException {
528 final DateParser parser = getInstance(DMY_DOT);
529 final Calendar cal = Calendar.getInstance();
530 cal.clear();
531
532 cal.set(1000, Calendar.JANUARY, 1);
533 assertEquals(cal.getTime(), parser.parse("01.01.1000"));
534 }
535
536 @ParameterizedTest
537 @MethodSource(DATE_PARSER_PARAMETERS)
538 void testParseErrorMessageJapaneseImperial(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider) {
539
540
541 final DateParser fdp = getInstance(dpProvider, "yyyy", TimeZones.GMT, FastDateParser.JAPANESE_IMPERIAL);
542 final String source = "not-a-date";
543 final ParseException e = assertThrows(ParseException.class, () -> fdp.parse(source));
544 final String message = e.getMessage();
545 assertTrue(message.startsWith("Unparseable date: '" + source + "'"), message);
546 assertTrue(message.contains("does not support dates before 1868-01-01."), message);
547 }
548
549 @ParameterizedTest
550 @CsvSource({
551 "Z, GMT+0:99", "Z, GMT-0:99", "Z, GMT+24:00", "Z, GMT-24:00",
552 "z, GMT+0:99", "z, GMT-0:99", "z, GMT+24:00", "z, GMT-24:00"
553 })
554 void testParseInvalidGmtTimeZoneOffsets(final String zonePattern, final String offset) {
555 final String pattern = "yyyy-MM-dd'T'HH:mm:ss" + zonePattern;
556 final String prefix = "2024-01-01T00:00:00";
557 assertParseFailure(new FastDateParser(pattern, TimeZones.GMT, Locale.US), prefix + offset, prefix.length());
558 assertParseFailure(FastDateFormat.getInstance(pattern, TimeZones.GMT, Locale.US), prefix + offset, prefix.length());
559 }
560
561 @ParameterizedTest
562 @MethodSource(DATE_PARSER_PARAMETERS)
563 void testParseInvalidTimeZoneOffsets(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider) {
564
565 final String prefix = "2024-01-01T00:00:00";
566 final String[][] cases = {
567 { "X", "+24", "-24", "+99", "-99" },
568 { "XX", "+2400", "-2400", "+0060", "-0060", "+9999" },
569 { "XXX", "+24:00", "-24:00", "+00:60", "-00:60", "+99:99" },
570 { "ZZ", "+24:00", "-24:00", "+00:60", "-00:60", "+99:99" },
571 { "Z", "+2400", "-2400", "+0060", "-0060" },
572 { "z", "+2400", "-2400", "+0060", "-0060" }
573 };
574 for (final String[] testCase : cases) {
575 final DateParser parser = getInstance(dpProvider, "yyyy-MM-dd'T'HH:mm:ss" + testCase[0], TimeZones.GMT, Locale.US);
576 for (int i = 1; i < testCase.length; i++) {
577 assertParseFailure(parser, prefix + testCase[i], prefix.length());
578 }
579 }
580 }
581
582 @ParameterizedTest
583 @MethodSource(DATE_PARSER_PARAMETERS)
584 void testParseLongShort(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider)
585 throws ParseException {
586 final Calendar cal = Calendar.getInstance(NEW_YORK, Locale.US);
587 cal.clear();
588 cal.set(2003, Calendar.FEBRUARY, 10, 15, 33, 20);
589 cal.set(Calendar.MILLISECOND, 989);
590 cal.setTimeZone(NEW_YORK);
591
592 DateParser fdf = getInstance(dpProvider, "yyyy GGGG MMMM dddd aaaa EEEE HHHH mmmm ssss SSSS ZZZZ", NEW_YORK,
593 Locale.US);
594
595 assertEquals(cal.getTime(), fdf.parse("2003 AD February 0010 PM Monday 0015 0033 0020 0989 GMT-05:00"));
596 cal.set(Calendar.ERA, GregorianCalendar.BC);
597
598 final Date parse = fdf.parse("2003 BC February 0010 PM Saturday 0015 0033 0020 0989 GMT-05:00");
599 assertEquals(cal.getTime(), parse);
600
601 fdf = getInstance(null, "y G M d a E H m s S Z", NEW_YORK, Locale.US);
602 assertEquals(cal.getTime(), fdf.parse("03 BC 2 10 PM Sat 15 33 20 989 -0500"));
603
604 cal.set(Calendar.ERA, GregorianCalendar.AD);
605 assertEquals(cal.getTime(), fdf.parse("03 AD 2 10 PM Saturday 15 33 20 989 -0500"));
606 }
607
608 @Test
609 void testParseMissingTimeZoneName() throws ReflectiveOperationException {
610 final FastDateParser parser = new FastDateParser("yyyy-MM-dd z", TimeZones.GMT, Locale.US);
611 final List<?> patterns = parser.getPatterns();
612 final Object zonePattern = patterns.get(patterns.size() - 1);
613 final Object cachedStrategy = FieldUtils.readField(zonePattern, "strategy", true);
614
615
616 final Constructor<?> constructor = cachedStrategy.getClass().getDeclaredConstructor(Locale.class);
617 constructor.setAccessible(true);
618 final Object strategy = constructor.newInstance(Locale.US);
619 final Map<?, ?> names = (Map<?, ?>) FieldUtils.readField(strategy, "tzNames", true);
620 assertNotNull(names.remove("PST"));
621 FieldUtils.writeField(zonePattern, "strategy", strategy, true);
622 assertParseFailure(parser, "2024-01-01 PST", 11);
623 }
624
625 @ParameterizedTest
626 @MethodSource(DATE_PARSER_PARAMETERS)
627 void testParseNumerics(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider)
628 throws ParseException {
629 final Calendar cal = Calendar.getInstance(NEW_YORK, Locale.US);
630 cal.clear();
631 cal.set(2003, Calendar.FEBRUARY, 10, 15, 33, 20);
632 cal.set(Calendar.MILLISECOND, 989);
633
634 final DateParser fdf = getInstance(dpProvider, "yyyyMMddHHmmssSSS", NEW_YORK, Locale.US);
635 assertEquals(cal.getTime(), fdf.parse("20030210153320989"));
636 }
637
638 @Test
639 void testParseOffset() {
640 final DateParser parser = getInstance(YMD_SLASH);
641 final Date date = parser.parse("Today is 2015/07/04", new ParsePosition(9));
642
643 final Calendar cal = Calendar.getInstance();
644 cal.clear();
645 cal.set(2015, Calendar.JULY, 4);
646 assertEquals(cal.getTime(), date);
647 }
648
649 @Test
650 public void testParsePositionBeyondInputLength() {
651 final String source = "Jan";
652 final int startingIndex = 10;
653 final String[] patterns = {"yyyy", "MM", "dd", "HH", "'x'", "-", "/", ":", " 'at' ", "MMM", "EEEE", "a", "z"};
654 for (final String pattern : patterns) {
655 final DateParser parser = getInstance(pattern);
656 final ParsePosition pos1 = new ParsePosition(startingIndex);
657 final Date date = parser.parse(source, pos1);
658 assertNull(date);
659 assertEquals(startingIndex, pos1.getIndex());
660 assertEquals(startingIndex, pos1.getErrorIndex());
661 final ParsePosition pos2 = new ParsePosition(startingIndex);
662 final Calendar cal = Calendar.getInstance();
663 final boolean success = parser.parse(source, pos2, cal);
664 assertFalse(success);
665 assertEquals(startingIndex, pos2.getIndex());
666 assertEquals(startingIndex, pos2.getErrorIndex());
667 }
668 }
669
670 @CartesianTest
671 @CartesianTest.MethodFactory("testParsesFactory")
672
673 void testParses(final String format, final Locale locale, final TimeZone timeZone, final int year) throws Exception {
674 final Calendar cal = getEraStart(year, timeZone, locale);
675 final Date centuryStart = cal.getTime();
676 cal.set(Calendar.MONTH, 1);
677 cal.set(Calendar.DAY_OF_MONTH, 10);
678 final Date in = cal.getTime();
679 final FastDateParser fastDateParser = new FastDateParser(format, timeZone, locale, centuryStart);
680 validateSdfFormatFdpParseEquality(format, locale, timeZone, fastDateParser, in, year, centuryStart);
681 }
682
683
684
685
686 @Test
687 void testParsesKnownJava16Ea25Failure() throws Exception {
688 final String format = LONG_FORMAT;
689 final int year = 2003;
690 final Locale locale = new Locale.Builder().setLanguage("sq").setRegion("MK").build();
691 assertEquals("sq_MK", locale.toString());
692 assertNotNull(locale);
693 final TimeZone timeZone = NEW_YORK;
694 final Calendar cal = getEraStart(year, timeZone, locale);
695 final Date centuryStart = cal.getTime();
696
697 cal.set(Calendar.MONTH, 1);
698 cal.set(Calendar.DAY_OF_MONTH, 10);
699 final Date in = cal.getTime();
700
701 final FastDateParser fastDateParser = new FastDateParser(format, timeZone, locale, centuryStart);
702 validateSdfFormatFdpParseEquality(format, locale, timeZone, fastDateParser, in, year, centuryStart);
703 }
704
705 @ParameterizedTest
706 @MethodSource(DATE_PARSER_PARAMETERS)
707 void testParseUnicodeTextLookupFailure(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider) {
708
709 assertParseFailure(getInstance(dpProvider, "dd MMMM yyyy", TimeZones.GMT, Locale.US), "01 Augu\u017ft 2024", 3);
710 assertParseFailure(getInstance(dpProvider, "yyyy-MM-dd EEEE", TimeZones.GMT, Locale.US), "2024-08-01 Thur\u017fday", 11);
711 assertParseFailure(getInstance(dpProvider, "yyyy-MM-dd EEEE", TimeZones.GMT, Locale.US), "2024-08-02 FR\u0130DAY", 11);
712 }
713
714 @ParameterizedTest
715 @MethodSource(DATE_PARSER_PARAMETERS)
716 void testParseUnicodeTextLookupSuccess(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider) throws ParseException {
717 final DateParser turkish = getInstance(dpProvider, "dd MMMM yyyy", TimeZones.GMT, new Locale("tr", "TR"));
718
719 assertEquals(turkish.parse("01 Nisan 2024"), turkish.parse("01 NISAN 2024"));
720 final DateParser german = getInstance(dpProvider, "dd MMMM yyyy", TimeZones.GMT, Locale.GERMANY);
721 assertEquals(german.parse("01 Oktober 2024"), german.parse("01 O\u212atober 2024"));
722 }
723
724 @ParameterizedTest
725 @MethodSource(DATE_PARSER_PARAMETERS)
726 void testParseValidTimeZoneOffsetBoundaries(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider) {
727 final String prefix = "2024-01-01T00:00:00";
728 final String[][] cases = {
729 { "X", "Z", "+00", "-00", "+23", "-23" },
730 { "XX", "Z", "+0000", "-0000", "+2359", "-2359" },
731 { "XXX", "Z", "+00:00", "-00:00", "+23:59", "-23:59" },
732 { "ZZ", "Z", "+00:00", "-00:00", "+23:59", "-23:59" },
733 { "Z", "+0000", "-0000", "+2359", "-2359", "GMT+0:00", "GMT-23:59" },
734 { "z", "+0000", "-0000", "+2359", "-2359", "GMT+0:00", "GMT-23:59" }
735 };
736 for (final String[] testCase : cases) {
737 final DateParser parser = getInstance(dpProvider, "yyyy-MM-dd'T'HH:mm:ss" + testCase[0], TimeZones.GMT, Locale.US);
738 for (int i = 1; i < testCase.length; i++) {
739 final String source = prefix + testCase[i];
740 final ParsePosition position = new ParsePosition(0);
741 assertNotNull(parser.parse(source, position), source);
742 assertEquals(source.length(), position.getIndex(), source);
743 assertEquals(-1, position.getErrorIndex(), source);
744 }
745 }
746 }
747
748 @ParameterizedTest
749 @MethodSource(DATE_PARSER_PARAMETERS)
750 void testParseZone(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider)
751 throws ParseException {
752 final Calendar cal = Calendar.getInstance(NEW_YORK, Locale.US);
753 cal.clear();
754 cal.set(2003, Calendar.JULY, 10, 16, 33, 20);
755
756 final DateParser fdf = getInstance(dpProvider, yMdHmsSZ, NEW_YORK, Locale.US);
757
758 assertEquals(cal.getTime(), fdf.parse("2003-07-10T15:33:20.000 -0500"));
759 assertEquals(cal.getTime(), fdf.parse("2003-07-10T15:33:20.000 GMT-05:00"));
760 assertEquals(cal.getTime(), fdf.parse("2003-07-10T16:33:20.000 Eastern Daylight Time"));
761 assertEquals(cal.getTime(), fdf.parse("2003-07-10T16:33:20.000 EDT"));
762
763 cal.setTimeZone(TimeZones.getTimeZone("GMT-3"));
764 cal.set(2003, Calendar.FEBRUARY, 10, 9, 0, 0);
765
766 assertEquals(cal.getTime(), fdf.parse("2003-02-10T09:00:00.000 -0300"));
767
768 cal.setTimeZone(TimeZones.getTimeZone("GMT+5"));
769 cal.set(2003, Calendar.FEBRUARY, 10, 15, 5, 6);
770
771 assertEquals(cal.getTime(), fdf.parse("2003-02-10T15:05:06.000 +0500"));
772 }
773
774 @Test
775 void testPatternMatches() {
776 final DateParser parser = getInstance(yMdHmsSZ);
777 assertEquals(yMdHmsSZ, parser.getPattern());
778 }
779
780 @ParameterizedTest
781 @MethodSource(DATE_PARSER_PARAMETERS)
782 void testQuotes(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider) throws ParseException {
783 final Calendar cal = Calendar.getInstance(NEW_YORK, Locale.US);
784 cal.clear();
785 cal.set(2003, Calendar.FEBRUARY, 10, 15, 33, 20);
786 cal.set(Calendar.MILLISECOND, 989);
787
788 final DateParser fdf = getInstance(dpProvider, "''yyyyMMdd'A''B'HHmmssSSS''", NEW_YORK, Locale.US);
789 assertEquals(cal.getTime(), fdf.parse("'20030210A'B153320989'"));
790 }
791
792 private void testSdfAndFdp(final TriFunction<String, TimeZone, Locale, DateParser> dbProvider, final String format,
793 final String date, final boolean shouldFail) throws Exception {
794 Date dfdp = null;
795 Date dsdf = null;
796 Throwable f = null;
797 Throwable s = null;
798
799 try {
800 final SimpleDateFormat sdf = new SimpleDateFormat(format, Locale.US);
801 sdf.setTimeZone(NEW_YORK);
802 dsdf = sdf.parse(date);
803 assertFalse(shouldFail, "Expected SDF failure, but got " + dsdf + " for [" + format + ", " + date + "]");
804 } catch (final Exception e) {
805 s = e;
806 if (!shouldFail) {
807 throw e;
808 }
809 }
810
811 try {
812 final DateParser fdp = getInstance(dbProvider, format, NEW_YORK, Locale.US);
813 dfdp = fdp.parse(date);
814 assertFalse(shouldFail, "Expected FDF failure, but got " + dfdp + " for [" + format + ", " + date + "]");
815 } catch (final Exception e) {
816 f = e;
817 if (!shouldFail) {
818 throw e;
819 }
820 }
821
822 assertEquals(f == null, s == null, "Should both or neither throw Exceptions");
823 assertEquals(dsdf, dfdp, "Parsed dates should be equal");
824 }
825
826
827
828
829
830
831 @Test
832 @ReadsDefaultLocale
833 @ReadsDefaultTimeZone
834 void testShortDateStyleWithLocales() throws ParseException {
835 DateParser fdf = getDateInstance(FastDateFormat.SHORT, Locale.US);
836 final Calendar cal = Calendar.getInstance();
837 cal.clear();
838
839 cal.set(2004, Calendar.FEBRUARY, 3);
840 assertEquals(cal.getTime(), fdf.parse("2/3/04"));
841
842 fdf = getDateInstance(FastDateFormat.SHORT, SWEDEN);
843 assertEquals(cal.getTime(), fdf.parse("2004-02-03"));
844 }
845
846 @ParameterizedTest
847 @MethodSource(DATE_PARSER_PARAMETERS)
848 void testSpecialCharacters(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider)
849 throws Exception {
850 testSdfAndFdp(dpProvider, "q", "", true);
851 testSdfAndFdp(dpProvider, "Q", "", true);
852 testSdfAndFdp(dpProvider, "$", "$", false);
853 testSdfAndFdp(dpProvider, "?.d", "?.12", false);
854 testSdfAndFdp(dpProvider, "''yyyyMMdd'A''B'HHmmssSSS''", "'20030210A'B153320989'", false);
855 testSdfAndFdp(dpProvider, "''''yyyyMMdd'A''B'HHmmssSSS''", "''20030210A'B153320989'", false);
856 testSdfAndFdp(dpProvider, "'$\\Ed'", "$\\Ed", false);
857
858
859 testSdfAndFdp(dpProvider, "'QED'", "QED", false);
860 testSdfAndFdp(dpProvider, "'QED'", "qed", true);
861
862 testSdfAndFdp(dpProvider, "yyyy-MM-dd 'QED'", "2003-02-10 QED", false);
863 testSdfAndFdp(dpProvider, "yyyy-MM-dd 'QED'", "2003-02-10 qed", true);
864 }
865
866
867
868
869 @Test
870 void testTimeZoneIsCopied() throws ParseException {
871 final TimeZone timeZone = TimeZones.getTimeZone("UTC");
872 final FastDateParser parser = new FastDateParser("yyyy-MM-dd HH:mm", timeZone, Locale.US);
873 timeZone.setRawOffset(5 * 3_600_000);
874 assertEquals(new Date(0), parser.parse("1970-01-01 00:00"));
875 parser.getTimeZone().setRawOffset(5 * 3_600_000);
876 assertEquals(TimeZones.getTimeZone("UTC"), parser.getTimeZone());
877 assertEquals(new Date(0), parser.parse("1970-01-01 00:00"));
878 }
879
880 @Test
881 @ReadsDefaultLocale
882 void testTimeZoneMatches() {
883 assertEquals(REYKJAVIK, getInstance(yMdHmsSZ, REYKJAVIK).getTimeZone());
884 }
885
886 @Test
887 void testToStringContainsName() {
888 assertTrue(getInstance(YMD_SLASH).toString().startsWith("FastDate"));
889 }
890
891
892
893 @ParameterizedTest
894 @MethodSource("org.apache.commons.lang3.LocaleUtils#availableLocaleList()")
895 void testTzParses(final Locale locale) throws Exception {
896
897 final FastDateParser fdp = new FastDateParser("yyyy/MM/dd z", TimeZone.getDefault(), locale);
898 for (final TimeZone timeZone : new TimeZone[] { NEW_YORK, REYKJAVIK, TimeZones.GMT }) {
899 final Calendar cal = Calendar.getInstance(timeZone, locale);
900 cal.clear();
901 cal.set(Calendar.YEAR, 2000);
902 cal.set(Calendar.MONTH, 1);
903 cal.set(Calendar.DAY_OF_MONTH, 10);
904 final Date expected = cal.getTime();
905 final Date actual = fdp.parse("2000/02/10 " + timeZone.getDisplayName(locale));
906 assertEquals(expected, actual, "timeZone:" + timeZone.getID() + " locale:" + locale.getDisplayName());
907 }
908 }
909
910 @Test
911 void testWeekYearParsing() throws ParseException {
912
913
914 final String[][] cases = {
915 { "YYYY-MM-dd", "2025-12-29" },
916 { "YYYY-'W'ww-u", "2025-W01-1" },
917 { "YYYY-'W'ww-u", "2020-W53-5" },
918 { "YYYY-'W'ww", "2024-W15" },
919 { "YY-MM-dd", "25-12-29" },
920 { "YYYY", "2025" },
921 { "yyyy-MM-dd", "2024-12-29" }
922 };
923 for (final Locale locale : new Locale[] { Locale.US, Locale.GERMANY }) {
924 for (final String[] testCase : cases) {
925 final SimpleDateFormat sdf = new SimpleDateFormat(testCase[0], locale);
926 final DateParser fdp = getInstance(testCase[0], locale);
927 assertEquals(sdf.parse(testCase[1]), fdp.parse(testCase[1]),
928 "Pattern " + testCase[0] + " input " + testCase[1] + " locale " + locale);
929 }
930 }
931 }
932
933 private void validateSdfFormatFdpParseEquality(final String formatStr, final Locale locale, final TimeZone timeZone,
934 final FastDateParser fastDateParser, final Date inDate, final int year, final Date csDate) throws ParseException {
935 final SimpleDateFormat sdf = new SimpleDateFormat(formatStr, locale);
936 sdf.setTimeZone(timeZone);
937 if (formatStr.equals(SHORT_FORMAT)) {
938 sdf.set2DigitYearStart(csDate);
939 }
940 final String fmt = sdf.format(inDate);
941
942
943 try {
944 final Date out = fastDateParser.parse(fmt);
945 assertEquals(inDate, out, "format: '" + formatStr + "', locale: '" + locale + "', time zone: '"
946 + timeZone.getID() + "', year: " + year + ", parse: '" + fmt);
947 } catch (final ParseException e) {
948 LocaleProblems.assumeLocaleSupportedB(locale, e);
949 if (year >= 1868 || !locale.getCountry().equals("JP")) {
950
951 throw new AssertionFailedError("locale " + locale, e);
952 }
953 }
954 }
955 }
956