/* ******************************************************************************** * Copyright (C) 2007-2015, Google, International Business Machines Corporation * * and others. All Rights Reserved. * ******************************************************************************** */ package com.ibm.icu.dev.test.format; import java.text.ParseException; import java.text.ParsePosition; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.EnumSet; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Pattern; import com.ibm.icu.impl.TZDBTimeZoneNames; import com.ibm.icu.impl.ZoneMeta; import com.ibm.icu.lang.UCharacter; import com.ibm.icu.text.SimpleDateFormat; import com.ibm.icu.text.TimeZoneFormat; import com.ibm.icu.text.TimeZoneFormat.ParseOption; import com.ibm.icu.text.TimeZoneFormat.Style; import com.ibm.icu.text.TimeZoneFormat.TimeType; import com.ibm.icu.text.TimeZoneNames; import com.ibm.icu.text.TimeZoneNames.NameType; import com.ibm.icu.util.BasicTimeZone; import com.ibm.icu.util.Calendar; import com.ibm.icu.util.Output; import com.ibm.icu.util.SimpleTimeZone; import com.ibm.icu.util.TimeZone; import com.ibm.icu.util.TimeZone.SystemTimeZoneType; import com.ibm.icu.util.TimeZoneTransition; import com.ibm.icu.util.ULocale; public class TimeZoneFormatTest extends com.ibm.icu.dev.test.TestFmwk { private static boolean JDKTZ = (TimeZone.getDefaultTimeZoneType() == TimeZone.TIMEZONE_JDK); private static final Pattern EXCL_TZ_PATTERN = Pattern.compile(".*/Riyadh8[7-9]"); public static void main(String[] args) throws Exception { new TimeZoneFormatTest().run(args); } private static final String[] PATTERNS = { "z", "zzzz", "Z", // equivalent to "xxxx" "ZZZZ", // equivalent to "OOOO" "v", "vvvv", "O", "OOOO", "X", "XX", "XXX", "XXXX", "XXXXX", "x", "xx", "xxx", "xxxx", "xxxxx", "V", "VV", "VVV", "VVVV" }; boolean REALLY_VERBOSE_LOG = false; /* * Test case for checking if a TimeZone is properly set in the result calendar * and if the result TimeZone has the expected behavior. */ public void TestTimeZoneRoundTrip() { boolean TEST_ALL = getBooleanProperty("TimeZoneRoundTripAll", false); TimeZone unknownZone = new SimpleTimeZone(-31415, "Etc/Unknown"); int badDstOffset = -1234; int badZoneOffset = -2345; int[][] testDateData = { {2007, 1, 15}, {2007, 6, 15}, {1990, 1, 15}, {1990, 6, 15}, {1960, 1, 15}, {1960, 6, 15}, }; Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); cal.clear(); // Set up rule equivalency test range long low, high; cal.set(1900, 0, 1); low = cal.getTimeInMillis(); cal.set(2040, 0, 1); high = cal.getTimeInMillis(); // Set up test dates Date[] DATES = new Date[testDateData.length]; cal.clear(); for (int i = 0; i < DATES.length; i++) { cal.set(testDateData[i][0], testDateData[i][1], testDateData[i][2]); DATES[i] = cal.getTime(); } // Set up test locales ULocale[] LOCALES = null; if (TEST_ALL || getInclusion() > 5) { LOCALES = ULocale.getAvailableLocales(); } else { LOCALES = new ULocale[] {new ULocale("en"), new ULocale("en_CA"), new ULocale("fr"), new ULocale("zh_Hant")}; } String[] tzids; if (JDKTZ) { tzids = java.util.TimeZone.getAvailableIDs(); } else { tzids = TimeZone.getAvailableIDs(); } int[] inOffsets = new int[2]; int[] outOffsets = new int[2]; // Run the roundtrip test for (int locidx = 0; locidx < LOCALES.length; locidx++) { logln("Locale: " + LOCALES[locidx].toString()); String localGMTString = TimeZoneFormat.getInstance(LOCALES[locidx]).formatOffsetLocalizedGMT(0); for (int patidx = 0; patidx < PATTERNS.length; patidx++) { logln(" pattern: " + PATTERNS[patidx]); SimpleDateFormat sdf = new SimpleDateFormat(PATTERNS[patidx], LOCALES[locidx]); for (int tzidx = 0; tzidx < tzids.length; tzidx++) { if (EXCL_TZ_PATTERN.matcher(tzids[tzidx]).matches()) { continue; } TimeZone tz = TimeZone.getTimeZone(tzids[tzidx]); for (int datidx = 0; datidx < DATES.length; datidx++) { // Format sdf.setTimeZone(tz); String tzstr = sdf.format(DATES[datidx]); // Before parse, set unknown zone to SimpleDateFormat instance // just for making sure that it does not depends on the time zone // originally set. sdf.setTimeZone(unknownZone); // Parse ParsePosition pos = new ParsePosition(0); Calendar outcal = Calendar.getInstance(unknownZone); outcal.set(Calendar.DST_OFFSET, badDstOffset); outcal.set(Calendar.ZONE_OFFSET, badZoneOffset); sdf.parse(tzstr, outcal, pos); // Check the result TimeZone outtz = outcal.getTimeZone(); tz.getOffset(DATES[datidx].getTime(), false, inOffsets); outtz.getOffset(DATES[datidx].getTime(), false, outOffsets); if (PATTERNS[patidx].equals("V")) { // Short zone ID - should support roundtrip for canonical CLDR IDs String canonicalID = TimeZone.getCanonicalID(tzids[tzidx]); if (!outtz.getID().equals(canonicalID)) { if (outtz.getID().equals("Etc/Unknown")) { // Note that some zones like Asia/Riyadh87 does not have // short zone ID and "unk" is used as the fallback if (REALLY_VERBOSE_LOG) { logln("Canonical round trip failed (probably as expected); tz=" + tzids[tzidx] + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx] + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr + ", outtz=" + outtz.getID()); } } else { errln("Canonical round trip failed; tz=" + tzids[tzidx] + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx] + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr + ", outtz=" + outtz.getID()); } } } else if (PATTERNS[patidx].equals("VV")) { // Zone ID - full roundtrip support if (!outtz.getID().equals(tzids[tzidx])) { errln("Zone ID round trip failed; tz=" + tzids[tzidx] + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx] + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr + ", outtz=" + outtz.getID()); } } else if (PATTERNS[patidx].equals("VVV") || PATTERNS[patidx].equals("VVVV")) { // Location: time zone rule must be preserved except // zones not actually associated with a specific location. String canonicalID = TimeZone.getCanonicalID(tzids[tzidx]); if (canonicalID != null && !outtz.getID().equals(canonicalID)) { // Canonical ID did not match - check the rules boolean bFailure = false; if ((tz instanceof BasicTimeZone) && (outtz instanceof BasicTimeZone)) { boolean hasNoLocation = TimeZone.getRegion(tzids[tzidx]).equals("001"); bFailure = !hasNoLocation && !((BasicTimeZone)outtz).hasEquivalentTransitions(tz, low, high); } if (bFailure) { errln("Canonical round trip failed; tz=" + tzids[tzidx] + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx] + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr + ", outtz=" + outtz.getID()); } else if (REALLY_VERBOSE_LOG) { logln("Canonical round trip failed (as expected); tz=" + tzids[tzidx] + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx] + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr + ", outtz=" + outtz.getID()); } } } else { boolean isOffsetFormat = (PATTERNS[patidx].charAt(0) == 'Z' || PATTERNS[patidx].charAt(0) == 'O' || PATTERNS[patidx].charAt(0) == 'X' || PATTERNS[patidx].charAt(0) == 'x'); boolean minutesOffset = false; if (PATTERNS[patidx].charAt(0) == 'X' || PATTERNS[patidx].charAt(0) == 'x') { minutesOffset = PATTERNS[patidx].length() <= 3; } if (!isOffsetFormat) { // Check if localized GMT format is used as a fallback of name styles int numDigits = 0; for (int n = 0; n < tzstr.length(); n++) { if (UCharacter.isDigit(tzstr.charAt(n))) { numDigits++; } } isOffsetFormat = (numDigits > 0); } if (isOffsetFormat || tzstr.equals(localGMTString)) { // Localized GMT or ISO: total offset (raw + dst) must be preserved. int inOffset = inOffsets[0] + inOffsets[1]; int outOffset = outOffsets[0] + outOffsets[1]; int diff = outOffset - inOffset; if (minutesOffset) { diff = (diff / 60000) * 60000; } if (diff != 0) { errln("Offset round trip failed; tz=" + tzids[tzidx] + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx] + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr + ", inOffset=" + inOffset + ", outOffset=" + outOffset); } } else { // Specific or generic: raw offset must be preserved. if (inOffsets[0] != outOffsets[0]) { if (JDKTZ && tzids[tzidx].startsWith("SystemV/")) { // JDK uses rule SystemV for these zones while // ICU handles these zones as aliases of existing time zones if (REALLY_VERBOSE_LOG) { logln("Raw offset round trip failed; tz=" + tzids[tzidx] + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx] + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr + ", inRawOffset=" + inOffsets[0] + ", outRawOffset=" + outOffsets[0]); } } else { errln("Raw offset round trip failed; tz=" + tzids[tzidx] + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx] + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr + ", inRawOffset=" + inOffsets[0] + ", outRawOffset=" + outOffsets[0]); } } } } } } } } } /* * Test case of round trip time and text. This test case detects every canonical TimeZone's * rule transition since 1900 until 2020, then check if time around each transition can * round trip as expected. */ public void TestTimeRoundTrip() { boolean TEST_ALL = getBooleanProperty("TimeZoneRoundTripAll", false); int startYear, endYear; if (TEST_ALL || getInclusion() > 5) { startYear = 1900; } else { startYear = 1990; } Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); endYear = cal.get(Calendar.YEAR) + 3; cal.set(startYear, Calendar.JANUARY, 1); final long START_TIME = cal.getTimeInMillis(); cal.set(endYear, Calendar.JANUARY, 1); final long END_TIME = cal.getTimeInMillis(); // These patterns are ambiguous at DST->STD local time overlap List AMBIGUOUS_DST_DECESSION = Arrays.asList("v", "vvvv", "V", "VV", "VVV", "VVVV"); // These patterns are ambiguous at STD->STD/DST->DST local time overlap List AMBIGUOUS_NEGATIVE_SHIFT = Arrays.asList("z", "zzzz", "v", "vvvv", "V", "VV", "VVV", "VVVV"); // These patterns only support integer minutes offset List MINUTES_OFFSET = Arrays.asList("X", "XX", "XXX", "x", "xx", "xxx"); // Regex pattern used for filtering zone IDs without exemplar location final Pattern LOC_EXCLUSION_PATTERN = Pattern.compile("Etc/.*|SystemV/.*|.*/Riyadh8[7-9]"); final String BASEPATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS"; ULocale[] LOCALES = null; // timer for performance analysis long[] times = new long[PATTERNS.length]; long timer; if (TEST_ALL) { // It may take about an hour for testing all locales LOCALES = ULocale.getAvailableLocales(); } else if (getInclusion() > 5) { LOCALES = new ULocale[] { new ULocale("ar_EG"), new ULocale("bg_BG"), new ULocale("ca_ES"), new ULocale("da_DK"), new ULocale("de"), new ULocale("de_DE"), new ULocale("el_GR"), new ULocale("en"), new ULocale("en_AU"), new ULocale("en_CA"), new ULocale("en_US"), new ULocale("es"), new ULocale("es_ES"), new ULocale("es_MX"), new ULocale("fi_FI"), new ULocale("fr"), new ULocale("fr_CA"), new ULocale("fr_FR"), new ULocale("he_IL"), new ULocale("hu_HU"), new ULocale("it"), new ULocale("it_IT"), new ULocale("ja"), new ULocale("ja_JP"), new ULocale("ko"), new ULocale("ko_KR"), new ULocale("nb_NO"), new ULocale("nl_NL"), new ULocale("nn_NO"), new ULocale("pl_PL"), new ULocale("pt"), new ULocale("pt_BR"), new ULocale("pt_PT"), new ULocale("ru_RU"), new ULocale("sv_SE"), new ULocale("th_TH"), new ULocale("tr_TR"), new ULocale("zh"), new ULocale("zh_Hans"), new ULocale("zh_Hans_CN"), new ULocale("zh_Hant"), new ULocale("zh_Hant_HK"), new ULocale("zh_Hant_TW") }; } else { LOCALES = new ULocale[] { new ULocale("en"), }; } SimpleDateFormat sdfGMT = new SimpleDateFormat(BASEPATTERN); sdfGMT.setTimeZone(TimeZone.getTimeZone("Etc/GMT")); long testCounts = 0; long[] testTimes = new long[4]; boolean[] expectedRoundTrip = new boolean[4]; int testLen = 0; for (int locidx = 0; locidx < LOCALES.length; locidx++) { logln("Locale: " + LOCALES[locidx].toString()); for (int patidx = 0; patidx < PATTERNS.length; patidx++) { logln(" pattern: " + PATTERNS[patidx]); String pattern = BASEPATTERN + " " + PATTERNS[patidx]; SimpleDateFormat sdf = new SimpleDateFormat(pattern, LOCALES[locidx]); boolean minutesOffset = MINUTES_OFFSET.contains(PATTERNS[patidx]); Set ids = null; if (JDKTZ) { ids = new TreeSet(); String[] jdkIDs = java.util.TimeZone.getAvailableIDs(); for (String jdkID : jdkIDs) { if (EXCL_TZ_PATTERN.matcher(jdkID).matches()) { continue; } String tmpID = TimeZone.getCanonicalID(jdkID); if (tmpID != null) { ids.add(tmpID); } } } else { ids = TimeZone.getAvailableIDs(SystemTimeZoneType.CANONICAL, null, null); } for (String id : ids) { if (PATTERNS[patidx].equals("V")) { // Some zones do not have short ID assigned, such as Asia/Riyadh87. // The time roundtrip will fail for such zones with pattern "V" (short zone ID). // This is expected behavior. String shortZoneID = ZoneMeta.getShortID(id); if (shortZoneID == null) { continue; } } else if (PATTERNS[patidx].equals("VVV")) { // Some zones are not associated with any region, such as Etc/GMT+8. // The time roundtrip will fail for such zones with pattern "VVV" (exemplar location). // This is expected behavior. if (id.indexOf('/') < 0 || LOC_EXCLUSION_PATTERN.matcher(id).matches()) { continue; } } if (id.equals("Pacific/Apia") && PATTERNS[patidx].equals("vvvv") && logKnownIssue("11052", "Ambiguous zone name - Samoa Time")) { continue; } BasicTimeZone btz = (BasicTimeZone)TimeZone.getTimeZone(id, TimeZone.TIMEZONE_ICU); TimeZone tz = TimeZone.getTimeZone(id); sdf.setTimeZone(tz); long t = START_TIME; TimeZoneTransition tzt = null; boolean middle = true; while (t < END_TIME) { if (tzt == null) { testTimes[0] = t; expectedRoundTrip[0] = true; testLen = 1; } else { int fromOffset = tzt.getFrom().getRawOffset() + tzt.getFrom().getDSTSavings(); int toOffset = tzt.getTo().getRawOffset() + tzt.getTo().getDSTSavings(); int delta = toOffset - fromOffset; if (delta < 0) { boolean isDstDecession = tzt.getFrom().getDSTSavings() > 0 && tzt.getTo().getDSTSavings() == 0; testTimes[0] = t + delta - 1; expectedRoundTrip[0] = true; testTimes[1] = t + delta; expectedRoundTrip[1] = isDstDecession ? !AMBIGUOUS_DST_DECESSION.contains(PATTERNS[patidx]) : !AMBIGUOUS_NEGATIVE_SHIFT.contains(PATTERNS[patidx]); testTimes[2] = t - 1; expectedRoundTrip[2] = isDstDecession ? !AMBIGUOUS_DST_DECESSION.contains(PATTERNS[patidx]) : !AMBIGUOUS_NEGATIVE_SHIFT.contains(PATTERNS[patidx]); testTimes[3] = t; expectedRoundTrip[3] = true; testLen = 4; } else { testTimes[0] = t - 1; expectedRoundTrip[0] = true; testTimes[1] = t; expectedRoundTrip[1] = true; testLen = 2; } } for (int testidx = 0; testidx < testLen; testidx++) { testCounts++; timer = System.currentTimeMillis(); String text = sdf.format(new Date(testTimes[testidx])); try { Date parsedDate = sdf.parse(text); long restime = parsedDate.getTime(); long timeDiff = restime - testTimes[testidx]; boolean bTimeMatch = minutesOffset ? (timeDiff/60000)*60000 == 0 : timeDiff == 0; if (!bTimeMatch) { StringBuffer msg = new StringBuffer(); msg.append("Time round trip failed for ") .append("tzid=").append(id) .append(", locale=").append(LOCALES[locidx]) .append(", pattern=").append(PATTERNS[patidx]) .append(", text=").append(text) .append(", gmt=").append(sdfGMT.format(new Date(testTimes[testidx]))) .append(", time=").append(testTimes[testidx]) .append(", restime=").append(restime) .append(", diff=").append(timeDiff); if (expectedRoundTrip[testidx] && !isSpecialTimeRoundTripCase(LOCALES[locidx], id, PATTERNS[patidx], testTimes[testidx])) { errln("FAIL: " + msg.toString()); } else if (REALLY_VERBOSE_LOG) { logln(msg.toString()); } } } catch (ParseException pe) { errln("FAIL: " + pe.getMessage() + " tzid=" + id + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx] + ", text=" + text); } times[patidx] += System.currentTimeMillis() - timer; } tzt = btz.getNextTransition(t, false); if (tzt == null) { break; } if (middle) { // Test the date in the middle of two transitions. t += (tzt.getTime() - t)/2; middle = false; tzt = null; } else { t = tzt.getTime(); } } } } } long total = 0; logln("### Elapsed time by patterns ###"); for (int i = 0; i < PATTERNS.length; i++) { logln(times[i] + "ms (" + PATTERNS[i] + ")"); total += times[i]; } logln("Total: " + total + "ms"); logln("Iteration: " + testCounts); } // Special exclusions in TestTimeZoneRoundTrip. // These special cases do not round trip time as designed. private boolean isSpecialTimeRoundTripCase(ULocale loc, String id, String pattern, long time) { final Object[][] EXCLUSIONS = { {null, "Asia/Chita", "zzzz", Long.valueOf(1414252800000L)}, {null, "Asia/Chita", "vvvv", Long.valueOf(1414252800000L)}, {null, "Asia/Srednekolymsk", "zzzz", Long.valueOf(1414241999999L)}, {null, "Asia/Srednekolymsk", "vvvv", Long.valueOf(1414241999999L)}, }; boolean isExcluded = false; for (Object[] excl : EXCLUSIONS) { if (excl[0] == null || loc.equals((ULocale)excl[0])) { if (id.equals(excl[1])) { if (excl[2] == null || pattern.equals((String)excl[2])) { if (excl[3] == null || ((Long)excl[3]).compareTo(time) == 0) { isExcluded = true; break; } } } } } return isExcluded; } public void TestParse() { final Object[][] DATA = { // text inpos locale style // parseOptions expected outpos time type {"Z", 0, "en_US", Style.ISO_EXTENDED_FULL, null, "Etc/GMT", 1, TimeType.UNKNOWN}, {"Z", 0, "en_US", Style.SPECIFIC_LONG, null, "Etc/GMT", 1, TimeType.UNKNOWN}, {"Zambia time", 0, "en_US", Style.ISO_EXTENDED_FULL, EnumSet.of(ParseOption.ALL_STYLES), "Etc/GMT", 1, TimeType.UNKNOWN}, {"Zambia time", 0, "en_US", Style.GENERIC_LOCATION, null, "Africa/Lusaka", 11, TimeType.UNKNOWN}, {"Zambia time", 0, "en_US", Style.ISO_BASIC_LOCAL_FULL, EnumSet.of(ParseOption.ALL_STYLES), "Africa/Lusaka", 11, TimeType.UNKNOWN}, {"+00:00", 0, "en_US", Style.ISO_EXTENDED_FULL, null, "Etc/GMT", 6, TimeType.UNKNOWN}, {"-01:30:45", 0, "en_US", Style.ISO_EXTENDED_FULL, null, "GMT-01:30:45", 9, TimeType.UNKNOWN}, {"-7", 0, "en_US", Style.ISO_BASIC_LOCAL_FULL, null, "GMT-07:00", 2, TimeType.UNKNOWN}, {"-2222", 0, "en_US", Style.ISO_BASIC_LOCAL_FULL, null, "GMT-22:22", 5, TimeType.UNKNOWN}, {"-3333", 0, "en_US", Style.ISO_BASIC_LOCAL_FULL, null, "GMT-03:33", 4, TimeType.UNKNOWN}, {"XXX+01:30YYY", 3, "en_US", Style.LOCALIZED_GMT, null, "GMT+01:30", 9, TimeType.UNKNOWN}, {"GMT0", 0, "en_US", Style.SPECIFIC_SHORT, null, "Etc/GMT", 3, TimeType.UNKNOWN}, {"EST", 0, "en_US", Style.SPECIFIC_SHORT, null, "America/New_York", 3, TimeType.STANDARD}, {"ESTx", 0, "en_US", Style.SPECIFIC_SHORT, null, "America/New_York", 3, TimeType.STANDARD}, {"EDTx", 0, "en_US", Style.SPECIFIC_SHORT, null, "America/New_York", 3, TimeType.DAYLIGHT}, {"EST", 0, "en_US", Style.SPECIFIC_LONG, null, null, 0, TimeType.UNKNOWN}, {"EST", 0, "en_US", Style.SPECIFIC_LONG, EnumSet.of(ParseOption.ALL_STYLES), "America/New_York", 3, TimeType.STANDARD}, {"EST", 0, "en_CA", Style.SPECIFIC_SHORT, null, "America/Toronto", 3, TimeType.STANDARD}, {"CST", 0, "en_US", Style.SPECIFIC_SHORT, null, "America/Chicago", 3, TimeType.STANDARD}, {"CST", 0, "en_GB", Style.SPECIFIC_SHORT, null, null, 0, TimeType.UNKNOWN}, {"CST", 0, "en_GB", Style.SPECIFIC_SHORT, EnumSet.of(ParseOption.TZ_DATABASE_ABBREVIATIONS), "America/Chicago", 3, TimeType.STANDARD}, {"--CST--", 2, "en_GB", Style.SPECIFIC_SHORT, EnumSet.of(ParseOption.TZ_DATABASE_ABBREVIATIONS), "America/Chicago", 5, TimeType.STANDARD}, {"CST", 0, "zh_CN", Style.SPECIFIC_SHORT, EnumSet.of(ParseOption.TZ_DATABASE_ABBREVIATIONS), "Asia/Shanghai", 3, TimeType.STANDARD}, {"AEST", 0, "en_AU", Style.SPECIFIC_SHORT, EnumSet.of(ParseOption.TZ_DATABASE_ABBREVIATIONS), "Australia/Sydney", 4, TimeType.STANDARD}, {"AST", 0, "ar_SA", Style.SPECIFIC_SHORT, EnumSet.of(ParseOption.TZ_DATABASE_ABBREVIATIONS), "Asia/Riyadh", 3, TimeType.STANDARD}, {"AQTST", 0, "en", Style.SPECIFIC_LONG, null, null, 0, TimeType.UNKNOWN}, {"AQTST", 0, "en", Style.SPECIFIC_LONG, EnumSet.of(ParseOption.ALL_STYLES), null, 0, TimeType.UNKNOWN}, {"AQTST", 0, "en", Style.SPECIFIC_LONG, EnumSet.of(ParseOption.ALL_STYLES, ParseOption.TZ_DATABASE_ABBREVIATIONS), "Asia/Aqtobe", 5, TimeType.DAYLIGHT}, }; for (Object[] test : DATA) { String text = (String)test[0]; int inPos = (Integer)test[1]; ULocale loc = new ULocale((String)test[2]); Style style = (Style)test[3]; EnumSet options = (EnumSet)test[4]; String expID = (String)test[5]; int expPos = (Integer)test[6]; TimeType expType = (TimeType)test[7]; TimeZoneFormat tzfmt = TimeZoneFormat.getInstance(loc); Output timeType = new Output(TimeType.UNKNOWN); ParsePosition pos = new ParsePosition(inPos); TimeZone tz = tzfmt.parse(style, text, pos, options, timeType); String errMsg = null; if (tz == null) { if (expID != null) { errMsg = "Parse failure - expected: " + expID; } } else if (!tz.getID().equals(expID)) { errMsg = "Time zone ID: " + tz.getID() + " - expected: " + expID; } else if (pos.getIndex() != expPos) { errMsg = "Parsed pos: " + pos.getIndex() + " - expected: " + expPos; } else if (timeType.value != expType) { errMsg = "Time type: " + timeType + " - expected: " + expType; } if (errMsg != null) { errln("Fail: " + errMsg + " [text=" + text + ", pos=" + inPos + ", style=" + style + "]"); } } } public void TestISOFormat() { final int[] OFFSET = { 0, // 0 999, // 0.999s -59999, // -59.999s 60000, // 1m -77777, // -1m 17.777s 1800000, // 30m -3600000, // -1h 36000000, // 10h -37800000, // -10h 30m -37845000, // -10h 30m 45s 108000000, // 30h }; final String[][] ISO_STR = { // 0 { "Z", "Z", "Z", "Z", "Z", "+00", "+0000", "+00:00", "+0000", "+00:00", "+0000" }, // 999 { "Z", "Z", "Z", "Z", "Z", "+00", "+0000", "+00:00", "+0000", "+00:00", "+0000" }, // -59999 { "Z", "Z", "Z", "-000059", "-00:00:59", "+00", "+0000", "+00:00", "-000059", "-00:00:59", "-000059" }, // 60000 { "+0001", "+0001", "+00:01", "+0001", "+00:01", "+0001", "+0001", "+00:01", "+0001", "+00:01", "+0001" }, // -77777 { "-0001", "-0001", "-00:01", "-000117", "-00:01:17", "-0001", "-0001", "-00:01", "-000117", "-00:01:17", "-000117" }, // 1800000 { "+0030", "+0030", "+00:30", "+0030", "+00:30", "+0030", "+0030", "+00:30", "+0030", "+00:30", "+0030" }, // -3600000 { "-01", "-0100", "-01:00", "-0100", "-01:00", "-01", "-0100", "-01:00", "-0100", "-01:00", "-0100" }, // 36000000 { "+10", "+1000", "+10:00", "+1000", "+10:00", "+10", "+1000", "+10:00", "+1000", "+10:00", "+1000" }, // -37800000 { "-1030", "-1030", "-10:30", "-1030", "-10:30", "-1030", "-1030", "-10:30", "-1030", "-10:30", "-1030" }, // -37845000 { "-1030", "-1030", "-10:30", "-103045", "-10:30:45", "-1030", "-1030", "-10:30", "-103045", "-10:30:45", "-103045" }, // 108000000 { null, null, null, null, null, null, null, null, null, null, null } }; final String[] PATTERN = { "X", "XX", "XXX", "XXXX", "XXXXX", "x", "xx", "xxx", "xxxx", "xxxxx", "Z", // equivalent to "xxxx" }; final int[] MIN_OFFSET_UNIT = { 60000, 60000, 60000, 1000, 1000, 60000, 60000, 60000, 1000, 1000, 1000, }; // Formatting SimpleDateFormat sdf = new SimpleDateFormat(); Date d = new Date(); for (int i = 0; i < OFFSET.length; i++) { SimpleTimeZone tz = new SimpleTimeZone(OFFSET[i], "Zone Offset:" + String.valueOf(OFFSET[i]) + "ms"); sdf.setTimeZone(tz); for (int j = 0; j < PATTERN.length; j++) { sdf.applyPattern(PATTERN[j]); try { String result = sdf.format(d); if (!result.equals(ISO_STR[i][j])) { errln("FAIL: pattern=" + PATTERN[j] + ", offset=" + OFFSET[i] + " -> " + result + " (expected: " + ISO_STR[i][j] + ")"); } } catch (IllegalArgumentException e) { if (ISO_STR[i][j] != null) { errln("FAIL: IAE thrown for pattern=" + PATTERN[j] + ", offset=" + OFFSET[i] + " (expected: " + ISO_STR[i][j] + ")"); } } } } // Parsing SimpleTimeZone bogusTZ = new SimpleTimeZone(-1, "Zone Offset: -1ms"); for (int i = 0; i < ISO_STR.length; i++) { for (int j = 0; j < ISO_STR[i].length; j++) { if (ISO_STR[i][j] == null) { continue; } ParsePosition pos = new ParsePosition(0); Calendar outcal = Calendar.getInstance(bogusTZ); sdf.applyPattern(PATTERN[j]); sdf.parse(ISO_STR[i][j], outcal, pos); if (pos.getIndex() != ISO_STR[i][j].length()) { errln("FAIL: Failed to parse the entire input string: " + ISO_STR[i][j]); continue; } TimeZone outtz = outcal.getTimeZone(); int outOffset = outtz.getRawOffset(); int adjustedOffset = OFFSET[i] / MIN_OFFSET_UNIT[j] * MIN_OFFSET_UNIT[j]; if (outOffset != adjustedOffset) { errln("FAIL: Incorrect offset:" + outOffset + "ms for input string: " + ISO_STR[i][j] + " (expected:" + adjustedOffset + "ms)"); } } } } public void TestFormat() { final Date dateJan = new Date(1358208000000L); // 2013-01-15T00:00:00Z final Date dateJul = new Date(1373846400000L); // 2013-07-15T00:00:00Z final Object[][] TESTDATA = { { "en", "America/Los_Angeles", dateJan, Style.GENERIC_LOCATION, "Los Angeles Time", TimeType.UNKNOWN }, { "en", "America/Los_Angeles", dateJan, Style.GENERIC_LONG, "Pacific Time", TimeType.UNKNOWN }, { "en", "America/Los_Angeles", dateJan, Style.SPECIFIC_LONG, "Pacific Standard Time", TimeType.STANDARD }, { "en", "America/Los_Angeles", dateJul, Style.SPECIFIC_LONG, "Pacific Daylight Time", TimeType.DAYLIGHT }, { "ja", "America/Los_Angeles", dateJan, Style.ZONE_ID, "America/Los_Angeles", TimeType.UNKNOWN }, { "fr", "America/Los_Angeles", dateJul, Style.ZONE_ID_SHORT, "uslax", TimeType.UNKNOWN }, { "en", "America/Los_Angeles", dateJan, Style.EXEMPLAR_LOCATION, "Los Angeles", TimeType.UNKNOWN }, { "ja", "Asia/Tokyo", dateJan, Style.GENERIC_LONG, "\u65E5\u672C\u6A19\u6E96\u6642", // "日本標準時" TimeType.UNKNOWN }, }; for (Object[] testCase : TESTDATA) { TimeZone tz = TimeZone.getTimeZone((String)testCase[1]); Output timeType = new Output(); ULocale uloc = new ULocale((String)testCase[0]); TimeZoneFormat tzfmt = TimeZoneFormat.getInstance(uloc); String out = tzfmt.format((Style)testCase[3], tz, ((Date)testCase[2]).getTime(), timeType); if (!out.equals((String)testCase[4]) || timeType.value != testCase[5]) { errln("Format result for [locale=" + testCase[0] + ",tzid=" + testCase[1] + ",date=" + testCase[2] + ",style=" + testCase[3] + "]: expected [output=" + testCase[4] + ",type=" + testCase[5] + "]; actual [output=" + out + ",type=" + timeType.value + "]"); } // with equivalent Java Locale Locale loc = uloc.toLocale(); tzfmt = TimeZoneFormat.getInstance(loc); out = tzfmt.format((Style)testCase[3], tz, ((Date)testCase[2]).getTime(), timeType); if (!out.equals((String)testCase[4]) || timeType.value != testCase[5]) { errln("Format result for [locale(Java)=" + testCase[0] + ",tzid=" + testCase[1] + ",date=" + testCase[2] + ",style=" + testCase[3] + "]: expected [output=" + testCase[4] + ",type=" + testCase[5] + "]; actual [output=" + out + ",type=" + timeType.value + "]"); } } } public void TestFormatTZDBNames() { final Date dateJan = new Date(1358208000000L); // 2013-01-15T00:00:00Z final Date dateJul = new Date(1373846400000L); // 2013-07-15T00:00:00Z final Object[][] TESTDATA = { { "en", "America/Chicago", dateJan, Style.SPECIFIC_SHORT, "CST", TimeType.STANDARD }, { "en", "Asia/Shanghai", dateJan, Style.SPECIFIC_SHORT, "CST", TimeType.STANDARD }, { "zh_Hans", "Asia/Shanghai", dateJan, Style.SPECIFIC_SHORT, "CST", TimeType.STANDARD }, { "en", "America/Los_Angeles", dateJul, Style.SPECIFIC_LONG, "GMT-07:00", // No long display names TimeType.DAYLIGHT }, { "ja", "America/Los_Angeles", dateJul, Style.SPECIFIC_SHORT, "PDT", TimeType.DAYLIGHT }, { "en", "Australia/Sydney", dateJan, Style.SPECIFIC_SHORT, "AEDT", TimeType.DAYLIGHT }, { "en", "Australia/Sydney", dateJul, Style.SPECIFIC_SHORT, "AEST", TimeType.STANDARD }, }; for (Object[] testCase : TESTDATA) { ULocale loc = new ULocale((String)testCase[0]); TimeZoneFormat tzfmt = TimeZoneFormat.getInstance(loc).cloneAsThawed(); TimeZoneNames tzdbNames = TimeZoneNames.getTZDBInstance(loc); tzfmt.setTimeZoneNames(tzdbNames); TimeZone tz = TimeZone.getTimeZone((String)testCase[1]); Output timeType = new Output(); String out = tzfmt.format((Style)testCase[3], tz, ((Date)testCase[2]).getTime(), timeType); if (!out.equals((String)testCase[4]) || timeType.value != testCase[5]) { errln("Format result for [locale=" + testCase[0] + ",tzid=" + testCase[1] + ",date=" + testCase[2] + ",style=" + testCase[3] + "]: expected [output=" + testCase[4] + ",type=" + testCase[5] + "]; actual [output=" + out + ",type=" + timeType.value + "]"); } } } // This is a test case of Ticket#11487. // Because the problem is reproduced for the very first time, // the reported problem cannot be reproduced with regular test // execution. Run this test alone reproduced the problem before // the fix was merged. public void TestTZDBNamesThreading() { final TZDBTimeZoneNames names = new TZDBTimeZoneNames(ULocale.ENGLISH); final AtomicInteger found = new AtomicInteger(); List threads = new ArrayList(); final int numIteration = 1000; try { for (int i = 0; i < numIteration; i++) { Thread thread = new Thread() { @Override public void run() { int resultSize = names.find("GMT", 0, EnumSet.allOf(NameType.class)).size(); if (resultSize > 0) { found.incrementAndGet(); } } }; thread.start(); threads.add(thread); } for(Thread thread: threads) { thread.join(); } } catch (Throwable t) { errln(t.toString()); } if (found.intValue() != numIteration) { errln("Incorrect count: " + found.toString() + ", expected: " + numIteration); } } }