]> gitweb.fperrin.net Git - Dictionary.git/blob - jars/icu4j-52_1/main/tests/core/src/com/ibm/icu/dev/test/format/TimeZoneFormatTest.java
Upgrade ICU4J.
[Dictionary.git] / jars / icu4j-52_1 / main / tests / core / src / com / ibm / icu / dev / test / format / TimeZoneFormatTest.java
1 /*
2  ********************************************************************************
3  * Copyright (C) 2007-2013, Google, International Business Machines Corporation *
4  * and others. All Rights Reserved.                                             *
5  ********************************************************************************
6  */
7
8 package com.ibm.icu.dev.test.format;
9
10 import java.text.ParseException;
11 import java.text.ParsePosition;
12 import java.util.Arrays;
13 import java.util.Date;
14 import java.util.EnumSet;
15 import java.util.List;
16 import java.util.Set;
17 import java.util.TreeSet;
18 import java.util.regex.Pattern;
19
20 import com.ibm.icu.impl.ZoneMeta;
21 import com.ibm.icu.lang.UCharacter;
22 import com.ibm.icu.text.SimpleDateFormat;
23 import com.ibm.icu.text.TimeZoneFormat;
24 import com.ibm.icu.text.TimeZoneFormat.ParseOption;
25 import com.ibm.icu.text.TimeZoneFormat.Style;
26 import com.ibm.icu.text.TimeZoneFormat.TimeType;
27 import com.ibm.icu.util.BasicTimeZone;
28 import com.ibm.icu.util.Calendar;
29 import com.ibm.icu.util.Output;
30 import com.ibm.icu.util.SimpleTimeZone;
31 import com.ibm.icu.util.TimeZone;
32 import com.ibm.icu.util.TimeZone.SystemTimeZoneType;
33 import com.ibm.icu.util.TimeZoneTransition;
34 import com.ibm.icu.util.ULocale;
35
36 public class TimeZoneFormatTest extends com.ibm.icu.dev.test.TestFmwk {
37
38         private static boolean JDKTZ = (TimeZone.getDefaultTimeZoneType() == TimeZone.TIMEZONE_JDK);
39
40         public static void main(String[] args) throws Exception {
41         new TimeZoneFormatTest().run(args);
42     }
43
44     private static final String[] PATTERNS = {
45         "z",
46         "zzzz",
47         "Z",        // equivalent to "xxxx"
48         "ZZZZ",     // equivalent to "OOOO"
49         "v",
50         "vvvv",
51         "O",
52         "OOOO",
53         "X",
54         "XX",
55         "XXX",
56         "XXXX",
57         "XXXXX",
58         "x",
59         "xx",
60         "xxx",
61         "xxxx",
62         "xxxxx",
63         "V",
64         "VV",
65         "VVV",
66         "VVVV"
67     };
68     boolean REALLY_VERBOSE_LOG = false;
69
70     /*
71      * Test case for checking if a TimeZone is properly set in the result calendar
72      * and if the result TimeZone has the expected behavior.
73      */
74     public void TestTimeZoneRoundTrip() {
75         boolean TEST_ALL = getBooleanProperty("TimeZoneRoundTripAll", false);
76
77         TimeZone unknownZone = new SimpleTimeZone(-31415, "Etc/Unknown");
78         int badDstOffset = -1234;
79         int badZoneOffset = -2345;
80
81         int[][] testDateData = {
82             {2007, 1, 15},
83             {2007, 6, 15},
84             {1990, 1, 15},
85             {1990, 6, 15},
86             {1960, 1, 15},
87             {1960, 6, 15},
88         };
89
90         Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
91         cal.clear();
92
93         // Set up rule equivalency test range
94         long low, high;
95         cal.set(1900, 0, 1);
96         low = cal.getTimeInMillis();
97         cal.set(2040, 0, 1);
98         high = cal.getTimeInMillis();
99
100         // Set up test dates
101         Date[] DATES = new Date[testDateData.length];
102         cal.clear();
103         for (int i = 0; i < DATES.length; i++) {
104             cal.set(testDateData[i][0], testDateData[i][1], testDateData[i][2]);
105             DATES[i] = cal.getTime();
106         }
107
108         // Set up test locales
109         ULocale[] LOCALES = null;
110         if (TEST_ALL || getInclusion() > 5) {
111             LOCALES = ULocale.getAvailableLocales();
112         } else {
113             LOCALES = new ULocale[] {new ULocale("en"), new ULocale("en_CA"), new ULocale("fr"), new ULocale("zh_Hant")};
114         }
115
116         String[] tzids;
117         if (JDKTZ) {
118             tzids = java.util.TimeZone.getAvailableIDs();
119         } else {
120             tzids = TimeZone.getAvailableIDs();
121         }
122         int[] inOffsets = new int[2];
123         int[] outOffsets = new int[2];
124
125         // Run the roundtrip test
126         for (int locidx = 0; locidx < LOCALES.length; locidx++) {
127             logln("Locale: " + LOCALES[locidx].toString());
128
129             String localGMTString = TimeZoneFormat.getInstance(LOCALES[locidx]).formatOffsetLocalizedGMT(0);
130
131             for (int patidx = 0; patidx < PATTERNS.length; patidx++) {
132                 logln("    pattern: " + PATTERNS[patidx]);
133                 SimpleDateFormat sdf = new SimpleDateFormat(PATTERNS[patidx], LOCALES[locidx]);
134
135                 for (int tzidx = 0; tzidx < tzids.length; tzidx++) {
136                     TimeZone tz = TimeZone.getTimeZone(tzids[tzidx]);
137
138                     for (int datidx = 0; datidx < DATES.length; datidx++) {
139                         // Format
140                         sdf.setTimeZone(tz);
141                         String tzstr = sdf.format(DATES[datidx]);
142
143                         // Before parse, set unknown zone to SimpleDateFormat instance
144                         // just for making sure that it does not depends on the time zone
145                         // originally set.
146                         sdf.setTimeZone(unknownZone);
147
148                         // Parse
149                         ParsePosition pos = new ParsePosition(0);
150                         Calendar outcal = Calendar.getInstance(unknownZone);
151                         outcal.set(Calendar.DST_OFFSET, badDstOffset);
152                         outcal.set(Calendar.ZONE_OFFSET, badZoneOffset);
153
154                         sdf.parse(tzstr, outcal, pos);
155
156                         // Check the result
157                         TimeZone outtz = outcal.getTimeZone();
158
159                         tz.getOffset(DATES[datidx].getTime(), false, inOffsets);
160                         outtz.getOffset(DATES[datidx].getTime(), false, outOffsets);
161
162                         if (PATTERNS[patidx].equals("V")) {
163                             // Short zone ID - should support roundtrip for canonical CLDR IDs
164                             String canonicalID = TimeZone.getCanonicalID(tzids[tzidx]);
165                             if (!outtz.getID().equals(canonicalID)) {
166                                 if (outtz.getID().equals("Etc/Unknown")) {
167                                     // Note that some zones like Asia/Riyadh87 does not have
168                                     // short zone ID and "unk" is used as the fallback
169                                     if (REALLY_VERBOSE_LOG) {
170                                         logln("Canonical round trip failed (probably as expected); tz=" + tzids[tzidx]
171                                             + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
172                                             + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
173                                             + ", outtz=" + outtz.getID());
174                                     }
175                                 } else {
176                                     errln("Canonical round trip failed; tz=" + tzids[tzidx]
177                                         + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
178                                         + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
179                                         + ", outtz=" + outtz.getID());
180                                 }
181                             }
182                         } else if (PATTERNS[patidx].equals("VV")) {
183                             // Zone ID - full roundtrip support
184                             if (!outtz.getID().equals(tzids[tzidx])) {
185                                 errln("Zone ID round trip failed; tz=" + tzids[tzidx]
186                                         + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
187                                         + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
188                                         + ", outtz=" + outtz.getID());
189                             }
190                         } else if (PATTERNS[patidx].equals("VVV") || PATTERNS[patidx].equals("VVVV")) {
191                             // Location: time zone rule must be preserved except
192                             // zones not actually associated with a specific location.
193                             String canonicalID = TimeZone.getCanonicalID(tzids[tzidx]);
194                             if (canonicalID != null && !outtz.getID().equals(canonicalID)) {
195                                 // Canonical ID did not match - check the rules
196                                 boolean bFailure = false;
197                                 if ((tz instanceof BasicTimeZone) && (outtz instanceof BasicTimeZone)) {
198                                     boolean hasNoLocation = TimeZone.getRegion(tzids[tzidx]).equals("001");
199                                     bFailure = !hasNoLocation
200                                                 && !((BasicTimeZone)outtz).hasEquivalentTransitions(tz, low, high);
201                                 }
202                                 if (bFailure) {
203                                     errln("Canonical round trip failed; tz=" + tzids[tzidx]
204                                             + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
205                                             + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
206                                             + ", outtz=" + outtz.getID());
207                                 } else if (REALLY_VERBOSE_LOG) {
208                                     logln("Canonical round trip failed (as expected); tz=" + tzids[tzidx]
209                                             + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
210                                             + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
211                                             + ", outtz=" + outtz.getID());
212                                 }
213                             }
214                         } else {
215                             boolean isOffsetFormat = (PATTERNS[patidx].charAt(0) == 'Z'
216                                     || PATTERNS[patidx].charAt(0) == 'O'
217                                     || PATTERNS[patidx].charAt(0) == 'X'
218                                     || PATTERNS[patidx].charAt(0) == 'x');
219                             boolean minutesOffset = false;
220                             if (PATTERNS[patidx].charAt(0) == 'X' || PATTERNS[patidx].charAt(0) == 'x') {
221                                 minutesOffset = PATTERNS[patidx].length() <= 3;
222                             }
223
224                             if (!isOffsetFormat) {
225                                 // Check if localized GMT format is used as a fallback of name styles
226                                 int numDigits = 0;
227                                 for (int n = 0; n < tzstr.length(); n++) {
228                                     if (UCharacter.isDigit(tzstr.charAt(n))) {
229                                         numDigits++;
230                                     }
231                                 }
232                                 isOffsetFormat = (numDigits > 0);
233                             }
234
235                             if (isOffsetFormat || tzstr.equals(localGMTString)) {
236                                 // Localized GMT or ISO: total offset (raw + dst) must be preserved.
237                                 int inOffset = inOffsets[0] + inOffsets[1];
238                                 int outOffset = outOffsets[0] + outOffsets[1];
239                                 int diff = outOffset - inOffset;
240                                 if (minutesOffset) {
241                                     diff = (diff / 60000) * 60000;
242                                 }
243                                 if (diff != 0) {
244                                     errln("Offset round trip failed; tz=" + tzids[tzidx]
245                                         + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
246                                         + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
247                                         + ", inOffset=" + inOffset + ", outOffset=" + outOffset);
248                                 }
249                             } else {
250                                 // Specific or generic: raw offset must be preserved.
251                                 if (inOffsets[0] != outOffsets[0]) {
252                                     if (JDKTZ && tzids[tzidx].startsWith("SystemV/")) {
253                                         // JDK uses rule SystemV for these zones while
254                                         // ICU handles these zones as aliases of existing time zones
255                                         if (REALLY_VERBOSE_LOG) {
256                                             logln("Raw offset round trip failed; tz=" + tzids[tzidx]
257                                                 + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
258                                                 + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
259                                                 + ", inRawOffset=" + inOffsets[0] + ", outRawOffset=" + outOffsets[0]);
260                                         }
261
262                                     } else {
263                                         errln("Raw offset round trip failed; tz=" + tzids[tzidx]
264                                             + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
265                                             + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
266                                             + ", inRawOffset=" + inOffsets[0] + ", outRawOffset=" + outOffsets[0]);
267                                     }
268                                 }
269                             }
270                         }
271                     }
272                 }
273             }
274         }
275
276     }
277
278     /*
279      * Test case of round trip time and text.  This test case detects every canonical TimeZone's
280      * rule transition since 1900 until 2020, then check if time around each transition can
281      * round trip as expected.
282      */
283     public void TestTimeRoundTrip() {
284
285         boolean TEST_ALL = getBooleanProperty("TimeZoneRoundTripAll", false);
286
287         int startYear, endYear;
288
289         if (TEST_ALL || getInclusion() > 5) {
290             startYear = 1900;
291         } else {
292             startYear = 1990;
293         }
294
295         Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
296         endYear = cal.get(Calendar.YEAR) + 3;
297
298         cal.set(startYear, Calendar.JANUARY, 1);
299         final long START_TIME = cal.getTimeInMillis();
300
301         cal.set(endYear, Calendar.JANUARY, 1);
302         final long END_TIME = cal.getTimeInMillis();
303
304         // These patterns are ambiguous at DST->STD local time overlap
305         List<String> AMBIGUOUS_DST_DECESSION = Arrays.asList("v", "vvvv", "V", "VV", "VVV", "VVVV");
306
307         // These patterns are ambiguous at STD->STD/DST->DST local time overlap
308         List<String> AMBIGUOUS_NEGATIVE_SHIFT = Arrays.asList("z", "zzzz", "v", "vvvv", "V", "VV", "VVV", "VVVV");
309
310         // These patterns only support integer minutes offset
311         List<String> MINUTES_OFFSET = Arrays.asList("X", "XX", "XXX", "x", "xx", "xxx");
312
313         // Regex pattern used for filtering zone IDs without exemplar location
314         final Pattern LOC_EXCLUSION_PATTERN = Pattern.compile("Etc/.*|SystemV/.*|.*/Riyadh8[7-9]");
315
316         final String BASEPATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS";
317
318         ULocale[] LOCALES = null;
319
320         // timer for performance analysis
321         long[] times = new long[PATTERNS.length];
322         long timer;
323
324         if (TEST_ALL) {
325             // It may take about an hour for testing all locales
326             LOCALES = ULocale.getAvailableLocales();
327         } else if (getInclusion() > 5) {
328             LOCALES = new ULocale[] {
329                 new ULocale("ar_EG"), new ULocale("bg_BG"), new ULocale("ca_ES"), new ULocale("da_DK"), new ULocale("de"),
330                 new ULocale("de_DE"), new ULocale("el_GR"), new ULocale("en"), new ULocale("en_AU"), new ULocale("en_CA"),
331                 new ULocale("en_US"), new ULocale("es"), new ULocale("es_ES"), new ULocale("es_MX"), new ULocale("fi_FI"),
332                 new ULocale("fr"), new ULocale("fr_CA"), new ULocale("fr_FR"), new ULocale("he_IL"), new ULocale("hu_HU"),
333                 new ULocale("it"), new ULocale("it_IT"), new ULocale("ja"), new ULocale("ja_JP"), new ULocale("ko"),
334                 new ULocale("ko_KR"), new ULocale("nb_NO"), new ULocale("nl_NL"), new ULocale("nn_NO"), new ULocale("pl_PL"),
335                 new ULocale("pt"), new ULocale("pt_BR"), new ULocale("pt_PT"), new ULocale("ru_RU"), new ULocale("sv_SE"),
336                 new ULocale("th_TH"), new ULocale("tr_TR"), new ULocale("zh"), new ULocale("zh_Hans"), new ULocale("zh_Hans_CN"),
337                 new ULocale("zh_Hant"), new ULocale("zh_Hant_HK"), new ULocale("zh_Hant_TW")
338             };
339         } else {
340             LOCALES = new ULocale[] {
341                 new ULocale("en"),
342             };
343         }
344
345         SimpleDateFormat sdfGMT = new SimpleDateFormat(BASEPATTERN);
346         sdfGMT.setTimeZone(TimeZone.getTimeZone("Etc/GMT"));
347
348         long testCounts = 0;
349         long[] testTimes = new long[4];
350         boolean[] expectedRoundTrip = new boolean[4];
351         int testLen = 0;
352         for (int locidx = 0; locidx < LOCALES.length; locidx++) {
353             logln("Locale: " + LOCALES[locidx].toString());
354             for (int patidx = 0; patidx < PATTERNS.length; patidx++) {
355                 logln("    pattern: " + PATTERNS[patidx]);
356                 String pattern = BASEPATTERN + " " + PATTERNS[patidx];
357                 SimpleDateFormat sdf = new SimpleDateFormat(pattern, LOCALES[locidx]);
358                 boolean minutesOffset = MINUTES_OFFSET.contains(PATTERNS[patidx]);
359
360                 Set<String> ids = null;
361                 if (JDKTZ) {
362                         ids = new TreeSet<String>();
363                         String[] jdkIDs = java.util.TimeZone.getAvailableIDs();
364                         for (String jdkID : jdkIDs) {
365                                 String tmpID = TimeZone.getCanonicalID(jdkID);
366                                 if (tmpID != null) {
367                                         ids.add(tmpID);
368                                 }
369                         }
370                 } else {
371                         ids = TimeZone.getAvailableIDs(SystemTimeZoneType.CANONICAL, null, null);                       
372                 }
373
374                 for (String id : ids) {
375                     if (PATTERNS[patidx].equals("V")) {
376                         // Some zones do not have short ID assigned, such as Asia/Riyadh87.
377                         // The time roundtrip will fail for such zones with pattern "V" (short zone ID).
378                         // This is expected behavior.
379                         String shortZoneID = ZoneMeta.getShortID(id);
380                         if (shortZoneID == null) {
381                             continue;
382                         }
383                     } else if (PATTERNS[patidx].equals("VVV")) {
384                         // Some zones are not associated with any region, such as Etc/GMT+8.
385                         // The time roundtrip will fail for such zones with pattern "VVV" (exemplar location).
386                         // This is expected behavior.
387                         if (id.indexOf('/') < 0 || LOC_EXCLUSION_PATTERN.matcher(id).matches()) {
388                             continue;
389                         }
390                     }
391
392                     BasicTimeZone btz = (BasicTimeZone)TimeZone.getTimeZone(id, TimeZone.TIMEZONE_ICU);
393                     TimeZone tz = TimeZone.getTimeZone(id);
394                     sdf.setTimeZone(tz);
395
396                     long t = START_TIME;
397                     TimeZoneTransition tzt = null;
398                     boolean middle = true;
399                     while (t < END_TIME) {
400                         if (tzt == null) {
401                             testTimes[0] = t;
402                             expectedRoundTrip[0] = true;
403                             testLen = 1;
404                         } else {
405                             int fromOffset = tzt.getFrom().getRawOffset() + tzt.getFrom().getDSTSavings();
406                             int toOffset = tzt.getTo().getRawOffset() + tzt.getTo().getDSTSavings();
407                             int delta = toOffset - fromOffset;
408                             if (delta < 0) {
409                                 boolean isDstDecession = tzt.getFrom().getDSTSavings() > 0 && tzt.getTo().getDSTSavings() == 0;
410                                 testTimes[0] = t + delta - 1;
411                                 expectedRoundTrip[0] = true;
412                                 testTimes[1] = t + delta;
413                                 expectedRoundTrip[1] = isDstDecession ?
414                                         !AMBIGUOUS_DST_DECESSION.contains(PATTERNS[patidx]) :
415                                         !AMBIGUOUS_NEGATIVE_SHIFT.contains(PATTERNS[patidx]);
416                                 testTimes[2] = t - 1;
417                                 expectedRoundTrip[2] = isDstDecession ?
418                                         !AMBIGUOUS_DST_DECESSION.contains(PATTERNS[patidx]) :
419                                         !AMBIGUOUS_NEGATIVE_SHIFT.contains(PATTERNS[patidx]);
420                                 testTimes[3] = t;
421                                 expectedRoundTrip[3] = true;
422                                 testLen = 4;
423                             } else {
424                                 testTimes[0] = t - 1;
425                                 expectedRoundTrip[0] = true;
426                                 testTimes[1] = t;
427                                 expectedRoundTrip[1] = true;
428                                 testLen = 2;
429                             }
430                         }
431                         for (int testidx = 0; testidx < testLen; testidx++) {
432                             testCounts++;
433                             timer = System.currentTimeMillis();
434                             String text = sdf.format(new Date(testTimes[testidx]));
435                             try {
436                                 Date parsedDate = sdf.parse(text);
437                                 long restime = parsedDate.getTime();
438                                 long timeDiff = restime - testTimes[testidx];
439                                 boolean bTimeMatch = minutesOffset ?
440                                         (timeDiff/60000)*60000 == 0 : timeDiff == 0;
441                                 if (!bTimeMatch) {
442                                     StringBuffer msg = new StringBuffer();
443                                     msg.append("Time round trip failed for ")
444                                         .append("tzid=").append(id)
445                                         .append(", locale=").append(LOCALES[locidx])
446                                         .append(", pattern=").append(PATTERNS[patidx])
447                                         .append(", text=").append(text)
448                                         .append(", gmt=").append(sdfGMT.format(new Date(testTimes[testidx])))
449                                         .append(", time=").append(testTimes[testidx])
450                                         .append(", restime=").append(restime)
451                                         .append(", diff=").append(timeDiff);
452                                     if (expectedRoundTrip[testidx]) {
453                                         errln("FAIL: " + msg.toString());
454                                     } else if (REALLY_VERBOSE_LOG) {
455                                         logln(msg.toString());
456                                     }
457                                 }
458                             } catch (ParseException pe) {
459                                 errln("FAIL: " + pe.getMessage() + " tzid=" + id + ", locale=" + LOCALES[locidx] +
460                                         ", pattern=" + PATTERNS[patidx] + ", text=" + text);
461                             }
462                             times[patidx] += System.currentTimeMillis() - timer;
463                         }
464                         tzt = btz.getNextTransition(t, false);
465                         if (tzt == null) {
466                             break;
467                         }
468                         if (middle) {
469                             // Test the date in the middle of two transitions.
470                             t += (tzt.getTime() - t)/2;
471                             middle = false;
472                             tzt = null;
473                         } else {
474                             t = tzt.getTime();
475                         }
476                     }
477                 }
478             }
479         }
480
481         long total = 0;
482         logln("### Elapsed time by patterns ###");
483         for (int i = 0; i < PATTERNS.length; i++) {
484             logln(times[i] + "ms (" + PATTERNS[i] + ")");
485             total += times[i];
486         }
487         logln("Total: " + total + "ms");
488         logln("Iteration: " + testCounts);
489     }
490
491     public void TestParse() {
492         final Object[][] DATA = {
493         //   text                   inpos       locale      style                       parseAll?   expected            outpos      time type
494             {"Z",                   0,          "en_US",    Style.ISO_EXTENDED_FULL,    false,      "Etc/GMT",          1,          TimeType.UNKNOWN},
495             {"Z",                   0,          "en_US",    Style.SPECIFIC_LONG,        false,      "Etc/GMT",          1,          TimeType.UNKNOWN},
496             {"Zambia time",         0,          "en_US",    Style.ISO_EXTENDED_FULL,    true,       "Etc/GMT",          1,          TimeType.UNKNOWN},
497             {"Zambia time",         0,          "en_US",    Style.GENERIC_LOCATION,     false,      "Africa/Lusaka",    11,         TimeType.UNKNOWN},
498             {"Zambia time",         0,          "en_US",    Style.ISO_BASIC_LOCAL_FULL, true,       "Africa/Lusaka",    11,         TimeType.UNKNOWN},
499             {"+00:00",              0,          "en_US",    Style.ISO_EXTENDED_FULL,    false,      "Etc/GMT",          6,          TimeType.UNKNOWN},
500             {"-01:30:45",           0,          "en_US",    Style.ISO_EXTENDED_FULL,    false,      "GMT-01:30:45",     9,          TimeType.UNKNOWN},
501             {"-7",                  0,          "en_US",    Style.ISO_BASIC_LOCAL_FULL, false,      "GMT-07:00",        2,          TimeType.UNKNOWN},
502             {"-2222",               0,          "en_US",    Style.ISO_BASIC_LOCAL_FULL, false,      "GMT-22:22",        5,          TimeType.UNKNOWN},
503             {"-3333",               0,          "en_US",    Style.ISO_BASIC_LOCAL_FULL, false,      "GMT-03:33",        4,          TimeType.UNKNOWN},
504             {"XXX+01:30YYY",        3,          "en_US",    Style.LOCALIZED_GMT,        false,      "GMT+01:30",        9,          TimeType.UNKNOWN},
505             {"GMT0",                0,          "en_US",    Style.SPECIFIC_SHORT,       false,      "Etc/GMT",          3,          TimeType.UNKNOWN},
506             {"EST",                 0,          "en_US",    Style.SPECIFIC_SHORT,       false,      "America/New_York", 3,          TimeType.STANDARD},
507             {"ESTx",                0,          "en_US",    Style.SPECIFIC_SHORT,       false,      "America/New_York", 3,          TimeType.STANDARD},
508             {"EDTx",                0,          "en_US",    Style.SPECIFIC_SHORT,       false,      "America/New_York", 3,          TimeType.DAYLIGHT},
509             {"EST",                 0,          "en_US",    Style.SPECIFIC_LONG,        false,      null,                 0,          TimeType.UNKNOWN},
510             {"EST",                 0,          "en_US",    Style.SPECIFIC_LONG,        true,       "America/New_York", 3,          TimeType.STANDARD},
511             {"EST",                 0,          "en_CA",    Style.SPECIFIC_SHORT,       false,      "America/Toronto",  3,          TimeType.STANDARD},
512         };
513
514         for (Object[] test : DATA) {
515             String text = (String)test[0];
516             int inPos = (Integer)test[1];
517             ULocale loc = new ULocale((String)test[2]);
518             Style style = (Style)test[3];
519             EnumSet<ParseOption> options = (Boolean)test[4] ? EnumSet.of(ParseOption.ALL_STYLES) : null;
520             String expID = (String)test[5];
521             int expPos = (Integer)test[6];
522             TimeType expType = (TimeType)test[7];
523
524             TimeZoneFormat tzfmt = TimeZoneFormat.getInstance(loc);
525             Output<TimeType> timeType = new Output<TimeType>(TimeType.UNKNOWN);
526             ParsePosition pos = new ParsePosition(inPos);
527             TimeZone tz = tzfmt.parse(style, text, pos, options, timeType);
528
529             String errMsg = null;
530             if (tz == null) {
531                 if (expID != null) {
532                     errMsg = "Parse failure - expected: " + expID;
533                 }
534             } else if (!tz.getID().equals(expID)) {
535                 errMsg = "Time zone ID: " + tz.getID() + " - expected: " + expID;
536             } else if (pos.getIndex() != expPos) {
537                 errMsg = "Parsed pos: " + pos.getIndex() + " - expected: " + expPos;
538             } else if (timeType.value != expType) {
539                 errMsg = "Time type: " + timeType + " - expected: " + expType;
540             }
541
542             if (errMsg != null) {
543                 errln("Fail: " + errMsg + " [text=" + text + ", pos=" + inPos + ", style=" + style + "]");
544             }
545         }
546     }
547
548     public void TestISOFormat() {
549         final int[] OFFSET = {
550             0,          // 0
551             999,        // 0.999s
552             -59999,     // -59.999s
553             60000,      // 1m
554             -77777,     // -1m 17.777s
555             1800000,    // 30m
556             -3600000,   // -1h
557             36000000,   // 10h
558             -37800000,  // -10h 30m
559             -37845000,  // -10h 30m 45s
560             108000000,  // 30h
561         };
562  
563         final String[][] ISO_STR = {
564             // 0
565             {
566                 "Z", "Z", "Z", "Z", "Z",
567                 "+00", "+0000", "+00:00", "+0000", "+00:00",
568                 "+0000"
569             },
570             // 999
571             {
572                 "Z", "Z", "Z", "Z", "Z",
573                 "+00", "+0000", "+00:00", "+0000", "+00:00",
574                 "+0000"
575             },
576             // -59999
577             {
578                 "Z", "Z", "Z", "-000059", "-00:00:59",
579                 "+00", "+0000", "+00:00", "-000059", "-00:00:59",
580                 "-000059"
581             },
582             // 60000
583             {
584                 "+0001", "+0001", "+00:01", "+0001", "+00:01",
585                 "+0001", "+0001", "+00:01", "+0001", "+00:01",
586                 "+0001"
587             },
588             // -77777
589             {
590                 "-0001", "-0001", "-00:01", "-000117", "-00:01:17",
591                 "-0001", "-0001", "-00:01", "-000117", "-00:01:17",
592                 "-000117"
593             },
594             // 1800000
595             {
596                 "+0030", "+0030", "+00:30", "+0030", "+00:30",
597                 "+0030", "+0030", "+00:30", "+0030", "+00:30",
598                 "+0030"
599             },
600             // -3600000
601             {
602                 "-01", "-0100", "-01:00", "-0100", "-01:00",
603                 "-01", "-0100", "-01:00", "-0100", "-01:00",
604                 "-0100"
605             },
606             // 36000000
607             {
608                 "+10", "+1000", "+10:00", "+1000", "+10:00",
609                 "+10", "+1000", "+10:00", "+1000", "+10:00",
610                 "+1000"
611             },
612             // -37800000
613             {
614                 "-1030", "-1030", "-10:30", "-1030", "-10:30",
615                 "-1030", "-1030", "-10:30", "-1030", "-10:30",
616                 "-1030"
617             },
618             // -37845000
619             {
620                 "-1030", "-1030", "-10:30", "-103045", "-10:30:45",
621                 "-1030", "-1030", "-10:30", "-103045", "-10:30:45",
622                 "-103045"
623             },
624             // 108000000
625             {
626                 null, null, null, null, null,
627                 null, null, null, null, null,
628                 null
629             }
630         };
631
632         final String[] PATTERN = {
633             "X", "XX", "XXX", "XXXX", "XXXXX", "x", "xx", "xxx", "xxxx", "xxxxx",
634             "Z", // equivalent to "xxxx"
635         };
636
637         final int[] MIN_OFFSET_UNIT = {
638             60000, 60000, 60000, 1000, 1000, 60000, 60000, 60000, 1000, 1000,
639             1000,
640         };
641
642         // Formatting
643         SimpleDateFormat sdf = new SimpleDateFormat();
644         Date d = new Date();
645
646         for (int i = 0; i < OFFSET.length; i++) {
647             SimpleTimeZone tz = new SimpleTimeZone(OFFSET[i], "Zone Offset:" + String.valueOf(OFFSET[i]) + "ms");
648             sdf.setTimeZone(tz);
649             for (int j = 0; j < PATTERN.length; j++) {
650                 sdf.applyPattern(PATTERN[j]);
651                 try {
652                     String result = sdf.format(d);
653                     if (!result.equals(ISO_STR[i][j])) {
654                         errln("FAIL: pattern=" + PATTERN[j] + ", offset=" + OFFSET[i] + " -> "
655                             + result + " (expected: " + ISO_STR[i][j] + ")");
656                     }
657                 } catch (IllegalArgumentException e) {
658                     if (ISO_STR[i][j] != null) {
659                         errln("FAIL: IAE thrown for pattern=" + PATTERN[j] + ", offset=" + OFFSET[i]
660                                 + " (expected: " + ISO_STR[i][j] + ")");
661                     }
662                 }
663             }
664         }
665
666         // Parsing
667         SimpleTimeZone bogusTZ = new SimpleTimeZone(-1, "Zone Offset: -1ms");
668         for (int i = 0; i < ISO_STR.length; i++) {
669             for (int j = 0; j < ISO_STR[i].length; j++) {
670                 if (ISO_STR[i][j] == null) {
671                     continue;
672                 }
673                 ParsePosition pos = new ParsePosition(0);
674                 Calendar outcal = Calendar.getInstance(bogusTZ);
675                 sdf.applyPattern(PATTERN[j]);
676
677                 sdf.parse(ISO_STR[i][j], outcal, pos);
678
679                 if (pos.getIndex() != ISO_STR[i][j].length()) {
680                     errln("FAIL: Failed to parse the entire input string: " + ISO_STR[i][j]);
681                     continue;
682                 }
683
684                 TimeZone outtz = outcal.getTimeZone();
685                 int outOffset = outtz.getRawOffset();
686                 int adjustedOffset = OFFSET[i] / MIN_OFFSET_UNIT[j] * MIN_OFFSET_UNIT[j];
687
688                 if (outOffset != adjustedOffset) {
689                     errln("FAIL: Incorrect offset:" + outOffset + "ms for input string: " + ISO_STR[i][j]
690                             + " (expected:" + adjustedOffset + "ms)");
691                 }
692             }
693         }
694     }
695
696     public void TestFormat() {
697         final Date dateJan = new Date(1358208000000L);  // 2013-01-15T00:00:00Z
698         final Date dateJul = new Date(1373846400000L);  // 2013-07-15T00:00:00Z
699
700         final Object[][] TESTDATA = {
701             {
702                 "en",
703                 "America/Los_Angeles", 
704                 dateJan,
705                 Style.GENERIC_LOCATION,
706                 "Los Angeles Time",
707                 TimeType.UNKNOWN
708             },
709             {
710                 "en",
711                 "America/Los_Angeles",
712                 dateJan,
713                 Style.GENERIC_LONG,
714                 "Pacific Time",
715                 TimeType.UNKNOWN
716             },
717             {
718                 "en",
719                 "America/Los_Angeles",
720                 dateJan,
721                 Style.SPECIFIC_LONG,
722                 "Pacific Standard Time",
723                 TimeType.STANDARD
724             },
725             {
726                 "en",
727                 "America/Los_Angeles",
728                 dateJul,
729                 Style.SPECIFIC_LONG,
730                 "Pacific Daylight Time",
731                 TimeType.DAYLIGHT
732             },
733             {
734                 "ja",
735                 "America/Los_Angeles",
736                 dateJan,
737                 Style.ZONE_ID,
738                 "America/Los_Angeles",
739                 TimeType.UNKNOWN
740             },
741             {
742                 "fr",
743                 "America/Los_Angeles",
744                 dateJul,
745                 Style.ZONE_ID_SHORT,
746                 "uslax",
747                 TimeType.UNKNOWN
748             },
749             {
750                 "en",
751                 "America/Los_Angeles",
752                 dateJan,
753                 Style.EXEMPLAR_LOCATION,
754                 "Los Angeles",
755                 TimeType.UNKNOWN
756             },
757             {
758                 "ja",
759                 "Asia/Tokyo",
760                 dateJan,
761                 Style.GENERIC_LONG,
762                 "\u65E5\u672C\u6A19\u6E96\u6642",   // "日本標準時"
763                 TimeType.UNKNOWN
764             },
765         };
766
767         for (Object[] testCase : TESTDATA) {
768             TimeZoneFormat tzfmt = TimeZoneFormat.getInstance(new ULocale((String)testCase[0]));
769             TimeZone tz = TimeZone.getTimeZone((String)testCase[1]);
770             Output<TimeType> timeType = new Output<TimeType>();
771             String out = tzfmt.format((Style)testCase[3], tz, ((Date)testCase[2]).getTime(), timeType);
772
773             if (!out.equals((String)testCase[4]) || timeType.value != testCase[5]) {
774                 errln("Format result for [locale=" + testCase[0] + ",tzid=" + testCase[1] + ",date=" + testCase[2]
775                         + ",style=" + testCase[3] + "]: expected [output=" + testCase[4] + ",type=" + testCase[5]
776                         + "]; actual [output=" + out + ",type=" + timeType.value + "]");
777             }
778         }
779     }
780 }