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