/* ******************************************************************************** * Copyright (C) 2007-2010, 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.Date; import com.ibm.icu.lang.UCharacter; import com.ibm.icu.text.SimpleDateFormat; import com.ibm.icu.util.BasicTimeZone; import com.ibm.icu.util.Calendar; import com.ibm.icu.util.SimpleTimeZone; import com.ibm.icu.util.TimeZone; import com.ibm.icu.util.TimeZoneTransition; import com.ibm.icu.util.ULocale; public class TimeZoneFormatTest extends com.ibm.icu.dev.test.TestFmwk { public static void main(String[] args) throws Exception { new TimeZoneFormatTest().run(args); } private static final String[] PATTERNS = {"z", "zzzz", "Z", "ZZZZ", "v", "vvvv", "V", "VVVV"}; /* * 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 = "true".equalsIgnoreCase(getProperty("TimeZoneRoundTripAll")); 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 = TimeZone.getAvailableIDs(); int[] inOffsets = new int[2]; int[] outOffsets = new int[2]; // Run the roundtrip test for (int locidx = 0; locidx < LOCALES.length; locidx++) { for (int patidx = 0; patidx < PATTERNS.length; patidx++) { SimpleDateFormat sdf = new SimpleDateFormat(PATTERNS[patidx], LOCALES[locidx]); for (int tzidx = 0; tzidx < tzids.length; tzidx++) { 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("VVVV")) { // Location: time zone rule must be preserved except // zones not actually associated with a specific location. // Time zones in this category do not have "/" in its ID. 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)) { bFailure = !(canonicalID.indexOf('/') == -1) && !((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 { 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 { // Check if localized GMT format or RFC format is used. int numDigits = 0; for (int n = 0; n < tzstr.length(); n++) { if (UCharacter.isDigit(tzstr.charAt(n))) { numDigits++; } } if (numDigits >= 3) { // Localized GMT or RFC: total offset (raw + dst) must be preserved. int inOffset = inOffsets[0] + inOffsets[1]; int outOffset = outOffsets[0] + outOffsets[1]; if (inOffset != outOffset) { 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 (TimeZone.getDefaultTimeZoneType() == TimeZone.TIMEZONE_JDK && tzids[tzidx].startsWith("SystemV/")) { // JDK uses rule SystemV for these zones while // ICU handles these zones as aliases of existing time zones 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 = "true".equalsIgnoreCase(getProperty("TimeZoneRoundTripAll")); 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(); // Whether each pattern is ambiguous at DST->STD local time overlap final boolean[] AMBIGUOUS_DST_DECESSION = {false, false, false, false, true, true, false, true}; // Whether each pattern is ambiguous at STD->STD/DST->DST local time overlap final boolean[] AMBIGUOUS_NEGATIVE_SHIFT = {true, true, false, false, true, true, true, true}; final String BASEPATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS"; ULocale[] LOCALES = null; boolean REALLY_VERBOSE = false; // 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]); String[] ids = TimeZone.getAvailableIDs(); for (int zidx = 0; zidx < ids.length; zidx++) { String id = TimeZone.getCanonicalID(ids[zidx]); if (id == null || !id.equals(ids[zidx])) { // Skip aliases continue; } BasicTimeZone btz = (BasicTimeZone)TimeZone.getTimeZone(ids[zidx], TimeZone.TIMEZONE_ICU); TimeZone tz = TimeZone.getTimeZone(ids[zidx]); 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[patidx] : !AMBIGUOUS_NEGATIVE_SHIFT[patidx]; testTimes[2] = t - 1; expectedRoundTrip[2] = isDstDecession ? !AMBIGUOUS_DST_DECESSION[patidx] : !AMBIGUOUS_NEGATIVE_SHIFT[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(); if (restime != testTimes[testidx]) { StringBuffer msg = new StringBuffer(); msg.append("Time round trip failed for ") .append("tzid=").append(ids[zidx]) .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(restime - testTimes[testidx]); if (expectedRoundTrip[testidx]) { errln("FAIL: " + msg.toString()); } else if (REALLY_VERBOSE) { logln(msg.toString()); } } } catch (ParseException pe) { errln("FAIL: " + pe.getMessage()); } 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); } }