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