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