1/* 2 ******************************************************************************** 3 * Copyright (C) 2007-2015, Google, International Business Machines Corporation * 4 * and others. All Rights Reserved. * 5 ******************************************************************************** 6 */ 7 8package com.ibm.icu.dev.test.format; 9 10import java.text.ParseException; 11import java.text.ParsePosition; 12import java.util.ArrayList; 13import java.util.Arrays; 14import java.util.Date; 15import java.util.EnumSet; 16import java.util.List; 17import java.util.Locale; 18import java.util.Set; 19import java.util.TreeSet; 20import java.util.concurrent.atomic.AtomicInteger; 21import java.util.regex.Pattern; 22 23import com.ibm.icu.impl.TZDBTimeZoneNames; 24import com.ibm.icu.impl.ZoneMeta; 25import com.ibm.icu.lang.UCharacter; 26import com.ibm.icu.text.SimpleDateFormat; 27import com.ibm.icu.text.TimeZoneFormat; 28import com.ibm.icu.text.TimeZoneFormat.ParseOption; 29import com.ibm.icu.text.TimeZoneFormat.Style; 30import com.ibm.icu.text.TimeZoneFormat.TimeType; 31import com.ibm.icu.text.TimeZoneNames; 32import com.ibm.icu.text.TimeZoneNames.NameType; 33import com.ibm.icu.util.BasicTimeZone; 34import com.ibm.icu.util.Calendar; 35import com.ibm.icu.util.Output; 36import com.ibm.icu.util.SimpleTimeZone; 37import com.ibm.icu.util.TimeZone; 38import com.ibm.icu.util.TimeZone.SystemTimeZoneType; 39import com.ibm.icu.util.TimeZoneTransition; 40import com.ibm.icu.util.ULocale; 41 42public class TimeZoneFormatTest extends com.ibm.icu.dev.test.TestFmwk { 43 44 private static boolean JDKTZ = (TimeZone.getDefaultTimeZoneType() == TimeZone.TIMEZONE_JDK); 45 private static final Pattern EXCL_TZ_PATTERN = Pattern.compile(".*/Riyadh8[7-9]"); 46 47 public static void main(String[] args) throws Exception { 48 new TimeZoneFormatTest().run(args); 49 } 50 51 private static final String[] PATTERNS = { 52 "z", 53 "zzzz", 54 "Z", // equivalent to "xxxx" 55 "ZZZZ", // equivalent to "OOOO" 56 "v", 57 "vvvv", 58 "O", 59 "OOOO", 60 "X", 61 "XX", 62 "XXX", 63 "XXXX", 64 "XXXXX", 65 "x", 66 "xx", 67 "xxx", 68 "xxxx", 69 "xxxxx", 70 "V", 71 "VV", 72 "VVV", 73 "VVVV" 74 }; 75 boolean REALLY_VERBOSE_LOG = false; 76 77 /* 78 * Test case for checking if a TimeZone is properly set in the result calendar 79 * and if the result TimeZone has the expected behavior. 80 */ 81 public void TestTimeZoneRoundTrip() { 82 boolean TEST_ALL = getBooleanProperty("TimeZoneRoundTripAll", false); 83 84 TimeZone unknownZone = new SimpleTimeZone(-31415, "Etc/Unknown"); 85 int badDstOffset = -1234; 86 int badZoneOffset = -2345; 87 88 int[][] testDateData = { 89 {2007, 1, 15}, 90 {2007, 6, 15}, 91 {1990, 1, 15}, 92 {1990, 6, 15}, 93 {1960, 1, 15}, 94 {1960, 6, 15}, 95 }; 96 97 Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); 98 cal.clear(); 99 100 // Set up rule equivalency test range 101 long low, high; 102 cal.set(1900, 0, 1); 103 low = cal.getTimeInMillis(); 104 cal.set(2040, 0, 1); 105 high = cal.getTimeInMillis(); 106 107 // Set up test dates 108 Date[] DATES = new Date[testDateData.length]; 109 cal.clear(); 110 for (int i = 0; i < DATES.length; i++) { 111 cal.set(testDateData[i][0], testDateData[i][1], testDateData[i][2]); 112 DATES[i] = cal.getTime(); 113 } 114 115 // Set up test locales 116 ULocale[] LOCALES = null; 117 if (TEST_ALL || getInclusion() > 5) { 118 LOCALES = ULocale.getAvailableLocales(); 119 } else { 120 LOCALES = new ULocale[] {new ULocale("en"), new ULocale("en_CA"), new ULocale("fr"), new ULocale("zh_Hant")}; 121 } 122 123 String[] tzids; 124 if (JDKTZ) { 125 tzids = java.util.TimeZone.getAvailableIDs(); 126 } else { 127 tzids = TimeZone.getAvailableIDs(); 128 } 129 int[] inOffsets = new int[2]; 130 int[] outOffsets = new int[2]; 131 132 // Run the roundtrip test 133 for (int locidx = 0; locidx < LOCALES.length; locidx++) { 134 logln("Locale: " + LOCALES[locidx].toString()); 135 136 String localGMTString = TimeZoneFormat.getInstance(LOCALES[locidx]).formatOffsetLocalizedGMT(0); 137 138 for (int patidx = 0; patidx < PATTERNS.length; patidx++) { 139 logln(" pattern: " + PATTERNS[patidx]); 140 SimpleDateFormat sdf = new SimpleDateFormat(PATTERNS[patidx], LOCALES[locidx]); 141 142 for (int tzidx = 0; tzidx < tzids.length; tzidx++) { 143 if (EXCL_TZ_PATTERN.matcher(tzids[tzidx]).matches()) { 144 continue; 145 } 146 TimeZone tz = TimeZone.getTimeZone(tzids[tzidx]); 147 148 for (int datidx = 0; datidx < DATES.length; datidx++) { 149 // Format 150 sdf.setTimeZone(tz); 151 String tzstr = sdf.format(DATES[datidx]); 152 153 // Before parse, set unknown zone to SimpleDateFormat instance 154 // just for making sure that it does not depends on the time zone 155 // originally set. 156 sdf.setTimeZone(unknownZone); 157 158 // Parse 159 ParsePosition pos = new ParsePosition(0); 160 Calendar outcal = Calendar.getInstance(unknownZone); 161 outcal.set(Calendar.DST_OFFSET, badDstOffset); 162 outcal.set(Calendar.ZONE_OFFSET, badZoneOffset); 163 164 sdf.parse(tzstr, outcal, pos); 165 166 // Check the result 167 TimeZone outtz = outcal.getTimeZone(); 168 169 tz.getOffset(DATES[datidx].getTime(), false, inOffsets); 170 outtz.getOffset(DATES[datidx].getTime(), false, outOffsets); 171 172 if (PATTERNS[patidx].equals("V")) { 173 // Short zone ID - should support roundtrip for canonical CLDR IDs 174 String canonicalID = TimeZone.getCanonicalID(tzids[tzidx]); 175 if (!outtz.getID().equals(canonicalID)) { 176 if (outtz.getID().equals("Etc/Unknown")) { 177 // Note that some zones like Asia/Riyadh87 does not have 178 // short zone ID and "unk" is used as the fallback 179 if (REALLY_VERBOSE_LOG) { 180 logln("Canonical round trip failed (probably as expected); tz=" + tzids[tzidx] 181 + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx] 182 + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr 183 + ", outtz=" + outtz.getID()); 184 } 185 } else { 186 errln("Canonical round trip failed; tz=" + tzids[tzidx] 187 + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx] 188 + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr 189 + ", outtz=" + outtz.getID()); 190 } 191 } 192 } else if (PATTERNS[patidx].equals("VV")) { 193 // Zone ID - full roundtrip support 194 if (!outtz.getID().equals(tzids[tzidx])) { 195 errln("Zone ID round trip failed; tz=" + tzids[tzidx] 196 + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx] 197 + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr 198 + ", outtz=" + outtz.getID()); 199 } 200 } else if (PATTERNS[patidx].equals("VVV") || PATTERNS[patidx].equals("VVVV")) { 201 // Location: time zone rule must be preserved except 202 // zones not actually associated with a specific location. 203 String canonicalID = TimeZone.getCanonicalID(tzids[tzidx]); 204 if (canonicalID != null && !outtz.getID().equals(canonicalID)) { 205 // Canonical ID did not match - check the rules 206 boolean bFailure = false; 207 if ((tz instanceof BasicTimeZone) && (outtz instanceof BasicTimeZone)) { 208 boolean hasNoLocation = TimeZone.getRegion(tzids[tzidx]).equals("001"); 209 bFailure = !hasNoLocation 210 && !((BasicTimeZone)outtz).hasEquivalentTransitions(tz, low, high); 211 } 212 if (bFailure) { 213 errln("Canonical round trip failed; tz=" + tzids[tzidx] 214 + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx] 215 + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr 216 + ", outtz=" + outtz.getID()); 217 } else if (REALLY_VERBOSE_LOG) { 218 logln("Canonical round trip failed (as expected); tz=" + tzids[tzidx] 219 + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx] 220 + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr 221 + ", outtz=" + outtz.getID()); 222 } 223 } 224 } else { 225 boolean isOffsetFormat = (PATTERNS[patidx].charAt(0) == 'Z' 226 || PATTERNS[patidx].charAt(0) == 'O' 227 || PATTERNS[patidx].charAt(0) == 'X' 228 || PATTERNS[patidx].charAt(0) == 'x'); 229 boolean minutesOffset = false; 230 if (PATTERNS[patidx].charAt(0) == 'X' || PATTERNS[patidx].charAt(0) == 'x') { 231 minutesOffset = PATTERNS[patidx].length() <= 3; 232 } 233 234 if (!isOffsetFormat) { 235 // Check if localized GMT format is used as a fallback of name styles 236 int numDigits = 0; 237 for (int n = 0; n < tzstr.length(); n++) { 238 if (UCharacter.isDigit(tzstr.charAt(n))) { 239 numDigits++; 240 } 241 } 242 isOffsetFormat = (numDigits > 0); 243 } 244 245 if (isOffsetFormat || tzstr.equals(localGMTString)) { 246 // Localized GMT or ISO: total offset (raw + dst) must be preserved. 247 int inOffset = inOffsets[0] + inOffsets[1]; 248 int outOffset = outOffsets[0] + outOffsets[1]; 249 int diff = outOffset - inOffset; 250 if (minutesOffset) { 251 diff = (diff / 60000) * 60000; 252 } 253 if (diff != 0) { 254 errln("Offset round trip failed; tz=" + tzids[tzidx] 255 + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx] 256 + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr 257 + ", inOffset=" + inOffset + ", outOffset=" + outOffset); 258 } 259 } else { 260 // Specific or generic: raw offset must be preserved. 261 if (inOffsets[0] != outOffsets[0]) { 262 if (JDKTZ && tzids[tzidx].startsWith("SystemV/")) { 263 // JDK uses rule SystemV for these zones while 264 // ICU handles these zones as aliases of existing time zones 265 if (REALLY_VERBOSE_LOG) { 266 logln("Raw offset round trip failed; tz=" + tzids[tzidx] 267 + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx] 268 + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr 269 + ", inRawOffset=" + inOffsets[0] + ", outRawOffset=" + outOffsets[0]); 270 } 271 272 } else { 273 errln("Raw offset round trip failed; tz=" + tzids[tzidx] 274 + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx] 275 + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr 276 + ", inRawOffset=" + inOffsets[0] + ", outRawOffset=" + outOffsets[0]); 277 } 278 } 279 } 280 } 281 } 282 } 283 } 284 } 285 286 } 287 288 /* 289 * Test case of round trip time and text. This test case detects every canonical TimeZone's 290 * rule transition since 1900 until 2020, then check if time around each transition can 291 * round trip as expected. 292 */ 293 public void TestTimeRoundTrip() { 294 295 boolean TEST_ALL = getBooleanProperty("TimeZoneRoundTripAll", false); 296 297 int startYear, endYear; 298 299 if (TEST_ALL || getInclusion() > 5) { 300 startYear = 1900; 301 } else { 302 startYear = 1990; 303 } 304 305 Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); 306 endYear = cal.get(Calendar.YEAR) + 3; 307 308 cal.set(startYear, Calendar.JANUARY, 1); 309 final long START_TIME = cal.getTimeInMillis(); 310 311 cal.set(endYear, Calendar.JANUARY, 1); 312 final long END_TIME = cal.getTimeInMillis(); 313 314 // These patterns are ambiguous at DST->STD local time overlap 315 List<String> AMBIGUOUS_DST_DECESSION = Arrays.asList("v", "vvvv", "V", "VV", "VVV", "VVVV"); 316 317 // These patterns are ambiguous at STD->STD/DST->DST local time overlap 318 List<String> AMBIGUOUS_NEGATIVE_SHIFT = Arrays.asList("z", "zzzz", "v", "vvvv", "V", "VV", "VVV", "VVVV"); 319 320 // These patterns only support integer minutes offset 321 List<String> MINUTES_OFFSET = Arrays.asList("X", "XX", "XXX", "x", "xx", "xxx"); 322 323 // Regex pattern used for filtering zone IDs without exemplar location 324 final Pattern LOC_EXCLUSION_PATTERN = Pattern.compile("Etc/.*|SystemV/.*|.*/Riyadh8[7-9]"); 325 326 final String BASEPATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS"; 327 328 ULocale[] LOCALES = null; 329 330 // timer for performance analysis 331 long[] times = new long[PATTERNS.length]; 332 long timer; 333 334 if (TEST_ALL) { 335 // It may take about an hour for testing all locales 336 LOCALES = ULocale.getAvailableLocales(); 337 } else if (getInclusion() > 5) { 338 LOCALES = new ULocale[] { 339 new ULocale("ar_EG"), new ULocale("bg_BG"), new ULocale("ca_ES"), new ULocale("da_DK"), new ULocale("de"), 340 new ULocale("de_DE"), new ULocale("el_GR"), new ULocale("en"), new ULocale("en_AU"), new ULocale("en_CA"), 341 new ULocale("en_US"), new ULocale("es"), new ULocale("es_ES"), new ULocale("es_MX"), new ULocale("fi_FI"), 342 new ULocale("fr"), new ULocale("fr_CA"), new ULocale("fr_FR"), new ULocale("he_IL"), new ULocale("hu_HU"), 343 new ULocale("it"), new ULocale("it_IT"), new ULocale("ja"), new ULocale("ja_JP"), new ULocale("ko"), 344 new ULocale("ko_KR"), new ULocale("nb_NO"), new ULocale("nl_NL"), new ULocale("nn_NO"), new ULocale("pl_PL"), 345 new ULocale("pt"), new ULocale("pt_BR"), new ULocale("pt_PT"), new ULocale("ru_RU"), new ULocale("sv_SE"), 346 new ULocale("th_TH"), new ULocale("tr_TR"), new ULocale("zh"), new ULocale("zh_Hans"), new ULocale("zh_Hans_CN"), 347 new ULocale("zh_Hant"), new ULocale("zh_Hant_HK"), new ULocale("zh_Hant_TW") 348 }; 349 } else { 350 LOCALES = new ULocale[] { 351 new ULocale("en"), 352 }; 353 } 354 355 SimpleDateFormat sdfGMT = new SimpleDateFormat(BASEPATTERN); 356 sdfGMT.setTimeZone(TimeZone.getTimeZone("Etc/GMT")); 357 358 long testCounts = 0; 359 long[] testTimes = new long[4]; 360 boolean[] expectedRoundTrip = new boolean[4]; 361 int testLen = 0; 362 for (int locidx = 0; locidx < LOCALES.length; locidx++) { 363 logln("Locale: " + LOCALES[locidx].toString()); 364 for (int patidx = 0; patidx < PATTERNS.length; patidx++) { 365 logln(" pattern: " + PATTERNS[patidx]); 366 String pattern = BASEPATTERN + " " + PATTERNS[patidx]; 367 SimpleDateFormat sdf = new SimpleDateFormat(pattern, LOCALES[locidx]); 368 boolean minutesOffset = MINUTES_OFFSET.contains(PATTERNS[patidx]); 369 370 Set<String> ids = null; 371 if (JDKTZ) { 372 ids = new TreeSet<String>(); 373 String[] jdkIDs = java.util.TimeZone.getAvailableIDs(); 374 for (String jdkID : jdkIDs) { 375 if (EXCL_TZ_PATTERN.matcher(jdkID).matches()) { 376 continue; 377 } 378 String tmpID = TimeZone.getCanonicalID(jdkID); 379 if (tmpID != null) { 380 ids.add(tmpID); 381 } 382 } 383 } else { 384 ids = TimeZone.getAvailableIDs(SystemTimeZoneType.CANONICAL, null, null); 385 } 386 387 for (String id : ids) { 388 if (PATTERNS[patidx].equals("V")) { 389 // Some zones do not have short ID assigned, such as Asia/Riyadh87. 390 // The time roundtrip will fail for such zones with pattern "V" (short zone ID). 391 // This is expected behavior. 392 String shortZoneID = ZoneMeta.getShortID(id); 393 if (shortZoneID == null) { 394 continue; 395 } 396 } else if (PATTERNS[patidx].equals("VVV")) { 397 // Some zones are not associated with any region, such as Etc/GMT+8. 398 // The time roundtrip will fail for such zones with pattern "VVV" (exemplar location). 399 // This is expected behavior. 400 if (id.indexOf('/') < 0 || LOC_EXCLUSION_PATTERN.matcher(id).matches()) { 401 continue; 402 } 403 } 404 405 if (id.equals("Pacific/Apia") && PATTERNS[patidx].equals("vvvv") 406 && logKnownIssue("11052", "Ambiguous zone name - Samoa Time")) { 407 continue; 408 } 409 410 BasicTimeZone btz = (BasicTimeZone)TimeZone.getTimeZone(id, TimeZone.TIMEZONE_ICU); 411 TimeZone tz = TimeZone.getTimeZone(id); 412 sdf.setTimeZone(tz); 413 414 long t = START_TIME; 415 TimeZoneTransition tzt = null; 416 boolean middle = true; 417 while (t < END_TIME) { 418 if (tzt == null) { 419 testTimes[0] = t; 420 expectedRoundTrip[0] = true; 421 testLen = 1; 422 } else { 423 int fromOffset = tzt.getFrom().getRawOffset() + tzt.getFrom().getDSTSavings(); 424 int toOffset = tzt.getTo().getRawOffset() + tzt.getTo().getDSTSavings(); 425 int delta = toOffset - fromOffset; 426 if (delta < 0) { 427 boolean isDstDecession = tzt.getFrom().getDSTSavings() > 0 && tzt.getTo().getDSTSavings() == 0; 428 testTimes[0] = t + delta - 1; 429 expectedRoundTrip[0] = true; 430 testTimes[1] = t + delta; 431 expectedRoundTrip[1] = isDstDecession ? 432 !AMBIGUOUS_DST_DECESSION.contains(PATTERNS[patidx]) : 433 !AMBIGUOUS_NEGATIVE_SHIFT.contains(PATTERNS[patidx]); 434 testTimes[2] = t - 1; 435 expectedRoundTrip[2] = isDstDecession ? 436 !AMBIGUOUS_DST_DECESSION.contains(PATTERNS[patidx]) : 437 !AMBIGUOUS_NEGATIVE_SHIFT.contains(PATTERNS[patidx]); 438 testTimes[3] = t; 439 expectedRoundTrip[3] = true; 440 testLen = 4; 441 } else { 442 testTimes[0] = t - 1; 443 expectedRoundTrip[0] = true; 444 testTimes[1] = t; 445 expectedRoundTrip[1] = true; 446 testLen = 2; 447 } 448 } 449 for (int testidx = 0; testidx < testLen; testidx++) { 450 testCounts++; 451 timer = System.currentTimeMillis(); 452 String text = sdf.format(new Date(testTimes[testidx])); 453 try { 454 Date parsedDate = sdf.parse(text); 455 long restime = parsedDate.getTime(); 456 long timeDiff = restime - testTimes[testidx]; 457 boolean bTimeMatch = minutesOffset ? 458 (timeDiff/60000)*60000 == 0 : timeDiff == 0; 459 if (!bTimeMatch) { 460 StringBuffer msg = new StringBuffer(); 461 msg.append("Time round trip failed for ") 462 .append("tzid=").append(id) 463 .append(", locale=").append(LOCALES[locidx]) 464 .append(", pattern=").append(PATTERNS[patidx]) 465 .append(", text=").append(text) 466 .append(", gmt=").append(sdfGMT.format(new Date(testTimes[testidx]))) 467 .append(", time=").append(testTimes[testidx]) 468 .append(", restime=").append(restime) 469 .append(", diff=").append(timeDiff); 470 if (expectedRoundTrip[testidx] 471 && !isSpecialTimeRoundTripCase(LOCALES[locidx], id, PATTERNS[patidx], testTimes[testidx])) { 472 errln("FAIL: " + msg.toString()); 473 } else if (REALLY_VERBOSE_LOG) { 474 logln(msg.toString()); 475 } 476 } 477 } catch (ParseException pe) { 478 errln("FAIL: " + pe.getMessage() + " tzid=" + id + ", locale=" + LOCALES[locidx] + 479 ", pattern=" + PATTERNS[patidx] + ", text=" + text); 480 } 481 times[patidx] += System.currentTimeMillis() - timer; 482 } 483 tzt = btz.getNextTransition(t, false); 484 if (tzt == null) { 485 break; 486 } 487 if (middle) { 488 // Test the date in the middle of two transitions. 489 t += (tzt.getTime() - t)/2; 490 middle = false; 491 tzt = null; 492 } else { 493 t = tzt.getTime(); 494 } 495 } 496 } 497 } 498 } 499 500 long total = 0; 501 logln("### Elapsed time by patterns ###"); 502 for (int i = 0; i < PATTERNS.length; i++) { 503 logln(times[i] + "ms (" + PATTERNS[i] + ")"); 504 total += times[i]; 505 } 506 logln("Total: " + total + "ms"); 507 logln("Iteration: " + testCounts); 508 } 509 510 // Special exclusions in TestTimeZoneRoundTrip. 511 // These special cases do not round trip time as designed. 512 private boolean isSpecialTimeRoundTripCase(ULocale loc, String id, String pattern, long time) { 513 final Object[][] EXCLUSIONS = { 514 {null, "Asia/Chita", "zzzz", Long.valueOf(1414252800000L)}, 515 {null, "Asia/Chita", "vvvv", Long.valueOf(1414252800000L)}, 516 {null, "Asia/Srednekolymsk", "zzzz", Long.valueOf(1414241999999L)}, 517 {null, "Asia/Srednekolymsk", "vvvv", Long.valueOf(1414241999999L)}, 518 }; 519 boolean isExcluded = false; 520 for (Object[] excl : EXCLUSIONS) { 521 if (excl[0] == null || loc.equals((ULocale)excl[0])) { 522 if (id.equals(excl[1])) { 523 if (excl[2] == null || pattern.equals((String)excl[2])) { 524 if (excl[3] == null || ((Long)excl[3]).compareTo(time) == 0) { 525 isExcluded = true; 526 break; 527 } 528 } 529 } 530 } 531 } 532 return isExcluded; 533 } 534 535 public void TestParse() { 536 final Object[][] DATA = { 537 // text inpos locale style 538 // parseOptions expected outpos time type 539 {"Z", 0, "en_US", Style.ISO_EXTENDED_FULL, 540 null, "Etc/GMT", 1, TimeType.UNKNOWN}, 541 542 {"Z", 0, "en_US", Style.SPECIFIC_LONG, 543 null, "Etc/GMT", 1, TimeType.UNKNOWN}, 544 545 {"Zambia time", 0, "en_US", Style.ISO_EXTENDED_FULL, 546 EnumSet.of(ParseOption.ALL_STYLES), "Etc/GMT", 1, TimeType.UNKNOWN}, 547 548 {"Zambia time", 0, "en_US", Style.GENERIC_LOCATION, 549 null, "Africa/Lusaka", 11, TimeType.UNKNOWN}, 550 551 {"Zambia time", 0, "en_US", Style.ISO_BASIC_LOCAL_FULL, 552 EnumSet.of(ParseOption.ALL_STYLES), "Africa/Lusaka", 11, TimeType.UNKNOWN}, 553 554 {"+00:00", 0, "en_US", Style.ISO_EXTENDED_FULL, 555 null, "Etc/GMT", 6, TimeType.UNKNOWN}, 556 557 {"-01:30:45", 0, "en_US", Style.ISO_EXTENDED_FULL, 558 null, "GMT-01:30:45", 9, TimeType.UNKNOWN}, 559 560 {"-7", 0, "en_US", Style.ISO_BASIC_LOCAL_FULL, 561 null, "GMT-07:00", 2, TimeType.UNKNOWN}, 562 563 {"-2222", 0, "en_US", Style.ISO_BASIC_LOCAL_FULL, 564 null, "GMT-22:22", 5, TimeType.UNKNOWN}, 565 566 {"-3333", 0, "en_US", Style.ISO_BASIC_LOCAL_FULL, 567 null, "GMT-03:33", 4, TimeType.UNKNOWN}, 568 569 {"XXX+01:30YYY", 3, "en_US", Style.LOCALIZED_GMT, 570 null, "GMT+01:30", 9, TimeType.UNKNOWN}, 571 572 {"GMT0", 0, "en_US", Style.SPECIFIC_SHORT, 573 null, "Etc/GMT", 3, TimeType.UNKNOWN}, 574 575 {"EST", 0, "en_US", Style.SPECIFIC_SHORT, 576 null, "America/New_York", 3, TimeType.STANDARD}, 577 578 {"ESTx", 0, "en_US", Style.SPECIFIC_SHORT, 579 null, "America/New_York", 3, TimeType.STANDARD}, 580 581 {"EDTx", 0, "en_US", Style.SPECIFIC_SHORT, 582 null, "America/New_York", 3, TimeType.DAYLIGHT}, 583 584 {"EST", 0, "en_US", Style.SPECIFIC_LONG, 585 null, null, 0, TimeType.UNKNOWN}, 586 587 {"EST", 0, "en_US", Style.SPECIFIC_LONG, 588 EnumSet.of(ParseOption.ALL_STYLES), "America/New_York", 3, TimeType.STANDARD}, 589 590 {"EST", 0, "en_CA", Style.SPECIFIC_SHORT, 591 null, "America/Toronto", 3, TimeType.STANDARD}, 592 593 {"CST", 0, "en_US", Style.SPECIFIC_SHORT, 594 null, "America/Chicago", 3, TimeType.STANDARD}, 595 596 {"CST", 0, "en_GB", Style.SPECIFIC_SHORT, 597 null, null, 0, TimeType.UNKNOWN}, 598 599 {"CST", 0, "en_GB", Style.SPECIFIC_SHORT, 600 EnumSet.of(ParseOption.TZ_DATABASE_ABBREVIATIONS), "America/Chicago", 3, TimeType.STANDARD}, 601 602 {"--CST--", 2, "en_GB", Style.SPECIFIC_SHORT, 603 EnumSet.of(ParseOption.TZ_DATABASE_ABBREVIATIONS), "America/Chicago", 5, TimeType.STANDARD}, 604 605 {"CST", 0, "zh_CN", Style.SPECIFIC_SHORT, 606 EnumSet.of(ParseOption.TZ_DATABASE_ABBREVIATIONS), "Asia/Shanghai", 3, TimeType.STANDARD}, 607 608 {"AEST", 0, "en_AU", Style.SPECIFIC_SHORT, 609 EnumSet.of(ParseOption.TZ_DATABASE_ABBREVIATIONS), "Australia/Sydney", 4, TimeType.STANDARD}, 610 611 {"AST", 0, "ar_SA", Style.SPECIFIC_SHORT, 612 EnumSet.of(ParseOption.TZ_DATABASE_ABBREVIATIONS), "Asia/Riyadh", 3, TimeType.STANDARD}, 613 614 {"AQTST", 0, "en", Style.SPECIFIC_LONG, 615 null, null, 0, TimeType.UNKNOWN}, 616 617 {"AQTST", 0, "en", Style.SPECIFIC_LONG, 618 EnumSet.of(ParseOption.ALL_STYLES), null, 0, TimeType.UNKNOWN}, 619 620 {"AQTST", 0, "en", Style.SPECIFIC_LONG, 621 EnumSet.of(ParseOption.ALL_STYLES, ParseOption.TZ_DATABASE_ABBREVIATIONS), "Asia/Aqtobe", 5, TimeType.DAYLIGHT}, 622 }; 623 624 for (Object[] test : DATA) { 625 String text = (String)test[0]; 626 int inPos = (Integer)test[1]; 627 ULocale loc = new ULocale((String)test[2]); 628 Style style = (Style)test[3]; 629 EnumSet<ParseOption> options = (EnumSet<ParseOption>)test[4]; 630 String expID = (String)test[5]; 631 int expPos = (Integer)test[6]; 632 TimeType expType = (TimeType)test[7]; 633 634 TimeZoneFormat tzfmt = TimeZoneFormat.getInstance(loc); 635 Output<TimeType> timeType = new Output<TimeType>(TimeType.UNKNOWN); 636 ParsePosition pos = new ParsePosition(inPos); 637 TimeZone tz = tzfmt.parse(style, text, pos, options, timeType); 638 639 String errMsg = null; 640 if (tz == null) { 641 if (expID != null) { 642 errMsg = "Parse failure - expected: " + expID; 643 } 644 } else if (!tz.getID().equals(expID)) { 645 errMsg = "Time zone ID: " + tz.getID() + " - expected: " + expID; 646 } else if (pos.getIndex() != expPos) { 647 errMsg = "Parsed pos: " + pos.getIndex() + " - expected: " + expPos; 648 } else if (timeType.value != expType) { 649 errMsg = "Time type: " + timeType + " - expected: " + expType; 650 } 651 652 if (errMsg != null) { 653 errln("Fail: " + errMsg + " [text=" + text + ", pos=" + inPos + ", style=" + style + "]"); 654 } 655 } 656 } 657 658 public void TestISOFormat() { 659 final int[] OFFSET = { 660 0, // 0 661 999, // 0.999s 662 -59999, // -59.999s 663 60000, // 1m 664 -77777, // -1m 17.777s 665 1800000, // 30m 666 -3600000, // -1h 667 36000000, // 10h 668 -37800000, // -10h 30m 669 -37845000, // -10h 30m 45s 670 108000000, // 30h 671 }; 672 673 final String[][] ISO_STR = { 674 // 0 675 { 676 "Z", "Z", "Z", "Z", "Z", 677 "+00", "+0000", "+00:00", "+0000", "+00:00", 678 "+0000" 679 }, 680 // 999 681 { 682 "Z", "Z", "Z", "Z", "Z", 683 "+00", "+0000", "+00:00", "+0000", "+00:00", 684 "+0000" 685 }, 686 // -59999 687 { 688 "Z", "Z", "Z", "-000059", "-00:00:59", 689 "+00", "+0000", "+00:00", "-000059", "-00:00:59", 690 "-000059" 691 }, 692 // 60000 693 { 694 "+0001", "+0001", "+00:01", "+0001", "+00:01", 695 "+0001", "+0001", "+00:01", "+0001", "+00:01", 696 "+0001" 697 }, 698 // -77777 699 { 700 "-0001", "-0001", "-00:01", "-000117", "-00:01:17", 701 "-0001", "-0001", "-00:01", "-000117", "-00:01:17", 702 "-000117" 703 }, 704 // 1800000 705 { 706 "+0030", "+0030", "+00:30", "+0030", "+00:30", 707 "+0030", "+0030", "+00:30", "+0030", "+00:30", 708 "+0030" 709 }, 710 // -3600000 711 { 712 "-01", "-0100", "-01:00", "-0100", "-01:00", 713 "-01", "-0100", "-01:00", "-0100", "-01:00", 714 "-0100" 715 }, 716 // 36000000 717 { 718 "+10", "+1000", "+10:00", "+1000", "+10:00", 719 "+10", "+1000", "+10:00", "+1000", "+10:00", 720 "+1000" 721 }, 722 // -37800000 723 { 724 "-1030", "-1030", "-10:30", "-1030", "-10:30", 725 "-1030", "-1030", "-10:30", "-1030", "-10:30", 726 "-1030" 727 }, 728 // -37845000 729 { 730 "-1030", "-1030", "-10:30", "-103045", "-10:30:45", 731 "-1030", "-1030", "-10:30", "-103045", "-10:30:45", 732 "-103045" 733 }, 734 // 108000000 735 { 736 null, null, null, null, null, 737 null, null, null, null, null, 738 null 739 } 740 }; 741 742 final String[] PATTERN = { 743 "X", "XX", "XXX", "XXXX", "XXXXX", "x", "xx", "xxx", "xxxx", "xxxxx", 744 "Z", // equivalent to "xxxx" 745 }; 746 747 final int[] MIN_OFFSET_UNIT = { 748 60000, 60000, 60000, 1000, 1000, 60000, 60000, 60000, 1000, 1000, 749 1000, 750 }; 751 752 // Formatting 753 SimpleDateFormat sdf = new SimpleDateFormat(); 754 Date d = new Date(); 755 756 for (int i = 0; i < OFFSET.length; i++) { 757 SimpleTimeZone tz = new SimpleTimeZone(OFFSET[i], "Zone Offset:" + String.valueOf(OFFSET[i]) + "ms"); 758 sdf.setTimeZone(tz); 759 for (int j = 0; j < PATTERN.length; j++) { 760 sdf.applyPattern(PATTERN[j]); 761 try { 762 String result = sdf.format(d); 763 if (!result.equals(ISO_STR[i][j])) { 764 errln("FAIL: pattern=" + PATTERN[j] + ", offset=" + OFFSET[i] + " -> " 765 + result + " (expected: " + ISO_STR[i][j] + ")"); 766 } 767 } catch (IllegalArgumentException e) { 768 if (ISO_STR[i][j] != null) { 769 errln("FAIL: IAE thrown for pattern=" + PATTERN[j] + ", offset=" + OFFSET[i] 770 + " (expected: " + ISO_STR[i][j] + ")"); 771 } 772 } 773 } 774 } 775 776 // Parsing 777 SimpleTimeZone bogusTZ = new SimpleTimeZone(-1, "Zone Offset: -1ms"); 778 for (int i = 0; i < ISO_STR.length; i++) { 779 for (int j = 0; j < ISO_STR[i].length; j++) { 780 if (ISO_STR[i][j] == null) { 781 continue; 782 } 783 ParsePosition pos = new ParsePosition(0); 784 Calendar outcal = Calendar.getInstance(bogusTZ); 785 sdf.applyPattern(PATTERN[j]); 786 787 sdf.parse(ISO_STR[i][j], outcal, pos); 788 789 if (pos.getIndex() != ISO_STR[i][j].length()) { 790 errln("FAIL: Failed to parse the entire input string: " + ISO_STR[i][j]); 791 continue; 792 } 793 794 TimeZone outtz = outcal.getTimeZone(); 795 int outOffset = outtz.getRawOffset(); 796 int adjustedOffset = OFFSET[i] / MIN_OFFSET_UNIT[j] * MIN_OFFSET_UNIT[j]; 797 798 if (outOffset != adjustedOffset) { 799 errln("FAIL: Incorrect offset:" + outOffset + "ms for input string: " + ISO_STR[i][j] 800 + " (expected:" + adjustedOffset + "ms)"); 801 } 802 } 803 } 804 } 805 806 public void TestFormat() { 807 final Date dateJan = new Date(1358208000000L); // 2013-01-15T00:00:00Z 808 final Date dateJul = new Date(1373846400000L); // 2013-07-15T00:00:00Z 809 810 final Object[][] TESTDATA = { 811 { 812 "en", 813 "America/Los_Angeles", 814 dateJan, 815 Style.GENERIC_LOCATION, 816 "Los Angeles Time", 817 TimeType.UNKNOWN 818 }, 819 { 820 "en", 821 "America/Los_Angeles", 822 dateJan, 823 Style.GENERIC_LONG, 824 "Pacific Time", 825 TimeType.UNKNOWN 826 }, 827 { 828 "en", 829 "America/Los_Angeles", 830 dateJan, 831 Style.SPECIFIC_LONG, 832 "Pacific Standard Time", 833 TimeType.STANDARD 834 }, 835 { 836 "en", 837 "America/Los_Angeles", 838 dateJul, 839 Style.SPECIFIC_LONG, 840 "Pacific Daylight Time", 841 TimeType.DAYLIGHT 842 }, 843 { 844 "ja", 845 "America/Los_Angeles", 846 dateJan, 847 Style.ZONE_ID, 848 "America/Los_Angeles", 849 TimeType.UNKNOWN 850 }, 851 { 852 "fr", 853 "America/Los_Angeles", 854 dateJul, 855 Style.ZONE_ID_SHORT, 856 "uslax", 857 TimeType.UNKNOWN 858 }, 859 { 860 "en", 861 "America/Los_Angeles", 862 dateJan, 863 Style.EXEMPLAR_LOCATION, 864 "Los Angeles", 865 TimeType.UNKNOWN 866 }, 867 { 868 "ja", 869 "Asia/Tokyo", 870 dateJan, 871 Style.GENERIC_LONG, 872 "\u65E5\u672C\u6A19\u6E96\u6642", // "日本標準時" 873 TimeType.UNKNOWN 874 }, 875 }; 876 877 for (Object[] testCase : TESTDATA) { 878 TimeZone tz = TimeZone.getTimeZone((String)testCase[1]); 879 Output<TimeType> timeType = new Output<TimeType>(); 880 881 ULocale uloc = new ULocale((String)testCase[0]); 882 TimeZoneFormat tzfmt = TimeZoneFormat.getInstance(uloc); 883 String out = tzfmt.format((Style)testCase[3], tz, ((Date)testCase[2]).getTime(), timeType); 884 885 if (!out.equals((String)testCase[4]) || timeType.value != testCase[5]) { 886 errln("Format result for [locale=" + testCase[0] + ",tzid=" + testCase[1] + ",date=" + testCase[2] 887 + ",style=" + testCase[3] + "]: expected [output=" + testCase[4] + ",type=" + testCase[5] 888 + "]; actual [output=" + out + ",type=" + timeType.value + "]"); 889 } 890 891 // with equivalent Java Locale 892 Locale loc = uloc.toLocale(); 893 tzfmt = TimeZoneFormat.getInstance(loc); 894 out = tzfmt.format((Style)testCase[3], tz, ((Date)testCase[2]).getTime(), timeType); 895 896 if (!out.equals((String)testCase[4]) || timeType.value != testCase[5]) { 897 errln("Format result for [locale(Java)=" + testCase[0] + ",tzid=" + testCase[1] + ",date=" + testCase[2] 898 + ",style=" + testCase[3] + "]: expected [output=" + testCase[4] + ",type=" + testCase[5] 899 + "]; actual [output=" + out + ",type=" + timeType.value + "]"); 900 } 901 } 902 } 903 904 public void TestFormatTZDBNames() { 905 final Date dateJan = new Date(1358208000000L); // 2013-01-15T00:00:00Z 906 final Date dateJul = new Date(1373846400000L); // 2013-07-15T00:00:00Z 907 908 final Object[][] TESTDATA = { 909 { 910 "en", 911 "America/Chicago", 912 dateJan, 913 Style.SPECIFIC_SHORT, 914 "CST", 915 TimeType.STANDARD 916 }, 917 { 918 "en", 919 "Asia/Shanghai", 920 dateJan, 921 Style.SPECIFIC_SHORT, 922 "CST", 923 TimeType.STANDARD 924 }, 925 { 926 "zh_Hans", 927 "Asia/Shanghai", 928 dateJan, 929 Style.SPECIFIC_SHORT, 930 "CST", 931 TimeType.STANDARD 932 }, 933 { 934 "en", 935 "America/Los_Angeles", 936 dateJul, 937 Style.SPECIFIC_LONG, 938 "GMT-07:00", // No long display names 939 TimeType.DAYLIGHT 940 }, 941 { 942 "ja", 943 "America/Los_Angeles", 944 dateJul, 945 Style.SPECIFIC_SHORT, 946 "PDT", 947 TimeType.DAYLIGHT 948 }, 949 { 950 "en", 951 "Australia/Sydney", 952 dateJan, 953 Style.SPECIFIC_SHORT, 954 "AEDT", 955 TimeType.DAYLIGHT 956 }, 957 { 958 "en", 959 "Australia/Sydney", 960 dateJul, 961 Style.SPECIFIC_SHORT, 962 "AEST", 963 TimeType.STANDARD 964 }, 965 }; 966 967 for (Object[] testCase : TESTDATA) { 968 ULocale loc = new ULocale((String)testCase[0]); 969 TimeZoneFormat tzfmt = TimeZoneFormat.getInstance(loc).cloneAsThawed(); 970 TimeZoneNames tzdbNames = TimeZoneNames.getTZDBInstance(loc); 971 tzfmt.setTimeZoneNames(tzdbNames); 972 973 TimeZone tz = TimeZone.getTimeZone((String)testCase[1]); 974 Output<TimeType> timeType = new Output<TimeType>(); 975 String out = tzfmt.format((Style)testCase[3], tz, ((Date)testCase[2]).getTime(), timeType); 976 977 if (!out.equals((String)testCase[4]) || timeType.value != testCase[5]) { 978 errln("Format result for [locale=" + testCase[0] + ",tzid=" + testCase[1] + ",date=" + testCase[2] 979 + ",style=" + testCase[3] + "]: expected [output=" + testCase[4] + ",type=" + testCase[5] 980 + "]; actual [output=" + out + ",type=" + timeType.value + "]"); 981 } 982 } 983 } 984 985 // This is a test case of Ticket#11487. 986 // Because the problem is reproduced for the very first time, 987 // the reported problem cannot be reproduced with regular test 988 // execution. Run this test alone reproduced the problem before 989 // the fix was merged. 990 public void TestTZDBNamesThreading() { 991 final TZDBTimeZoneNames names = new TZDBTimeZoneNames(ULocale.ENGLISH); 992 final AtomicInteger found = new AtomicInteger(); 993 List<Thread> threads = new ArrayList<Thread>(); 994 final int numIteration = 1000; 995 996 try { 997 for (int i = 0; i < numIteration; i++) { 998 Thread thread = new Thread() { 999 @Override 1000 public void run() { 1001 int resultSize = names.find("GMT", 0, EnumSet.allOf(NameType.class)).size(); 1002 if (resultSize > 0) { 1003 found.incrementAndGet(); 1004 } 1005 } 1006 }; 1007 thread.start(); 1008 threads.add(thread); 1009 } 1010 1011 for(Thread thread: threads) { 1012 thread.join(); 1013 } 1014 } catch (Throwable t) { 1015 errln(t.toString()); 1016 } 1017 1018 if (found.intValue() != numIteration) { 1019 errln("Incorrect count: " + found.toString() + ", expected: " + numIteration); 1020 } 1021 } 1022}