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