2 ********************************************************************************
\r
3 * Copyright (C) 2007-2010, Google, International Business Machines Corporation *
\r
4 * and others. All Rights Reserved. *
\r
5 ********************************************************************************
\r
8 package com.ibm.icu.dev.test.format;
\r
10 import java.text.ParseException;
\r
11 import java.text.ParsePosition;
\r
12 import java.util.Date;
\r
14 import com.ibm.icu.lang.UCharacter;
\r
15 import com.ibm.icu.text.SimpleDateFormat;
\r
16 import com.ibm.icu.util.BasicTimeZone;
\r
17 import com.ibm.icu.util.Calendar;
\r
18 import com.ibm.icu.util.SimpleTimeZone;
\r
19 import com.ibm.icu.util.TimeZone;
\r
20 import com.ibm.icu.util.TimeZoneTransition;
\r
21 import com.ibm.icu.util.ULocale;
\r
23 public class TimeZoneFormatTest extends com.ibm.icu.dev.test.TestFmwk {
\r
25 public static void main(String[] args) throws Exception {
\r
26 new TimeZoneFormatTest().run(args);
\r
29 private static final String[] PATTERNS = {"z", "zzzz", "Z", "ZZZZ", "v", "vvvv", "V", "VVVV"};
\r
32 * Test case for checking if a TimeZone is properly set in the result calendar
\r
33 * and if the result TimeZone has the expected behavior.
\r
35 public void TestTimeZoneRoundTrip() {
\r
36 boolean TEST_ALL = "true".equalsIgnoreCase(getProperty("TimeZoneRoundTripAll"));
\r
38 TimeZone unknownZone = new SimpleTimeZone(-31415, "Etc/Unknown");
\r
39 int badDstOffset = -1234;
\r
40 int badZoneOffset = -2345;
\r
42 int[][] testDateData = {
\r
51 Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
\r
54 // Set up rule equivalency test range
\r
56 cal.set(1900, 0, 1);
\r
57 low = cal.getTimeInMillis();
\r
58 cal.set(2040, 0, 1);
\r
59 high = cal.getTimeInMillis();
\r
61 // Set up test dates
\r
62 Date[] DATES = new Date[testDateData.length];
\r
64 for (int i = 0; i < DATES.length; i++) {
\r
65 cal.set(testDateData[i][0], testDateData[i][1], testDateData[i][2]);
\r
66 DATES[i] = cal.getTime();
\r
69 // Set up test locales
\r
70 ULocale[] LOCALES = null;
\r
71 if (TEST_ALL || getInclusion() > 5) {
\r
72 LOCALES = ULocale.getAvailableLocales();
\r
74 LOCALES = new ULocale[] {new ULocale("en"), new ULocale("en_CA"), new ULocale("fr"), new ULocale("zh_Hant")};
\r
77 String[] tzids = TimeZone.getAvailableIDs();
\r
78 int[] inOffsets = new int[2];
\r
79 int[] outOffsets = new int[2];
\r
81 // Run the roundtrip test
\r
82 for (int locidx = 0; locidx < LOCALES.length; locidx++) {
\r
83 for (int patidx = 0; patidx < PATTERNS.length; patidx++) {
\r
84 SimpleDateFormat sdf = new SimpleDateFormat(PATTERNS[patidx], LOCALES[locidx]);
\r
86 for (int tzidx = 0; tzidx < tzids.length; tzidx++) {
\r
87 TimeZone tz = TimeZone.getTimeZone(tzids[tzidx]);
\r
89 for (int datidx = 0; datidx < DATES.length; datidx++) {
\r
91 sdf.setTimeZone(tz);
\r
92 String tzstr = sdf.format(DATES[datidx]);
\r
94 // Before parse, set unknown zone to SimpleDateFormat instance
\r
95 // just for making sure that it does not depends on the time zone
\r
97 sdf.setTimeZone(unknownZone);
\r
100 ParsePosition pos = new ParsePosition(0);
\r
101 Calendar outcal = Calendar.getInstance(unknownZone);
\r
102 outcal.set(Calendar.DST_OFFSET, badDstOffset);
\r
103 outcal.set(Calendar.ZONE_OFFSET, badZoneOffset);
\r
105 sdf.parse(tzstr, outcal, pos);
\r
107 // Check the result
\r
108 TimeZone outtz = outcal.getTimeZone();
\r
110 tz.getOffset(DATES[datidx].getTime(), false, inOffsets);
\r
111 outtz.getOffset(DATES[datidx].getTime(), false, outOffsets);
\r
113 if (PATTERNS[patidx].equals("VVVV")) {
\r
114 // Location: time zone rule must be preserved except
\r
115 // zones not actually associated with a specific location.
\r
116 // Time zones in this category do not have "/" in its ID.
\r
117 String canonicalID = TimeZone.getCanonicalID(tzids[tzidx]);
\r
118 if (canonicalID != null && !outtz.getID().equals(canonicalID)) {
\r
119 // Canonical ID did not match - check the rules
\r
120 boolean bFailure = false;
\r
121 if ((tz instanceof BasicTimeZone) && (outtz instanceof BasicTimeZone)) {
\r
122 bFailure = !(canonicalID.indexOf('/') == -1)
\r
123 && !((BasicTimeZone)outtz).hasEquivalentTransitions(tz, low, high);
\r
126 errln("Canonical round trip failed; tz=" + tzids[tzidx]
\r
127 + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
\r
128 + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
\r
129 + ", outtz=" + outtz.getID());
\r
131 logln("Canonical round trip failed (as expected); tz=" + tzids[tzidx]
\r
132 + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
\r
133 + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
\r
134 + ", outtz=" + outtz.getID());
\r
138 // Check if localized GMT format or RFC format is used.
\r
140 for (int n = 0; n < tzstr.length(); n++) {
\r
141 if (UCharacter.isDigit(tzstr.charAt(n))) {
\r
146 if (numDigits >= 3) {
\r
147 // Localized GMT or RFC: total offset (raw + dst) must be preserved.
\r
148 int inOffset = inOffsets[0] + inOffsets[1];
\r
149 int outOffset = outOffsets[0] + outOffsets[1];
\r
150 if (inOffset != outOffset) {
\r
151 errln("Offset round trip failed; tz=" + tzids[tzidx]
\r
152 + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
\r
153 + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
\r
154 + ", inOffset=" + inOffset + ", outOffset=" + outOffset);
\r
157 // Specific or generic: raw offset must be preserved.
\r
158 if (inOffsets[0] != outOffsets[0]) {
\r
159 if (TimeZone.getDefaultTimeZoneType() == TimeZone.TIMEZONE_JDK
\r
160 && tzids[tzidx].startsWith("SystemV/")) {
\r
161 // JDK uses rule SystemV for these zones while
\r
162 // ICU handles these zones as aliases of existing time zones
\r
163 logln("Raw offset round trip failed; tz=" + tzids[tzidx]
\r
164 + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
\r
165 + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
\r
166 + ", inRawOffset=" + inOffsets[0] + ", outRawOffset=" + outOffsets[0]);
\r
169 errln("Raw offset round trip failed; tz=" + tzids[tzidx]
\r
170 + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
\r
171 + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
\r
172 + ", inRawOffset=" + inOffsets[0] + ", outRawOffset=" + outOffsets[0]);
\r
185 * Test case of round trip time and text. This test case detects every canonical TimeZone's
\r
186 * rule transition since 1900 until 2020, then check if time around each transition can
\r
187 * round trip as expected.
\r
189 public void TestTimeRoundTrip() {
\r
191 boolean TEST_ALL = "true".equalsIgnoreCase(getProperty("TimeZoneRoundTripAll"));
\r
193 int startYear, endYear;
\r
195 if (TEST_ALL || getInclusion() > 5) {
\r
201 Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
\r
202 endYear = cal.get(Calendar.YEAR) + 3;
\r
204 cal.set(startYear, Calendar.JANUARY, 1);
\r
205 final long START_TIME = cal.getTimeInMillis();
\r
207 cal.set(endYear, Calendar.JANUARY, 1);
\r
208 final long END_TIME = cal.getTimeInMillis();
\r
210 // Whether each pattern is ambiguous at DST->STD local time overlap
\r
211 final boolean[] AMBIGUOUS_DST_DECESSION = {false, false, false, false, true, true, false, true};
\r
212 // Whether each pattern is ambiguous at STD->STD/DST->DST local time overlap
\r
213 final boolean[] AMBIGUOUS_NEGATIVE_SHIFT = {true, true, false, false, true, true, true, true};
\r
215 final String BASEPATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS";
\r
217 ULocale[] LOCALES = null;
\r
218 boolean REALLY_VERBOSE = false;
\r
220 // timer for performance analysis
\r
221 long[] times = new long[PATTERNS.length];
\r
225 // It may take about an hour for testing all locales
\r
226 LOCALES = ULocale.getAvailableLocales();
\r
227 } else if (getInclusion() > 5) {
\r
228 LOCALES = new ULocale[] {
\r
229 new ULocale("ar_EG"), new ULocale("bg_BG"), new ULocale("ca_ES"), new ULocale("da_DK"), new ULocale("de"),
\r
230 new ULocale("de_DE"), new ULocale("el_GR"), new ULocale("en"), new ULocale("en_AU"), new ULocale("en_CA"),
\r
231 new ULocale("en_US"), new ULocale("es"), new ULocale("es_ES"), new ULocale("es_MX"), new ULocale("fi_FI"),
\r
232 new ULocale("fr"), new ULocale("fr_CA"), new ULocale("fr_FR"), new ULocale("he_IL"), new ULocale("hu_HU"),
\r
233 new ULocale("it"), new ULocale("it_IT"), new ULocale("ja"), new ULocale("ja_JP"), new ULocale("ko"),
\r
234 new ULocale("ko_KR"), new ULocale("nb_NO"), new ULocale("nl_NL"), new ULocale("nn_NO"), new ULocale("pl_PL"),
\r
235 new ULocale("pt"), new ULocale("pt_BR"), new ULocale("pt_PT"), new ULocale("ru_RU"), new ULocale("sv_SE"),
\r
236 new ULocale("th_TH"), new ULocale("tr_TR"), new ULocale("zh"), new ULocale("zh_Hans"), new ULocale("zh_Hans_CN"),
\r
237 new ULocale("zh_Hant"), new ULocale("zh_Hant_HK"), new ULocale("zh_Hant_TW")
\r
240 LOCALES = new ULocale[] {
\r
245 SimpleDateFormat sdfGMT = new SimpleDateFormat(BASEPATTERN);
\r
246 sdfGMT.setTimeZone(TimeZone.getTimeZone("Etc/GMT"));
\r
248 long testCounts = 0;
\r
249 long[] testTimes = new long[4];
\r
250 boolean[] expectedRoundTrip = new boolean[4];
\r
252 for (int locidx = 0; locidx < LOCALES.length; locidx++) {
\r
253 logln("Locale: " + LOCALES[locidx].toString());
\r
254 for (int patidx = 0; patidx < PATTERNS.length; patidx++) {
\r
255 logln(" pattern: " + PATTERNS[patidx]);
\r
256 String pattern = BASEPATTERN + " " + PATTERNS[patidx];
\r
257 SimpleDateFormat sdf = new SimpleDateFormat(pattern, LOCALES[locidx]);
\r
259 String[] ids = TimeZone.getAvailableIDs();
\r
260 for (int zidx = 0; zidx < ids.length; zidx++) {
\r
261 String id = TimeZone.getCanonicalID(ids[zidx]);
\r
262 if (id == null || !id.equals(ids[zidx])) {
\r
266 BasicTimeZone btz = (BasicTimeZone)TimeZone.getTimeZone(ids[zidx], TimeZone.TIMEZONE_ICU);
\r
267 TimeZone tz = TimeZone.getTimeZone(ids[zidx]);
\r
268 sdf.setTimeZone(tz);
\r
270 long t = START_TIME;
\r
271 TimeZoneTransition tzt = null;
\r
272 boolean middle = true;
\r
273 while (t < END_TIME) {
\r
276 expectedRoundTrip[0] = true;
\r
279 int fromOffset = tzt.getFrom().getRawOffset() + tzt.getFrom().getDSTSavings();
\r
280 int toOffset = tzt.getTo().getRawOffset() + tzt.getTo().getDSTSavings();
\r
281 int delta = toOffset - fromOffset;
\r
283 boolean isDstDecession = tzt.getFrom().getDSTSavings() > 0 && tzt.getTo().getDSTSavings() == 0;
\r
284 testTimes[0] = t + delta - 1;
\r
285 expectedRoundTrip[0] = true;
\r
286 testTimes[1] = t + delta;
\r
287 expectedRoundTrip[1] = isDstDecession ?
\r
288 !AMBIGUOUS_DST_DECESSION[patidx] : !AMBIGUOUS_NEGATIVE_SHIFT[patidx];
\r
289 testTimes[2] = t - 1;
\r
290 expectedRoundTrip[2] = isDstDecession ?
\r
291 !AMBIGUOUS_DST_DECESSION[patidx] : !AMBIGUOUS_NEGATIVE_SHIFT[patidx];
\r
293 expectedRoundTrip[3] = true;
\r
296 testTimes[0] = t - 1;
\r
297 expectedRoundTrip[0] = true;
\r
299 expectedRoundTrip[1] = true;
\r
303 for (int testidx = 0; testidx < testLen; testidx++) {
\r
305 timer = System.currentTimeMillis();
\r
306 String text = sdf.format(new Date(testTimes[testidx]));
\r
308 Date parsedDate = sdf.parse(text);
\r
309 long restime = parsedDate.getTime();
\r
310 if (restime != testTimes[testidx]) {
\r
311 StringBuffer msg = new StringBuffer();
\r
312 msg.append("Time round trip failed for ")
\r
313 .append("tzid=").append(ids[zidx])
\r
314 .append(", locale=").append(LOCALES[locidx])
\r
315 .append(", pattern=").append(PATTERNS[patidx])
\r
316 .append(", text=").append(text)
\r
317 .append(", gmt=").append(sdfGMT.format(new Date(testTimes[testidx])))
\r
318 .append(", time=").append(testTimes[testidx])
\r
319 .append(", restime=").append(restime)
\r
320 .append(", diff=").append(restime - testTimes[testidx]);
\r
321 if (expectedRoundTrip[testidx]) {
\r
322 errln("FAIL: " + msg.toString());
\r
323 } else if (REALLY_VERBOSE) {
\r
324 logln(msg.toString());
\r
327 } catch (ParseException pe) {
\r
328 errln("FAIL: " + pe.getMessage());
\r
330 times[patidx] += System.currentTimeMillis() - timer;
\r
332 tzt = btz.getNextTransition(t, false);
\r
337 // Test the date in the middle of two transitions.
\r
338 t += (tzt.getTime() - t)/2;
\r
350 logln("### Elapsed time by patterns ###");
\r
351 for (int i = 0; i < PATTERNS.length; i++) {
\r
352 logln(times[i] + "ms (" + PATTERNS[i] + ")");
\r
355 logln("Total: " + total + "ms");
\r
356 logln("Iteration: " + testCounts);
\r