View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
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.assertThrows;
25  import static org.junit.jupiter.api.Assertions.assertTrue;
26  import static org.junit.jupiter.api.Assertions.fail;
27  
28  import java.io.Serializable;
29  import java.text.ParseException;
30  import java.text.ParsePosition;
31  import java.text.SimpleDateFormat;
32  import java.time.Instant;
33  import java.util.Calendar;
34  import java.util.Date;
35  import java.util.GregorianCalendar;
36  import java.util.HashMap;
37  import java.util.Locale;
38  import java.util.Map;
39  import java.util.TimeZone;
40  import java.util.stream.Stream;
41  
42  import org.apache.commons.lang3.AbstractLangTest;
43  import org.apache.commons.lang3.LocaleProblems;
44  import org.apache.commons.lang3.LocaleUtils;
45  import org.apache.commons.lang3.SerializationUtils;
46  import org.apache.commons.lang3.SystemUtils;
47  import org.apache.commons.lang3.function.TriFunction;
48  import org.junit.jupiter.api.AfterEach;
49  import org.junit.jupiter.api.BeforeEach;
50  import org.junit.jupiter.api.Test;
51  import org.junit.jupiter.params.ParameterizedTest;
52  import org.junit.jupiter.params.provider.Arguments;
53  import org.junit.jupiter.params.provider.MethodSource;
54  import org.junitpioneer.jupiter.DefaultLocale;
55  import org.junitpioneer.jupiter.DefaultTimeZone;
56  import org.junitpioneer.jupiter.ReadsDefaultLocale;
57  import org.junitpioneer.jupiter.ReadsDefaultTimeZone;
58  import org.junitpioneer.jupiter.cartesian.ArgumentSets;
59  import org.junitpioneer.jupiter.cartesian.CartesianTest;
60  import org.opentest4j.AssertionFailedError;
61  
62  /**
63   * Tests {@link org.apache.commons.lang3.time.FastDateParser}.
64   */
65  /* Make test reproducible */ @DefaultLocale(language = "en")
66  /* Make test reproducible */ @DefaultTimeZone(TimeZones.GMT_ID)
67  /* Make test reproducible */ @ReadsDefaultLocale
68  /* Make test reproducible */ @ReadsDefaultTimeZone
69  class FastDateParserTest extends AbstractLangTest {
70  
71      private enum Expected1806 {
72  
73          // @formatter:off
74          India(INDIA, "+05", "+0530", "+05:30", true),
75          Greenwich(TimeZones.GMT, "Z", "Z", "Z", false),
76          NewYork(NEW_YORK, "-05", "-0500", "-05:00", false);
77          // @formatter:on
78  
79          final TimeZone zone;
80          final String one;
81          final String two;
82          final String three;
83          final long offset;
84  
85          Expected1806(final TimeZone zone, final String one, final String two, final String three,
86              final boolean hasHalfHourOffset) {
87              this.zone = zone;
88              this.one = one;
89              this.two = two;
90              this.three = three;
91              this.offset = hasHalfHourOffset ? 30 * 60 * 1000 : 0;
92          }
93      }
94  
95      static final String DATE_PARSER_PARAMETERS = "dateParserParameters";
96  
97      static final String SHORT_FORMAT_NOERA = "y/M/d/h/a/m/s/E";
98  
99      static final String LONG_FORMAT_NOERA = "yyyy/MMMM/dddd/hhhh/mmmm/ss/aaaa/EEEE";
100     static final String SHORT_FORMAT = "G/" + SHORT_FORMAT_NOERA;
101     static final String LONG_FORMAT = "GGGG/" + LONG_FORMAT_NOERA;
102 
103     private static final String yMdHmsSZ = "yyyy-MM-dd'T'HH:mm:ss.SSS Z";
104     private static final String DMY_DOT = "dd.MM.yyyy";
105     private static final String YMD_SLASH = "yyyy/MM/dd";
106     private static final String MDY_DASH = "MM-DD-yyyy";
107     private static final String MDY_SLASH = "MM/DD/yyyy";
108 
109     private static final TimeZone REYKJAVIK = TimeZones.getTimeZone("Atlantic/Reykjavik");
110     private static final TimeZone NEW_YORK = TimeZones.getTimeZone("America/New_York");
111     private static final TimeZone INDIA = TimeZones.getTimeZone("Asia/Calcutta");
112 
113     private static final Locale SWEDEN = new Locale("sv", "SE");
114 
115     static void checkParse(final Locale locale, final Calendar cal, final SimpleDateFormat simpleDateFormat,
116             final DateParser dateParser) {
117         final String formattedDate = simpleDateFormat.format(cal.getTime());
118         checkParse(locale, simpleDateFormat, dateParser, formattedDate, formattedDate);
119         checkParse(locale, simpleDateFormat, dateParser, formattedDate.toLowerCase(locale), formattedDate);
120         checkParse(locale, simpleDateFormat, dateParser, formattedDate.toUpperCase(locale), formattedDate);
121     }
122 
123     static void checkParse(final Locale locale, final SimpleDateFormat simpleDateFormat, final DateParser dateParser,
124         final String formattedDate, final String originalFormattedDate) {
125         try {
126             final Date expectedTime = simpleDateFormat.parse(formattedDate);
127             final Date actualTime = dateParser.parse(formattedDate);
128             assertEquals(expectedTime, actualTime,
129                 "locale: " + locale + ", formattedDate: '" + formattedDate + "', originalFormattedDate: '"
130                     + originalFormattedDate + ", simpleDateFormat.pattern: '" + simpleDateFormat + "', Java: "
131                     + SystemUtils.JAVA_RUNTIME_VERSION + "\n");
132         } catch (final Exception e) {
133             fail("locale: " + locale + ", formattedDate: '" + formattedDate + "', error : " + e + "\n", e);
134         }
135     }
136 
137     static Stream<Arguments> dateParserParameters() {
138         return Stream.of(
139         // @formatter:off
140             Arguments.of((TriFunction<String, TimeZone, Locale, DateParser>) (format, timeZone, locale)
141                 -> new FastDateParser(format, timeZone, locale, null)),
142             Arguments.of((TriFunction<String, TimeZone, Locale, DateParser>) FastDateFormat::getInstance)
143         // @formatter:on
144         );
145     }
146 
147     private static Calendar initializeCalendar(final TimeZone timeZone) {
148         final Calendar cal = Calendar.getInstance(timeZone);
149         cal.set(Calendar.YEAR, 2001);
150         cal.set(Calendar.MONTH, 1); // not daylight savings
151         cal.set(Calendar.DAY_OF_MONTH, 4);
152         cal.set(Calendar.HOUR_OF_DAY, 12);
153         cal.set(Calendar.MINUTE, 8);
154         cal.set(Calendar.SECOND, 56);
155         cal.set(Calendar.MILLISECOND, 235);
156         return cal;
157     }
158 
159     static ArgumentSets testParsesFactory() {
160         // @formatter:off
161         return ArgumentSets
162             .argumentsForFirstParameter(LONG_FORMAT, SHORT_FORMAT)
163             .argumentsForNextParameter(LocaleUtils.availableLocaleList())
164             .argumentsForNextParameter(NEW_YORK, REYKJAVIK, TimeZones.GMT)
165             .argumentsForNextParameter(2003, 1940, 1868, 1867, 1, -1, -1940);
166         // @formatter:on
167     }
168 
169     private final TriFunction<String, TimeZone, Locale, DateParser> dateParserProvider = (format, timeZone, locale) -> new FastDateParser(format, timeZone,
170             locale, null);
171 
172     @BeforeEach
173     @AfterEach
174     void clear() {
175         AbstractFormatCache.clear();
176         FastDateFormat.clear();
177         FastDateParser.clear();
178         FastDatePrinter.clear();
179     }
180 
181     private DateParser getDateInstance(final int dateStyle, final Locale locale) {
182         return getInstance(null, AbstractFormatCache.getPatternForStyle(Integer.valueOf(dateStyle), null, locale), TimeZone.getDefault(), Locale.getDefault());
183     }
184 
185     private Calendar getEraStart(int year, final TimeZone zone, final Locale locale) {
186         final Calendar cal = Calendar.getInstance(zone, locale);
187         cal.clear();
188         // https://docs.oracle.com/javase/8/docs/technotes/guides/intl/calendar.doc.html
189         if (locale.equals(FastDateParser.JAPANESE_IMPERIAL)) {
190             if (year < 1868) {
191                 cal.set(Calendar.ERA, 0);
192                 cal.set(Calendar.YEAR, 1868 - year);
193             }
194         } else {
195             if (year < 0) {
196                 cal.set(Calendar.ERA, GregorianCalendar.BC);
197                 year = -year;
198             }
199             cal.set(Calendar.YEAR, year / 100 * 100);
200         }
201         return cal;
202     }
203 
204     DateParser getInstance(final String format) {
205         return getInstance(null, format, TimeZone.getDefault(), Locale.getDefault());
206     }
207 
208     DateParser getInstance(final String format, final Locale locale) {
209         return getInstance(null, format, TimeZone.getDefault(), locale);
210     }
211 
212     private DateParser getInstance(final String format, final TimeZone timeZone) {
213         return getInstance(null, format, timeZone, Locale.getDefault());
214     }
215 
216     /**
217      * Override this method in derived tests to change the construction of instances
218      *
219      * @param dpProvider TODO
220      * @param format the format string to use
221      * @param timeZone the time zone to use
222      * @param locale the locale to use
223      * @return the DateParser instance to use for testing
224      */
225     protected DateParser getInstance(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider,
226         final String format, final TimeZone timeZone, final Locale locale) {
227         return (dpProvider == null ? this.dateParserProvider : dpProvider).apply(format, timeZone, locale);
228     }
229 
230     @ParameterizedTest
231     @MethodSource(DATE_PARSER_PARAMETERS)
232     void test_Equality_Hash(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider) {
233         // @formatter:off
234         final DateParser[] parsers = {
235             getInstance(dpProvider, yMdHmsSZ, NEW_YORK, Locale.US),
236             getInstance(dpProvider, DMY_DOT, NEW_YORK, Locale.US),
237             getInstance(dpProvider, YMD_SLASH, NEW_YORK, Locale.US),
238             getInstance(dpProvider, MDY_DASH, NEW_YORK, Locale.US),
239             getInstance(dpProvider, MDY_SLASH, NEW_YORK, Locale.US),
240             getInstance(dpProvider, MDY_SLASH, REYKJAVIK, Locale.US),
241             getInstance(dpProvider, MDY_SLASH, REYKJAVIK, SWEDEN)
242         };
243         // @formatter:on
244         final Map<DateParser, Integer> map = new HashMap<>();
245         int i = 0;
246         for (final DateParser parser : parsers) {
247             map.put(parser, Integer.valueOf(i++));
248         }
249         i = 0;
250         for (final DateParser parser : parsers) {
251             assertEquals(i++, map.get(parser).intValue());
252         }
253     }
254 
255     @Test
256     void test1806() throws ParseException {
257         final String formatStub = "yyyy-MM-dd'T'HH:mm:ss.SSS";
258         final String dateStub = "2001-02-04T12:08:56.235";
259 
260         for (final Expected1806 trial : Expected1806.values()) {
261             final Calendar cal = initializeCalendar(trial.zone);
262 
263             final String message = trial.zone.getDisplayName() + ";";
264 
265             DateParser parser = getInstance(formatStub + "X", trial.zone);
266             assertEquals(cal.getTime().getTime(), parser.parse(dateStub + trial.one).getTime() - trial.offset,
267                 message + trial.one);
268 
269             parser = getInstance(formatStub + "XX", trial.zone);
270             assertEquals(cal.getTime(), parser.parse(dateStub + trial.two), message + trial.two);
271 
272             parser = getInstance(formatStub + "XXX", trial.zone);
273             assertEquals(cal.getTime(), parser.parse(dateStub + trial.three), message + trial.three);
274         }
275     }
276 
277     @Test
278     void test1806Argument() {
279         assertIllegalArgumentException(() -> getInstance("XXXX"));
280     }
281 
282     @ParameterizedTest
283     @MethodSource(DATE_PARSER_PARAMETERS)
284     void testAmPm(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider) throws ParseException {
285         final Calendar cal = Calendar.getInstance(NEW_YORK, Locale.US);
286         cal.clear();
287 
288         final DateParser h = getInstance(dpProvider, "yyyy-MM-dd hh a mm:ss", NEW_YORK, Locale.US);
289         final DateParser K = getInstance(dpProvider, "yyyy-MM-dd KK a mm:ss", NEW_YORK, Locale.US);
290         final DateParser k = getInstance(dpProvider, "yyyy-MM-dd kk:mm:ss", NEW_YORK, Locale.US);
291         final DateParser H = getInstance(dpProvider, "yyyy-MM-dd HH:mm:ss", NEW_YORK, Locale.US);
292 
293         cal.set(2010, Calendar.AUGUST, 1, 0, 33, 20);
294         assertEquals(cal.getTime(), h.parse("2010-08-01 12 AM 33:20"));
295         assertEquals(cal.getTime(), K.parse("2010-08-01 0 AM 33:20"));
296         assertEquals(cal.getTime(), k.parse("2010-08-01 00:33:20"));
297         assertEquals(cal.getTime(), H.parse("2010-08-01 00:33:20"));
298 
299         cal.set(2010, Calendar.AUGUST, 1, 3, 33, 20);
300         assertEquals(cal.getTime(), h.parse("2010-08-01 3 AM 33:20"));
301         assertEquals(cal.getTime(), K.parse("2010-08-01 3 AM 33:20"));
302         assertEquals(cal.getTime(), k.parse("2010-08-01 03:33:20"));
303         assertEquals(cal.getTime(), H.parse("2010-08-01 03:33:20"));
304 
305         cal.set(2010, Calendar.AUGUST, 1, 15, 33, 20);
306         assertEquals(cal.getTime(), h.parse("2010-08-01 3 PM 33:20"));
307         assertEquals(cal.getTime(), K.parse("2010-08-01 3 PM 33:20"));
308         assertEquals(cal.getTime(), k.parse("2010-08-01 15:33:20"));
309         assertEquals(cal.getTime(), H.parse("2010-08-01 15:33:20"));
310 
311         cal.set(2010, Calendar.AUGUST, 1, 12, 33, 20);
312         assertEquals(cal.getTime(), h.parse("2010-08-01 12 PM 33:20"));
313         assertEquals(cal.getTime(), K.parse("2010-08-01 0 PM 33:20"));
314         assertEquals(cal.getTime(), k.parse("2010-08-01 12:33:20"));
315         assertEquals(cal.getTime(), H.parse("2010-08-01 12:33:20"));
316     }
317 
318     @Test
319     void testDayNumberOfWeek() throws ParseException {
320         final DateParser parser = getInstance("u");
321         final Calendar calendar = Calendar.getInstance();
322 
323         calendar.setTime(parser.parse("1"));
324         assertEquals(Calendar.MONDAY, calendar.get(Calendar.DAY_OF_WEEK));
325 
326         calendar.setTime(parser.parse("6"));
327         assertEquals(Calendar.SATURDAY, calendar.get(Calendar.DAY_OF_WEEK));
328 
329         calendar.setTime(parser.parse("7"));
330         assertEquals(Calendar.SUNDAY, calendar.get(Calendar.DAY_OF_WEEK));
331     }
332 
333     @ParameterizedTest
334     @MethodSource(DATE_PARSER_PARAMETERS)
335     void testDayOf(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider) throws ParseException {
336         final Calendar cal = Calendar.getInstance(NEW_YORK, Locale.US);
337         cal.clear();
338         cal.set(2003, Calendar.FEBRUARY, 10);
339 
340         final DateParser fdf = getInstance(dpProvider, "W w F D y", NEW_YORK, Locale.US);
341         assertEquals(cal.getTime(), fdf.parse("3 7 2 41 03"));
342     }
343 
344     @Test
345     void testEquals() {
346         final DateParser parser1 = getInstance(YMD_SLASH);
347         final DateParser parser2 = getInstance(YMD_SLASH);
348 
349         assertEquals(parser1, parser2);
350         assertEquals(parser1.hashCode(), parser2.hashCode());
351 
352         assertNotEquals(parser1, new Object());
353     }
354 
355     @Test
356     void testISO8601TimeZoneVariants() throws Exception {
357         final Date date = Date.from(Instant.parse("2026-01-17T04:30:00Z"));
358         final TimeZone timeZone = TimeZone.getTimeZone("UTC");
359         assertEquals(date, new FastDateParser("yyyy-MM-dd'T'HH:mm:ssXXX", timeZone, Locale.US).parse("2026-01-17T10:00:00+05:30"));
360         assertEquals(date, new FastDateParser("yyyy-MM-dd'T'HH:mm:ssXX", timeZone, Locale.US).parse("2026-01-17T10:00:00+0530"));
361     }
362 
363     @Test
364     void testJpLocales() {
365 
366         final Calendar cal = Calendar.getInstance(TimeZones.GMT);
367         cal.clear();
368         cal.set(2003, Calendar.FEBRUARY, 10);
369         cal.set(Calendar.ERA, GregorianCalendar.BC);
370 
371         final Locale locale = LocaleUtils.toLocale("zh");
372         // ja_JP_JP cannot handle dates before 1868 properly
373 
374         final SimpleDateFormat sdf = new SimpleDateFormat(LONG_FORMAT, locale);
375         final DateParser fdf = getInstance(LONG_FORMAT, locale);
376 
377         // If parsing fails, a ParseException will be thrown and the test will fail
378         checkParse(locale, cal, sdf, fdf);
379     }
380 
381     @ParameterizedTest
382     @MethodSource(DATE_PARSER_PARAMETERS)
383     void testLANG_831(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider) throws Exception {
384         testSdfAndFdp(dpProvider, "M E", "3  Tue", true);
385     }
386 
387     @ParameterizedTest
388     @MethodSource(DATE_PARSER_PARAMETERS)
389     void testLANG_832(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider) throws Exception {
390         testSdfAndFdp(dpProvider, "'d'd", "d3", false); // OK
391         testSdfAndFdp(dpProvider, "'d'd'", "d3", true); // should fail (unterminated quote)
392     }
393 
394     @ParameterizedTest
395     @MethodSource(DATE_PARSER_PARAMETERS)
396     void testLang1121(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider) throws ParseException {
397         final TimeZone kst = TimeZones.getTimeZone("KST");
398         final DateParser fdp = getInstance(dpProvider, "yyyyMMdd", kst, Locale.KOREA);
399 
400         assertThrows(ParseException.class, () -> fdp.parse("2015"));
401 
402         // Wed Apr 29 00:00:00 KST 2015
403         Date actual = fdp.parse("20150429");
404         final Calendar cal = Calendar.getInstance(kst, Locale.KOREA);
405         cal.clear();
406         cal.set(2015, Calendar.APRIL, 29);
407         Date expected = cal.getTime();
408         assertEquals(expected, actual);
409 
410         final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd", Locale.KOREA);
411         sdf.setTimeZone(kst);
412         expected = sdf.parse("20150429113100");
413 
414         // Thu Mar 16 00:00:00 KST 81724
415         actual = fdp.parse("20150429113100");
416         assertEquals(expected, actual);
417     }
418 
419     @ParameterizedTest
420     @MethodSource(DATE_PARSER_PARAMETERS)
421     void testLang1380(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider) throws ParseException {
422         final Calendar expected = Calendar.getInstance(TimeZones.GMT, Locale.FRANCE);
423         expected.clear();
424         expected.set(2014, Calendar.APRIL, 14);
425 
426         final DateParser fdp = getInstance(dpProvider, "dd MMM yyyy", TimeZones.GMT, Locale.FRANCE);
427         assertEquals(expected.getTime(), fdp.parse("14 avril 2014"));
428         assertEquals(expected.getTime(), fdp.parse("14 avr. 2014"));
429         assertEquals(expected.getTime(), fdp.parse("14 avr 2014"));
430     }
431 
432     @Test
433     void testLang303() throws ParseException {
434         DateParser parser = getInstance(YMD_SLASH);
435         final Calendar cal = Calendar.getInstance();
436         cal.set(2004, Calendar.DECEMBER, 31);
437 
438         final Date date = parser.parse("2004/11/31");
439 
440         parser = SerializationUtils.deserialize(SerializationUtils.serialize((Serializable) parser));
441         assertEquals(date, parser.parse("2004/11/31"));
442     }
443 
444     @Test
445     void testLang538() throws ParseException {
446         final DateParser parser = getInstance("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", TimeZones.GMT);
447 
448         final Calendar cal = Calendar.getInstance(TimeZones.getTimeZone("GMT-8"));
449         cal.clear();
450         cal.set(2009, Calendar.OCTOBER, 16, 8, 42, 16);
451 
452         assertEquals(cal.getTime(), parser.parse("2009-10-16T16:42:16.000Z"));
453     }
454 
455     @ParameterizedTest
456     @MethodSource(DATE_PARSER_PARAMETERS)
457     void testLang996(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider) throws ParseException {
458         final Calendar expected = Calendar.getInstance(NEW_YORK, Locale.US);
459         expected.clear();
460         expected.set(2014, Calendar.MAY, 14);
461 
462         final DateParser fdp = getInstance(dpProvider, "ddMMMyyyy", NEW_YORK, Locale.US);
463         assertEquals(expected.getTime(), fdp.parse("14may2014"));
464         assertEquals(expected.getTime(), fdp.parse("14MAY2014"));
465         assertEquals(expected.getTime(), fdp.parse("14May2014"));
466     }
467 
468     @Test
469     void testLocaleMatches() {
470         final DateParser parser = getInstance(yMdHmsSZ, SWEDEN);
471         assertEquals(SWEDEN, parser.getLocale());
472     }
473 
474     /**
475      * Tests that pre-1000AD years get padded with yyyy
476      *
477      * @throws ParseException so we don't have to catch it
478      */
479     @Test
480     void testLowYearPadding() throws ParseException {
481         final DateParser parser = getInstance(YMD_SLASH);
482         final Calendar cal = Calendar.getInstance();
483         cal.clear();
484 
485         cal.set(1, Calendar.JANUARY, 1);
486         assertEquals(cal.getTime(), parser.parse("0001/01/01"));
487         cal.set(10, Calendar.JANUARY, 1);
488         assertEquals(cal.getTime(), parser.parse("0010/01/01"));
489         cal.set(100, Calendar.JANUARY, 1);
490         assertEquals(cal.getTime(), parser.parse("0100/01/01"));
491         cal.set(999, Calendar.JANUARY, 1);
492         assertEquals(cal.getTime(), parser.parse("0999/01/01"));
493     }
494 
495     @Test
496     void testMilleniumBug() throws ParseException {
497         final DateParser parser = getInstance(DMY_DOT);
498         final Calendar cal = Calendar.getInstance();
499         cal.clear();
500 
501         cal.set(1000, Calendar.JANUARY, 1);
502         assertEquals(cal.getTime(), parser.parse("01.01.1000"));
503     }
504 
505     @ParameterizedTest
506     @MethodSource(DATE_PARSER_PARAMETERS)
507     void testParseLongShort(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider)
508         throws ParseException {
509         final Calendar cal = Calendar.getInstance(NEW_YORK, Locale.US);
510         cal.clear();
511         cal.set(2003, Calendar.FEBRUARY, 10, 15, 33, 20);
512         cal.set(Calendar.MILLISECOND, 989);
513         cal.setTimeZone(NEW_YORK);
514 
515         DateParser fdf = getInstance(dpProvider, "yyyy GGGG MMMM dddd aaaa EEEE HHHH mmmm ssss SSSS ZZZZ", NEW_YORK,
516             Locale.US);
517 
518         assertEquals(cal.getTime(), fdf.parse("2003 AD February 0010 PM Monday 0015 0033 0020 0989 GMT-05:00"));
519         cal.set(Calendar.ERA, GregorianCalendar.BC);
520 
521         final Date parse = fdf.parse("2003 BC February 0010 PM Saturday 0015 0033 0020 0989 GMT-05:00");
522         assertEquals(cal.getTime(), parse);
523 
524         fdf = getInstance(null, "y G M d a E H m s S Z", NEW_YORK, Locale.US);
525         assertEquals(cal.getTime(), fdf.parse("03 BC 2 10 PM Sat 15 33 20 989 -0500"));
526 
527         cal.set(Calendar.ERA, GregorianCalendar.AD);
528         assertEquals(cal.getTime(), fdf.parse("03 AD 2 10 PM Saturday 15 33 20 989 -0500"));
529     }
530 
531     @ParameterizedTest
532     @MethodSource(DATE_PARSER_PARAMETERS)
533     void testParseNumerics(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider)
534         throws ParseException {
535         final Calendar cal = Calendar.getInstance(NEW_YORK, Locale.US);
536         cal.clear();
537         cal.set(2003, Calendar.FEBRUARY, 10, 15, 33, 20);
538         cal.set(Calendar.MILLISECOND, 989);
539 
540         final DateParser fdf = getInstance(dpProvider, "yyyyMMddHHmmssSSS", NEW_YORK, Locale.US);
541         assertEquals(cal.getTime(), fdf.parse("20030210153320989"));
542     }
543 
544     @Test
545     void testParseOffset() {
546         final DateParser parser = getInstance(YMD_SLASH);
547         final Date date = parser.parse("Today is 2015/07/04", new ParsePosition(9));
548 
549         final Calendar cal = Calendar.getInstance();
550         cal.clear();
551         cal.set(2015, Calendar.JULY, 4);
552         assertEquals(cal.getTime(), date);
553     }
554 
555     @CartesianTest
556     @CartesianTest.MethodFactory("testParsesFactory")
557     // Check that all Locales can parse the formats we use
558     void testParses(final String format, final Locale locale, final TimeZone timeZone, final int year) throws Exception {
559         final Calendar cal = getEraStart(year, timeZone, locale);
560         final Date centuryStart = cal.getTime();
561         cal.set(Calendar.MONTH, 1);
562         cal.set(Calendar.DAY_OF_MONTH, 10);
563         final Date in = cal.getTime();
564         final FastDateParser fastDateParser = new FastDateParser(format, timeZone, locale, centuryStart);
565         validateSdfFormatFdpParseEquality(format, locale, timeZone, fastDateParser, in, year, centuryStart);
566     }
567 
568     /**
569      * Fails on Java 16 Early Access build 25 and above, last tested with build 36.
570      */
571     @Test
572     void testParsesKnownJava16Ea25Failure() throws Exception {
573         final String format = LONG_FORMAT;
574         final int year = 2003;
575         final Locale locale = new Locale.Builder().setLanguage("sq").setRegion("MK").build();
576         assertEquals("sq_MK", locale.toString());
577         assertNotNull(locale);
578         final TimeZone timeZone = NEW_YORK;
579         final Calendar cal = getEraStart(year, timeZone, locale);
580         final Date centuryStart = cal.getTime();
581 
582         cal.set(Calendar.MONTH, 1);
583         cal.set(Calendar.DAY_OF_MONTH, 10);
584         final Date in = cal.getTime();
585 
586         final FastDateParser fastDateParser = new FastDateParser(format, timeZone, locale, centuryStart);
587         validateSdfFormatFdpParseEquality(format, locale, timeZone, fastDateParser, in, year, centuryStart);
588     }
589 
590     @ParameterizedTest
591     @MethodSource(DATE_PARSER_PARAMETERS)
592     void testParseZone(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider)
593         throws ParseException {
594         final Calendar cal = Calendar.getInstance(NEW_YORK, Locale.US);
595         cal.clear();
596         cal.set(2003, Calendar.JULY, 10, 16, 33, 20);
597 
598         final DateParser fdf = getInstance(dpProvider, yMdHmsSZ, NEW_YORK, Locale.US);
599 
600         assertEquals(cal.getTime(), fdf.parse("2003-07-10T15:33:20.000 -0500"));
601         assertEquals(cal.getTime(), fdf.parse("2003-07-10T15:33:20.000 GMT-05:00"));
602         assertEquals(cal.getTime(), fdf.parse("2003-07-10T16:33:20.000 Eastern Daylight Time"));
603         assertEquals(cal.getTime(), fdf.parse("2003-07-10T16:33:20.000 EDT"));
604 
605         cal.setTimeZone(TimeZones.getTimeZone("GMT-3"));
606         cal.set(2003, Calendar.FEBRUARY, 10, 9, 0, 0);
607 
608         assertEquals(cal.getTime(), fdf.parse("2003-02-10T09:00:00.000 -0300"));
609 
610         cal.setTimeZone(TimeZones.getTimeZone("GMT+5"));
611         cal.set(2003, Calendar.FEBRUARY, 10, 15, 5, 6);
612 
613         assertEquals(cal.getTime(), fdf.parse("2003-02-10T15:05:06.000 +0500"));
614     }
615 
616     @Test
617     void testPatternMatches() {
618         final DateParser parser = getInstance(yMdHmsSZ);
619         assertEquals(yMdHmsSZ, parser.getPattern());
620     }
621 
622     @ParameterizedTest
623     @MethodSource(DATE_PARSER_PARAMETERS)
624     void testQuotes(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider) throws ParseException {
625         final Calendar cal = Calendar.getInstance(NEW_YORK, Locale.US);
626         cal.clear();
627         cal.set(2003, Calendar.FEBRUARY, 10, 15, 33, 20);
628         cal.set(Calendar.MILLISECOND, 989);
629 
630         final DateParser fdf = getInstance(dpProvider, "''yyyyMMdd'A''B'HHmmssSSS''", NEW_YORK, Locale.US);
631         assertEquals(cal.getTime(), fdf.parse("'20030210A'B153320989'"));
632     }
633 
634     private void testSdfAndFdp(final TriFunction<String, TimeZone, Locale, DateParser> dbProvider, final String format,
635         final String date, final boolean shouldFail) throws Exception {
636         Date dfdp = null;
637         Date dsdf = null;
638         Throwable f = null;
639         Throwable s = null;
640 
641         try {
642             final SimpleDateFormat sdf = new SimpleDateFormat(format, Locale.US);
643             sdf.setTimeZone(NEW_YORK);
644             dsdf = sdf.parse(date);
645             assertFalse(shouldFail, "Expected SDF failure, but got " + dsdf + " for [" + format + ", " + date + "]");
646         } catch (final Exception e) {
647             s = e;
648             if (!shouldFail) {
649                 throw e;
650             }
651         }
652 
653         try {
654             final DateParser fdp = getInstance(dbProvider, format, NEW_YORK, Locale.US);
655             dfdp = fdp.parse(date);
656             assertFalse(shouldFail, "Expected FDF failure, but got " + dfdp + " for [" + format + ", " + date + "]");
657         } catch (final Exception e) {
658             f = e;
659             if (!shouldFail) {
660                 throw e;
661             }
662         }
663         // SDF and FDF should produce equivalent results
664         assertEquals(f == null, s == null, "Should both or neither throw Exceptions");
665         assertEquals(dsdf, dfdp, "Parsed dates should be equal");
666     }
667 
668     /**
669      * Test case for {@link FastDateParser#FastDateParser(String, TimeZone, Locale)}.
670      *
671      * @throws ParseException so we don't have to catch it
672      */
673     @Test
674     void testShortDateStyleWithLocales() throws ParseException {
675         DateParser fdf = getDateInstance(FastDateFormat.SHORT, Locale.US);
676         final Calendar cal = Calendar.getInstance();
677         cal.clear();
678 
679         cal.set(2004, Calendar.FEBRUARY, 3);
680         assertEquals(cal.getTime(), fdf.parse("2/3/04"));
681 
682         fdf = getDateInstance(FastDateFormat.SHORT, SWEDEN);
683         assertEquals(cal.getTime(), fdf.parse("2004-02-03"));
684     }
685 
686     @ParameterizedTest
687     @MethodSource(DATE_PARSER_PARAMETERS)
688     void testSpecialCharacters(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider)
689         throws Exception {
690         testSdfAndFdp(dpProvider, "q", "", true); // bad pattern character (at present)
691         testSdfAndFdp(dpProvider, "Q", "", true); // bad pattern character
692         testSdfAndFdp(dpProvider, "$", "$", false); // OK
693         testSdfAndFdp(dpProvider, "?.d", "?.12", false); // OK
694         testSdfAndFdp(dpProvider, "''yyyyMMdd'A''B'HHmmssSSS''", "'20030210A'B153320989'", false); // OK
695         testSdfAndFdp(dpProvider, "''''yyyyMMdd'A''B'HHmmssSSS''", "''20030210A'B153320989'", false); // OK
696         testSdfAndFdp(dpProvider, "'$\\Ed'", "$\\Ed", false); // OK
697 
698         // quoted characters are case-sensitive
699         testSdfAndFdp(dpProvider, "'QED'", "QED", false);
700         testSdfAndFdp(dpProvider, "'QED'", "qed", true);
701         // case-sensitive after insensitive Month field
702         testSdfAndFdp(dpProvider, "yyyy-MM-dd 'QED'", "2003-02-10 QED", false);
703         testSdfAndFdp(dpProvider, "yyyy-MM-dd 'QED'", "2003-02-10 qed", true);
704     }
705 
706     @Test
707     void testTimeZoneMatches() {
708         final DateParser parser = getInstance(yMdHmsSZ, REYKJAVIK);
709         assertEquals(REYKJAVIK, parser.getTimeZone());
710     }
711 
712     @Test
713     void testToStringContainsName() {
714         final DateParser parser = getInstance(YMD_SLASH);
715         assertTrue(parser.toString().startsWith("FastDate"));
716     }
717 
718     // we cannot use historic dates to test time zone parsing, some time zones have second offsets
719     // as well as hours and minutes which makes the z formats a low fidelity round trip
720     @ParameterizedTest
721     @MethodSource("org.apache.commons.lang3.LocaleUtils#availableLocaleList()")
722     void testTzParses(final Locale locale) throws Exception {
723         // Check that all Locales can parse the time formats we use
724         final FastDateParser fdp = new FastDateParser("yyyy/MM/dd z", TimeZone.getDefault(), locale);
725         for (final TimeZone timeZone : new TimeZone[] { NEW_YORK, REYKJAVIK, TimeZones.GMT }) {
726             final Calendar cal = Calendar.getInstance(timeZone, locale);
727             cal.clear();
728             cal.set(Calendar.YEAR, 2000);
729             cal.set(Calendar.MONTH, 1);
730             cal.set(Calendar.DAY_OF_MONTH, 10);
731             final Date expected = cal.getTime();
732             final Date actual = fdp.parse("2000/02/10 " + timeZone.getDisplayName(locale));
733             assertEquals(expected, actual, "timeZone:" + timeZone.getID() + " locale:" + locale.getDisplayName());
734         }
735     }
736 
737     private void validateSdfFormatFdpParseEquality(final String formatStr, final Locale locale, final TimeZone timeZone,
738         final FastDateParser fastDateParser, final Date inDate, final int year, final Date csDate) throws ParseException {
739         final SimpleDateFormat sdf = new SimpleDateFormat(formatStr, locale);
740         sdf.setTimeZone(timeZone);
741         if (formatStr.equals(SHORT_FORMAT)) {
742             sdf.set2DigitYearStart(csDate);
743         }
744         final String fmt = sdf.format(inDate);
745 //        System.out.printf("[Java %s] Date: '%s' formatted with '%s' -> '%s'%n", SystemUtils.JAVA_RUNTIME_VERSION, inDate,
746 //            formatStr, fmt);
747         try {
748             final Date out = fastDateParser.parse(fmt);
749             assertEquals(inDate, out, "format: '" + formatStr + "', locale: '" + locale + "', time zone: '"
750                 + timeZone.getID() + "', year: " + year + ", parse: '" + fmt);
751         } catch (final ParseException e) {
752             LocaleProblems.assumeLocaleSupportedB(locale, e);
753             if (year >= 1868 || !locale.getCountry().equals("JP")) {
754                 // LANG-978
755                 throw new AssertionFailedError("locale " + locale, e);
756             }
757         }
758     }
759 }
760