xref: /aosp_15_r20/external/apache-commons-lang/src/test/java/org/apache/commons/lang3/time/FastDateParserTest.java (revision 455610af95f3bf5f4bc8a9eda520f57e389a4c42)
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 
Expected1806(final TimeZone zone, final String one, final String two, final String three, final boolean hasHalfHourOffset)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 
checkParse(final Locale locale, final Calendar cal, final SimpleDateFormat simpleDateFormat, final DateParser dateParser)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 
checkParse(final Locale locale, final SimpleDateFormat simpleDateFormat, final DateParser dateParser, final String formattedDate, final String originalFormattedDate)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 
dateParserParameters()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 
initializeCalendar(final TimeZone timeZone)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 
getDateInstance(final int dateStyle, final Locale locale)145     private DateParser getDateInstance(final int dateStyle, final Locale locale) {
146         return getInstance(null, FormatCache.getPatternForStyle(Integer.valueOf(dateStyle), null, locale),
147             TimeZone.getDefault(), Locale.getDefault());
148     }
149 
getEraStart(int year, final TimeZone zone, final Locale locale)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 
getInstance(final String format)170     DateParser getInstance(final String format) {
171         return getInstance(null, format, TimeZone.getDefault(), Locale.getDefault());
172     }
173 
getInstance(final String format, final Locale locale)174     DateParser getInstance(final String format, final Locale locale) {
175         return getInstance(null, format, TimeZone.getDefault(), locale);
176     }
177 
getInstance(final String format, final TimeZone timeZone)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      */
getInstance(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider, final String format, final TimeZone timeZone, final Locale locale)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)
test_Equality_Hash(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider)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
test1806()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
test1806Argument()243     public void test1806Argument() {
244         assertThrows(IllegalArgumentException.class, () -> getInstance("XXXX"));
245     }
246 
247     @ParameterizedTest
248     @MethodSource(DATE_PARSER_PARAMETERS)
testAmPm(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider)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
testDayNumberOfWeek()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)
testDayOf(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider)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
testEquals()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
testJpLocales()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)
testLANG_831(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider)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)
testLANG_832(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider)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)
testLang1121(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider)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)
testLang1380(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider)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
testLang303()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
testLang538()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)
testLang996(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider)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
testLocaleMatches()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
testLowYearPadding()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
testMilleniumBug()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)
testParseLongShort(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider)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)
testParseNumerics(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider)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
testParseOffset()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
testParses()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
testParsesKnownJava16Ea25Failure()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)
testParseZone(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider)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
testPatternMatches()585     public void testPatternMatches() {
586         final DateParser parser = getInstance(yMdHmsSZ);
587         assertEquals(yMdHmsSZ, parser.getPattern());
588     }
589 
590     @ParameterizedTest
591     @MethodSource(DATE_PARSER_PARAMETERS)
testQuotes(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider)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 
testSdfAndFdp(final TriFunction<String, TimeZone, Locale, DateParser> dbProvider, final String format, final String date, final boolean shouldFail)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
testShortDateStyleWithLocales()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)
testSpecialCharacters(final TriFunction<String, TimeZone, Locale, DateParser> dpProvider)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
testTimeZoneMatches()675     public void testTimeZoneMatches() {
676         final DateParser parser = getInstance(yMdHmsSZ, REYKJAVIK);
677         assertEquals(REYKJAVIK, parser.getTimeZone());
678     }
679 
680     @Test
testToStringContainsName()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
testTzParses()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 
validateSdfFormatFdpParseEquality(final String formatStr, final Locale locale, final TimeZone timeZone, final FastDateParser dateParser, final Date inDate, final int year, final Date csDate)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 
731