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