1// © 2016 and later: Unicode, Inc. and others.
2// License & terms of use: http://www.unicode.org/copyright.html#License
3/*
4 ********************************************************************************
5 * Copyright (C) 2007-2016, Google, International Business Machines Corporation
6 * and others. All Rights Reserved.
7 ********************************************************************************
8 */
9
10package com.ibm.icu.dev.test.format;
11
12import java.text.FieldPosition;
13import java.text.ParseException;
14import java.text.ParsePosition;
15import java.util.ArrayList;
16import java.util.Arrays;
17import java.util.Collections;
18import java.util.Date;
19import java.util.EnumSet;
20import java.util.List;
21import java.util.Locale;
22import java.util.Random;
23import java.util.Set;
24import java.util.TreeSet;
25import java.util.concurrent.atomic.AtomicInteger;
26import java.util.regex.Pattern;
27
28import org.junit.Test;
29import org.junit.runner.RunWith;
30import org.junit.runners.JUnit4;
31
32import com.ibm.icu.dev.test.TestFmwk;
33import com.ibm.icu.impl.TZDBTimeZoneNames;
34import com.ibm.icu.impl.ZoneMeta;
35import com.ibm.icu.lang.UCharacter;
36import com.ibm.icu.text.DateFormat;
37import com.ibm.icu.text.SimpleDateFormat;
38import com.ibm.icu.text.TimeZoneFormat;
39import com.ibm.icu.text.TimeZoneFormat.GMTOffsetPatternType;
40import com.ibm.icu.text.TimeZoneFormat.ParseOption;
41import com.ibm.icu.text.TimeZoneFormat.Style;
42import com.ibm.icu.text.TimeZoneFormat.TimeType;
43import com.ibm.icu.text.TimeZoneNames;
44import com.ibm.icu.text.TimeZoneNames.Factory;
45import com.ibm.icu.text.TimeZoneNames.NameType;
46import com.ibm.icu.util.BasicTimeZone;
47import com.ibm.icu.util.Calendar;
48import com.ibm.icu.util.Output;
49import com.ibm.icu.util.SimpleTimeZone;
50import com.ibm.icu.util.TimeZone;
51import com.ibm.icu.util.TimeZone.SystemTimeZoneType;
52import com.ibm.icu.util.TimeZoneTransition;
53import com.ibm.icu.util.ULocale;
54
55@RunWith(JUnit4.class)
56public class TimeZoneFormatTest extends TestFmwk {
57
58    private static boolean JDKTZ = (TimeZone.getDefaultTimeZoneType() == TimeZone.TIMEZONE_JDK);
59    private static final Pattern EXCL_TZ_PATTERN = Pattern.compile(".*/Riyadh8[7-9]");
60
61    private static final String[] PATTERNS = {
62        "z",
63        "zzzz",
64        "Z",        // equivalent to "xxxx"
65        "ZZZZ",     // equivalent to "OOOO"
66        "v",
67        "vvvv",
68        "O",
69        "OOOO",
70        "X",
71        "XX",
72        "XXX",
73        "XXXX",
74        "XXXXX",
75        "x",
76        "xx",
77        "xxx",
78        "xxxx",
79        "xxxxx",
80        "V",
81        "VV",
82        "VVV",
83        "VVVV"
84    };
85    boolean REALLY_VERBOSE_LOG = false;
86
87    /*
88     * Test case for checking if a TimeZone is properly set in the result calendar
89     * and if the result TimeZone has the expected behavior.
90     */
91    @Test
92    public void TestTimeZoneRoundTrip() {
93        boolean TEST_ALL = getBooleanProperty("TimeZoneRoundTripAll", false);
94
95        TimeZone unknownZone = new SimpleTimeZone(-31415, "Etc/Unknown");
96        int badDstOffset = -1234;
97        int badZoneOffset = -2345;
98
99        int[][] testDateData = {
100            {2007, 1, 15},
101            {2007, 6, 15},
102            {1990, 1, 15},
103            {1990, 6, 15},
104            {1960, 1, 15},
105            {1960, 6, 15},
106        };
107
108        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
109        cal.clear();
110
111        // Set up rule equivalency test range
112        long low, high;
113        cal.set(1900, 0, 1);
114        low = cal.getTimeInMillis();
115        cal.set(2040, 0, 1);
116        high = cal.getTimeInMillis();
117
118        // Set up test dates
119        Date[] DATES = new Date[testDateData.length];
120        cal.clear();
121        for (int i = 0; i < DATES.length; i++) {
122            cal.set(testDateData[i][0], testDateData[i][1], testDateData[i][2]);
123            DATES[i] = cal.getTime();
124        }
125
126        // Set up test locales
127        ULocale[] LOCALES = null;
128        if (TEST_ALL || TestFmwk.getExhaustiveness() > 5) {
129            LOCALES = ULocale.getAvailableLocales();
130        } else {
131            LOCALES = new ULocale[] {new ULocale("en"), new ULocale("en_CA"), new ULocale("fr"),
132                    new ULocale("zh_Hant"), new ULocale("fa"), new ULocale("ccp")};
133        }
134
135        String[] tzids;
136        if (JDKTZ) {
137            tzids = java.util.TimeZone.getAvailableIDs();
138        } else {
139            tzids = TimeZone.getAvailableIDs();
140        }
141        int[] inOffsets = new int[2];
142        int[] outOffsets = new int[2];
143
144        // Run the roundtrip test
145        for (int locidx = 0; locidx < LOCALES.length; locidx++) {
146            logln("Locale: " + LOCALES[locidx].toString());
147
148            String localGMTString = TimeZoneFormat.getInstance(LOCALES[locidx]).formatOffsetLocalizedGMT(0);
149
150            for (int patidx = 0; patidx < PATTERNS.length; patidx++) {
151                logln("    pattern: " + PATTERNS[patidx]);
152                SimpleDateFormat sdf = new SimpleDateFormat(PATTERNS[patidx], LOCALES[locidx]);
153
154                for (int tzidx = 0; tzidx < tzids.length; tzidx++) {
155                    if (EXCL_TZ_PATTERN.matcher(tzids[tzidx]).matches()) {
156                        continue;
157                    }
158                    TimeZone tz = TimeZone.getTimeZone(tzids[tzidx]);
159
160                    for (int datidx = 0; datidx < DATES.length; datidx++) {
161                        // Format
162                        sdf.setTimeZone(tz);
163                        String tzstr = sdf.format(DATES[datidx]);
164
165                        // Before parse, set unknown zone to SimpleDateFormat instance
166                        // just for making sure that it does not depends on the time zone
167                        // originally set.
168                        sdf.setTimeZone(unknownZone);
169
170                        // Parse
171                        ParsePosition pos = new ParsePosition(0);
172                        Calendar outcal = Calendar.getInstance(unknownZone);
173                        outcal.set(Calendar.DST_OFFSET, badDstOffset);
174                        outcal.set(Calendar.ZONE_OFFSET, badZoneOffset);
175
176                        sdf.parse(tzstr, outcal, pos);
177
178                        // Check the result
179                        TimeZone outtz = outcal.getTimeZone();
180
181                        tz.getOffset(DATES[datidx].getTime(), false, inOffsets);
182                        outtz.getOffset(DATES[datidx].getTime(), false, outOffsets);
183
184                        if (PATTERNS[patidx].equals("V")) {
185                            // Short zone ID - should support roundtrip for canonical CLDR IDs
186                            String canonicalID = TimeZone.getCanonicalID(tzids[tzidx]);
187                            if (!outtz.getID().equals(canonicalID)) {
188                                if (outtz.getID().equals("Etc/Unknown")) {
189                                    // Note that some zones like Asia/Riyadh87 does not have
190                                    // short zone ID and "unk" is used as the fallback
191                                    if (REALLY_VERBOSE_LOG) {
192                                        logln("Canonical round trip failed (probably as expected); tz=" + tzids[tzidx]
193                                            + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
194                                            + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
195                                            + ", outtz=" + outtz.getID());
196                                    }
197                                } else {
198                                    errln("Canonical round trip failed; tz=" + tzids[tzidx]
199                                        + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
200                                        + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
201                                        + ", outtz=" + outtz.getID());
202                                }
203                            }
204                        } else if (PATTERNS[patidx].equals("VV")) {
205                            // Zone ID - full roundtrip support
206                            if (!outtz.getID().equals(tzids[tzidx])) {
207                                errln("Zone ID round trip failed; tz=" + tzids[tzidx]
208                                        + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
209                                        + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
210                                        + ", outtz=" + outtz.getID());
211                            }
212                        } else if (PATTERNS[patidx].equals("VVV") || PATTERNS[patidx].equals("VVVV")) {
213                            // Location: time zone rule must be preserved except
214                            // zones not actually associated with a specific location.
215                            String canonicalID = TimeZone.getCanonicalID(tzids[tzidx]);
216                            if (canonicalID != null && !outtz.getID().equals(canonicalID)) {
217                                // Canonical ID did not match - check the rules
218                                boolean bFailure = false;
219                                if ((tz instanceof BasicTimeZone) && (outtz instanceof BasicTimeZone)) {
220                                    boolean hasNoLocation = TimeZone.getRegion(tzids[tzidx]).equals("001");
221                                    bFailure = !hasNoLocation
222                                                && !((BasicTimeZone)outtz).hasEquivalentTransitions(tz, low, high);
223                                }
224                                if (bFailure) {
225                                    errln("Canonical round trip failed; tz=" + tzids[tzidx]
226                                            + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
227                                            + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
228                                            + ", outtz=" + outtz.getID());
229                                } else if (REALLY_VERBOSE_LOG) {
230                                    logln("Canonical round trip failed (as expected); tz=" + tzids[tzidx]
231                                            + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
232                                            + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
233                                            + ", outtz=" + outtz.getID());
234                                }
235                            }
236                        } else {
237                            boolean isOffsetFormat = (PATTERNS[patidx].charAt(0) == 'Z'
238                                    || PATTERNS[patidx].charAt(0) == 'O'
239                                    || PATTERNS[patidx].charAt(0) == 'X'
240                                    || PATTERNS[patidx].charAt(0) == 'x');
241                            boolean minutesOffset = false;
242                            if (PATTERNS[patidx].charAt(0) == 'X' || PATTERNS[patidx].charAt(0) == 'x') {
243                                minutesOffset = PATTERNS[patidx].length() <= 3;
244                            }
245
246                            if (!isOffsetFormat) {
247                                // Check if localized GMT format is used as a fallback of name styles
248                                int numDigits = 0;
249                                int idx = 0;
250                                while (idx < tzstr.length()) {
251                                    int cp = tzstr.codePointAt(idx);
252                                    if (UCharacter.isDigit(cp)) {
253                                        numDigits++;
254                                    }
255                                    idx += UCharacter.charCount(cp);
256                                }
257                                isOffsetFormat = (numDigits > 0);
258                            }
259
260                            if (isOffsetFormat || tzstr.equals(localGMTString)) {
261                                // Localized GMT or ISO: total offset (raw + dst) must be preserved.
262                                int inOffset = inOffsets[0] + inOffsets[1];
263                                int outOffset = outOffsets[0] + outOffsets[1];
264                                int diff = outOffset - inOffset;
265                                if (minutesOffset) {
266                                    diff = (diff / 60000) * 60000;
267                                }
268                                if (diff != 0) {
269                                    errln("Offset round trip failed; tz=" + tzids[tzidx]
270                                        + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
271                                        + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
272                                        + ", inOffset=" + inOffset + ", outOffset=" + outOffset);
273                                }
274                            } else {
275                                // Specific or generic: raw offset must be preserved.
276                                if (inOffsets[0] != outOffsets[0]) {
277                                    if (JDKTZ && tzids[tzidx].startsWith("SystemV/")) {
278                                        // JDK uses rule SystemV for these zones while
279                                        // ICU handles these zones as aliases of existing time zones
280                                        if (REALLY_VERBOSE_LOG) {
281                                            logln("Raw offset round trip failed; tz=" + tzids[tzidx]
282                                                + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
283                                                + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
284                                                + ", inRawOffset=" + inOffsets[0] + ", outRawOffset=" + outOffsets[0]);
285                                        }
286
287                                    } else {
288                                        errln("Raw offset round trip failed; tz=" + tzids[tzidx]
289                                            + ", locale=" + LOCALES[locidx] + ", pattern=" + PATTERNS[patidx]
290                                            + ", time=" + DATES[datidx].getTime() + ", str=" + tzstr
291                                            + ", inRawOffset=" + inOffsets[0] + ", outRawOffset=" + outOffsets[0]);
292                                    }
293                                }
294                            }
295                        }
296                    }
297                }
298            }
299        }
300
301    }
302
303    /*
304     * Test case of round trip time and text.  This test case detects every canonical TimeZone's
305     * rule transition since 1900 until 2020, then check if time around each transition can
306     * round trip as expected.
307     */
308    @Test
309    public void TestTimeRoundTrip() {
310
311        boolean TEST_ALL = getBooleanProperty("TimeZoneRoundTripAll", false);
312
313        int startYear, endYear;
314
315        if (TEST_ALL || TestFmwk.getExhaustiveness() > 5) {
316            startYear = 1900;
317        } else {
318            startYear = 1990;
319        }
320
321        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
322        endYear = cal.get(Calendar.YEAR) + 3;
323
324        cal.set(startYear, Calendar.JANUARY, 1);
325        final long START_TIME = cal.getTimeInMillis();
326
327        cal.set(endYear, Calendar.JANUARY, 1);
328        final long END_TIME = cal.getTimeInMillis();
329
330        // These patterns are ambiguous at DST->STD local time overlap
331        List<String> AMBIGUOUS_DST_DECESSION = Arrays.asList("v", "vvvv", "V", "VV", "VVV", "VVVV");
332
333        // These patterns are ambiguous at STD->STD/DST->DST local time overlap
334        List<String> AMBIGUOUS_NEGATIVE_SHIFT = Arrays.asList("z", "zzzz", "v", "vvvv", "V", "VV", "VVV", "VVVV");
335
336        // These patterns only support integer minutes offset
337        List<String> MINUTES_OFFSET = Arrays.asList("X", "XX", "XXX", "x", "xx", "xxx");
338
339        // Regex pattern used for filtering zone IDs without exemplar location
340        final Pattern LOC_EXCLUSION_PATTERN = Pattern.compile("Etc/.*|SystemV/.*|.*/Riyadh8[7-9]");
341
342        final String BASEPATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS";
343
344        ULocale[] LOCALES = null;
345
346        // timer for performance analysis
347        long[] times = new long[PATTERNS.length];
348        long timer;
349
350        if (TEST_ALL) {
351            // It may take about an hour for testing all locales
352            LOCALES = ULocale.getAvailableLocales();
353        } else if (TestFmwk.getExhaustiveness() > 5) {
354            LOCALES = new ULocale[] {
355                new ULocale("ar_EG"), new ULocale("bg_BG"), new ULocale("ca_ES"), new ULocale("da_DK"), new ULocale("de"),
356                new ULocale("de_DE"), new ULocale("el_GR"), new ULocale("en"), new ULocale("en_AU"), new ULocale("en_CA"),
357                new ULocale("en_US"), new ULocale("es"), new ULocale("es_ES"), new ULocale("es_MX"), new ULocale("fi_FI"),
358                new ULocale("fr"), new ULocale("fr_CA"), new ULocale("fr_FR"), new ULocale("he_IL"), new ULocale("hu_HU"),
359                new ULocale("it"), new ULocale("it_IT"), new ULocale("ja"), new ULocale("ja_JP"), new ULocale("ko"),
360                new ULocale("ko_KR"), new ULocale("nb_NO"), new ULocale("nl_NL"), new ULocale("nn_NO"), new ULocale("pl_PL"),
361                new ULocale("pt"), new ULocale("pt_BR"), new ULocale("pt_PT"), new ULocale("ru_RU"), new ULocale("sv_SE"),
362                new ULocale("th_TH"), new ULocale("tr_TR"), new ULocale("zh"), new ULocale("zh_Hans"), new ULocale("zh_Hans_CN"),
363                new ULocale("zh_Hant"), new ULocale("zh_Hant_HK"), new ULocale("zh_Hant_TW"), new ULocale("ccp"), new ULocale("fa")
364            };
365        } else {
366            LOCALES = new ULocale[] {
367                new ULocale("en"),
368            };
369        }
370
371        SimpleDateFormat sdfGMT = new SimpleDateFormat(BASEPATTERN);
372        sdfGMT.setTimeZone(TimeZone.getTimeZone("Etc/GMT"));
373
374        long testCounts = 0;
375        long[] testTimes = new long[4];
376        boolean[] expectedRoundTrip = new boolean[4];
377        int testLen = 0;
378        for (int locidx = 0; locidx < LOCALES.length; locidx++) {
379            logln("Locale: " + LOCALES[locidx].toString());
380            for (int patidx = 0; patidx < PATTERNS.length; patidx++) {
381                logln("    pattern: " + PATTERNS[patidx]);
382                String pattern = BASEPATTERN + " " + PATTERNS[patidx];
383                SimpleDateFormat sdf = new SimpleDateFormat(pattern, LOCALES[locidx]);
384                boolean minutesOffset = MINUTES_OFFSET.contains(PATTERNS[patidx]);
385
386                Set<String> ids = null;
387                if (JDKTZ) {
388                    ids = new TreeSet<String>();
389                    String[] jdkIDs = java.util.TimeZone.getAvailableIDs();
390                    for (String jdkID : jdkIDs) {
391                        if (EXCL_TZ_PATTERN.matcher(jdkID).matches()) {
392                            continue;
393                        }
394                        String tmpID = TimeZone.getCanonicalID(jdkID);
395                        if (tmpID != null) {
396                            ids.add(tmpID);
397                        }
398                    }
399                } else {
400                    ids = TimeZone.getAvailableIDs(SystemTimeZoneType.CANONICAL, null, null);
401                }
402
403                for (String id : ids) {
404                    if (PATTERNS[patidx].equals("V")) {
405                        // Some zones do not have short ID assigned, such as Asia/Riyadh87.
406                        // The time roundtrip will fail for such zones with pattern "V" (short zone ID).
407                        // This is expected behavior.
408                        String shortZoneID = ZoneMeta.getShortID(id);
409                        if (shortZoneID == null) {
410                            continue;
411                        }
412                    } else if (PATTERNS[patidx].equals("VVV")) {
413                        // Some zones are not associated with any region, such as Etc/GMT+8.
414                        // The time roundtrip will fail for such zones with pattern "VVV" (exemplar location).
415                        // This is expected behavior.
416                        if (id.indexOf('/') < 0 || LOC_EXCLUSION_PATTERN.matcher(id).matches()) {
417                            continue;
418                        }
419                    }
420
421                    if ((id.equals("Pacific/Apia") || id.equals("Pacific/Midway") || id.equals("Pacific/Pago_Pago"))
422                            && PATTERNS[patidx].equals("vvvv")
423                            && logKnownIssue("11052", "Ambiguous zone name - Samoa Time")) {
424                        continue;
425                    }
426
427                    BasicTimeZone btz = (BasicTimeZone)TimeZone.getTimeZone(id, TimeZone.TIMEZONE_ICU);
428                    TimeZone tz = TimeZone.getTimeZone(id);
429                    sdf.setTimeZone(tz);
430
431                    long t = START_TIME;
432                    TimeZoneTransition tzt = null;
433                    boolean middle = true;
434                    boolean last = false;
435                    while (t < END_TIME) {
436                        if (tzt == null) {
437                            testTimes[0] = t;
438                            expectedRoundTrip[0] = true;
439                            testLen = 1;
440                        } else {
441                            int fromOffset = tzt.getFrom().getRawOffset() + tzt.getFrom().getDSTSavings();
442                            int toOffset = tzt.getTo().getRawOffset() + tzt.getTo().getDSTSavings();
443                            int delta = toOffset - fromOffset;
444                            if (delta < 0) {
445                                boolean isDstDecession = tzt.getFrom().getDSTSavings() > 0 && tzt.getTo().getDSTSavings() == 0;
446                                testTimes[0] = t + delta - 1;
447                                expectedRoundTrip[0] = true;
448                                testTimes[1] = t + delta;
449                                expectedRoundTrip[1] = isDstDecession ?
450                                        !AMBIGUOUS_DST_DECESSION.contains(PATTERNS[patidx]) :
451                                        !AMBIGUOUS_NEGATIVE_SHIFT.contains(PATTERNS[patidx]);
452                                testTimes[2] = t - 1;
453                                expectedRoundTrip[2] = isDstDecession ?
454                                        !AMBIGUOUS_DST_DECESSION.contains(PATTERNS[patidx]) :
455                                        !AMBIGUOUS_NEGATIVE_SHIFT.contains(PATTERNS[patidx]);
456                                testTimes[3] = t;
457                                expectedRoundTrip[3] = true;
458                                testLen = 4;
459                            } else {
460                                testTimes[0] = t - 1;
461                                expectedRoundTrip[0] = true;
462                                testTimes[1] = t;
463                                expectedRoundTrip[1] = true;
464                                testLen = 2;
465                            }
466                        }
467                        for (int testidx = 0; testidx < testLen; testidx++) {
468                            testCounts++;
469                            timer = System.currentTimeMillis();
470                            String text = sdf.format(new Date(testTimes[testidx]));
471                            try {
472                                Date parsedDate = sdf.parse(text);
473                                long restime = parsedDate.getTime();
474                                long timeDiff = restime - testTimes[testidx];
475                                boolean bTimeMatch = minutesOffset ?
476                                        (timeDiff/60000)*60000 == 0 : timeDiff == 0;
477                                if (!bTimeMatch) {
478                                    StringBuffer msg = new StringBuffer();
479                                    msg.append("Time round trip failed for ")
480                                        .append("tzid=").append(id)
481                                        .append(", locale=").append(LOCALES[locidx])
482                                        .append(", pattern=").append(PATTERNS[patidx])
483                                        .append(", text=").append(text)
484                                        .append(", gmt=").append(sdfGMT.format(new Date(testTimes[testidx])))
485                                        .append(", time=").append(testTimes[testidx])
486                                        .append(", restime=").append(restime)
487                                        .append(", diff=").append(timeDiff);
488                                    if (expectedRoundTrip[testidx]
489                                            && !isSpecialTimeRoundTripCase(LOCALES[locidx], id, PATTERNS[patidx], testTimes[testidx])) {
490                                        errln("FAIL: " + msg.toString());
491                                    } else if (REALLY_VERBOSE_LOG) {
492                                        logln(msg.toString());
493                                    }
494                                }
495                            } catch (ParseException pe) {
496                                errln("FAIL: " + pe.getMessage() + " tzid=" + id + ", locale=" + LOCALES[locidx] +
497                                        ", pattern=" + PATTERNS[patidx] + ", text=" + text);
498                            }
499                            times[patidx] += System.currentTimeMillis() - timer;
500                        }
501
502                        if (last) {
503                            break;
504                        }
505
506                        tzt = btz.getNextTransition(t, false);
507                        if (tzt == null) {
508                            last = true;
509                            t = END_TIME - 1;
510                        } else if (middle) {
511                            // Test the date in the middle of two transitions.
512                            t += (tzt.getTime() - t)/2;
513                            middle = false;
514                            tzt = null;
515                        } else {
516                            t = tzt.getTime();
517                        }
518                    }
519                }
520            }
521        }
522
523        long total = 0;
524        logln("### Elapsed time by patterns ###");
525        for (int i = 0; i < PATTERNS.length; i++) {
526            logln(times[i] + "ms (" + PATTERNS[i] + ")");
527            total += times[i];
528        }
529        logln("Total: " + total + "ms");
530        logln("Iteration: " + testCounts);
531    }
532
533    // Special exclusions in TestTimeZoneRoundTrip.
534    // These special cases do not round trip time as designed.
535    private boolean isSpecialTimeRoundTripCase(ULocale loc, String id, String pattern, long time) {
536        final Object[][] EXCLUSIONS = {
537            {null, "Asia/Chita", "zzzz", Long.valueOf(1414252800000L)},
538            {null, "Asia/Chita", "vvvv", Long.valueOf(1414252800000L)},
539            {null, "Asia/Srednekolymsk", "zzzz", Long.valueOf(1414241999999L)},
540            {null, "Asia/Srednekolymsk", "vvvv", Long.valueOf(1414241999999L)},
541        };
542        boolean isExcluded = false;
543        for (Object[] excl : EXCLUSIONS) {
544            if (excl[0] == null || loc.equals(excl[0])) {
545                if (id.equals(excl[1])) {
546                    if (excl[2] == null || pattern.equals(excl[2])) {
547                        if (excl[3] == null || ((Long)excl[3]).compareTo(time) == 0) {
548                            isExcluded = true;
549                            break;
550                        }
551                    }
552                }
553            }
554        }
555        return isExcluded;
556    }
557
558    @Test
559    public void TestParse() {
560        final Object[][] DATA = {
561        //   text                   inpos       locale      style
562        //      parseOptions            expected            outpos      time type
563            {"Z",                   0,          "en_US",    Style.ISO_EXTENDED_FULL,
564                null,                   "Etc/GMT",          1,          TimeType.UNKNOWN},
565
566            {"Z",                   0,          "en_US",    Style.SPECIFIC_LONG,
567                null,                   "Etc/GMT",          1,          TimeType.UNKNOWN},
568
569            {"Zambia time",         0,          "en_US",    Style.ISO_EXTENDED_FULL,
570                EnumSet.of(ParseOption.ALL_STYLES), "Etc/GMT",  1,      TimeType.UNKNOWN},
571
572            {"Zambia time",         0,          "en_US",    Style.GENERIC_LOCATION,
573                null,                   "Africa/Lusaka",    11,         TimeType.UNKNOWN},
574
575            {"Zambia time",         0,          "en_US",    Style.ISO_BASIC_LOCAL_FULL,
576                EnumSet.of(ParseOption.ALL_STYLES), "Africa/Lusaka",    11, TimeType.UNKNOWN},
577
578            {"+00:00",              0,          "en_US",    Style.ISO_EXTENDED_FULL,
579                null,                   "Etc/GMT",          6,          TimeType.UNKNOWN},
580
581            {"-01:30:45",           0,          "en_US",    Style.ISO_EXTENDED_FULL,
582                null,                   "GMT-01:30:45",     9,          TimeType.UNKNOWN},
583
584            {"-7",                  0,          "en_US",    Style.ISO_BASIC_LOCAL_FULL,
585                null,                   "GMT-07:00",        2,          TimeType.UNKNOWN},
586
587            {"-2222",               0,          "en_US",    Style.ISO_BASIC_LOCAL_FULL,
588                null,                   "GMT-22:22",        5,          TimeType.UNKNOWN},
589
590            {"-3333",               0,          "en_US",    Style.ISO_BASIC_LOCAL_FULL,
591                null,                   "GMT-03:33",        4,          TimeType.UNKNOWN},
592
593            {"XXX+01:30YYY",        3,          "en_US",    Style.LOCALIZED_GMT,
594                null,                   "GMT+01:30",        9,          TimeType.UNKNOWN},
595
596            {"GMT0",                0,          "en_US",    Style.SPECIFIC_SHORT,
597                null,                   "Etc/GMT",          3,          TimeType.UNKNOWN},
598
599            {"EST",                 0,          "en_US",    Style.SPECIFIC_SHORT,
600                null,                   "America/New_York", 3,          TimeType.STANDARD},
601
602            {"ESTx",                0,          "en_US",    Style.SPECIFIC_SHORT,
603                null,                   "America/New_York", 3,          TimeType.STANDARD},
604
605            {"EDTx",                0,          "en_US",    Style.SPECIFIC_SHORT,
606                null,                   "America/New_York", 3,          TimeType.DAYLIGHT},
607
608            {"EST",                 0,          "en_US",    Style.SPECIFIC_LONG,
609                null,                   null,               0,          TimeType.UNKNOWN},
610
611            {"EST",                 0,          "en_US",    Style.SPECIFIC_LONG,
612                EnumSet.of(ParseOption.ALL_STYLES), "America/New_York", 3,  TimeType.STANDARD},
613
614            {"EST",                 0,          "en_CA",    Style.SPECIFIC_SHORT,
615                null,                   "America/Toronto",  3,          TimeType.STANDARD},
616
617            {"CST",                 0,          "en_US",    Style.SPECIFIC_SHORT,
618                null,                   "America/Chicago",  3,          TimeType.STANDARD},
619
620            {"CST",                 0,          "en_GB",    Style.SPECIFIC_SHORT,
621                null,                   null,               0,          TimeType.UNKNOWN},
622
623            {"CST",                 0,          "en_GB",    Style.SPECIFIC_SHORT,
624                EnumSet.of(ParseOption.TZ_DATABASE_ABBREVIATIONS),  "America/Chicago",  3,  TimeType.STANDARD},
625
626            {"--CST--",             2,          "en_GB",    Style.SPECIFIC_SHORT,
627                EnumSet.of(ParseOption.TZ_DATABASE_ABBREVIATIONS),  "America/Chicago",  5,  TimeType.STANDARD},
628
629            {"CST",                 0,          "zh_CN",    Style.SPECIFIC_SHORT,
630                EnumSet.of(ParseOption.TZ_DATABASE_ABBREVIATIONS),  "Asia/Shanghai",    3,  TimeType.STANDARD},
631
632            {"AEST",                0,          "en_AU",    Style.SPECIFIC_SHORT,
633                EnumSet.of(ParseOption.TZ_DATABASE_ABBREVIATIONS),  "Australia/Sydney", 4,  TimeType.STANDARD},
634
635            {"AST",                 0,          "ar_SA",    Style.SPECIFIC_SHORT,
636                EnumSet.of(ParseOption.TZ_DATABASE_ABBREVIATIONS),  "Asia/Riyadh",      3,  TimeType.STANDARD},
637
638            {"AQTST",               0,          "en",       Style.SPECIFIC_LONG,
639                null,                       null,           0,          TimeType.UNKNOWN},
640
641            {"AQTST",           0,      "en",       Style.SPECIFIC_LONG,
642                EnumSet.of(ParseOption.ALL_STYLES), null,   0,          TimeType.UNKNOWN},
643
644            {"AQTST",           0,      "en",       Style.SPECIFIC_LONG,
645                EnumSet.of(ParseOption.ALL_STYLES, ParseOption.TZ_DATABASE_ABBREVIATIONS),  "Asia/Aqtobe",  5,  TimeType.DAYLIGHT},
646
647            {"hora de verano británica", 0,     "es",       Style.SPECIFIC_LONG,
648                null,                   "Europe/London",    24,         TimeType.DAYLIGHT},
649        };
650
651        for (Object[] test : DATA) {
652            String text = (String)test[0];
653            int inPos = (Integer)test[1];
654            ULocale loc = new ULocale((String)test[2]);
655            Style style = (Style)test[3];
656            EnumSet<ParseOption> options = (EnumSet<ParseOption>)test[4];
657            String expID = (String)test[5];
658            int expPos = (Integer)test[6];
659            TimeType expType = (TimeType)test[7];
660
661            TimeZoneFormat tzfmt = TimeZoneFormat.getInstance(loc);
662            Output<TimeType> timeType = new Output<TimeType>(TimeType.UNKNOWN);
663            ParsePosition pos = new ParsePosition(inPos);
664            TimeZone tz = tzfmt.parse(style, text, pos, options, timeType);
665
666            String errMsg = null;
667            if (tz == null) {
668                if (expID != null) {
669                    errMsg = "Parse failure - expected: " + expID;
670                }
671            } else if (!tz.getID().equals(expID)) {
672                errMsg = "Time zone ID: " + tz.getID() + " - expected: " + expID;
673            } else if (pos.getIndex() != expPos) {
674                errMsg = "Parsed pos: " + pos.getIndex() + " - expected: " + expPos;
675            } else if (timeType.value != expType) {
676                errMsg = "Time type: " + timeType + " - expected: " + expType;
677            }
678
679            if (errMsg != null) {
680                errln("Fail: " + errMsg +
681                        " [text=" + text + ", pos=" + inPos +
682                        ", locale=" + loc + ", style=" + style + "]");
683            }
684        }
685    }
686
687    // Coverage tests for other versions of the parse() method. All of them end up
688    // calling the full parse() method tested on the TestParse() test.
689    @Test
690    public void TestParseCoverage() {
691        TimeZone expectedTZ = TimeZone.getTimeZone("America/Los_Angeles");
692        TimeZoneFormat fmt = TimeZoneFormat.getInstance(ULocale.ENGLISH);
693
694        // Test parse(String)
695        try {
696            TimeZone tz1 = fmt.parse("America/Los_Angeles");
697            if (tz1 == null) {
698                errln("Parse failure using parse(String) - expected: " + expectedTZ.getID());
699            } else if (!expectedTZ.equals(tz1)) {
700                errln("Parsed TimeZone: '" + tz1.getID()  + "' using parse(String) - expected: "
701                        + expectedTZ.getID());
702            }
703        } catch (ParseException e) {
704            errln("Parse failure using parse(String) - expected: " + expectedTZ.getID()
705                    + " exception: " + e.getMessage());
706        }
707
708        // Test parse(String, ParsePosition)
709        TimeZone tz2 = fmt.parse("++America/Los_Angeles", new ParsePosition(2));
710        if (tz2 == null) {
711            errln("Parse failure using parse(String, ParsePosition) - expected: "
712                    + expectedTZ.getID());
713        } else if (!expectedTZ.equals(tz2)) {
714            errln("Parsed TimeZone: '" + tz2.getID()  + "' using parse(String, ParsePosition) - expected: "
715                    + expectedTZ.getID());
716        }
717
718        // Test parseObject(String, ParsePosition)
719        Object tz3 = fmt.parseObject("++America/Los_Angeles", new ParsePosition(2));
720        if (tz3 == null) {
721            errln("Parse failure using parseObject(String, ParsePosition) - expected: "
722                    + expectedTZ.getID());
723        } else if (!expectedTZ.equals(tz3)) {
724            errln("Parsed TimeZone: '" + ((TimeZone)tz3).getID()
725                    + "' using parseObject(String, ParsePosition) - expected: "
726                    + expectedTZ.getID());
727        }
728    }
729
730    @Test
731    public void TestISOFormat() {
732        final int[] OFFSET = {
733            0,          // 0
734            999,        // 0.999s
735            -59999,     // -59.999s
736            60000,      // 1m
737            -77777,     // -1m 17.777s
738            1800000,    // 30m
739            -3600000,   // -1h
740            36000000,   // 10h
741            -37800000,  // -10h 30m
742            -37845000,  // -10h 30m 45s
743            108000000,  // 30h
744        };
745
746        final String[][] ISO_STR = {
747            // 0
748            {
749                "Z", "Z", "Z", "Z", "Z",
750                "+00", "+0000", "+00:00", "+0000", "+00:00",
751                "+0000"
752            },
753            // 999
754            {
755                "Z", "Z", "Z", "Z", "Z",
756                "+00", "+0000", "+00:00", "+0000", "+00:00",
757                "+0000"
758            },
759            // -59999
760            {
761                "Z", "Z", "Z", "-000059", "-00:00:59",
762                "+00", "+0000", "+00:00", "-000059", "-00:00:59",
763                "-000059"
764            },
765            // 60000
766            {
767                "+0001", "+0001", "+00:01", "+0001", "+00:01",
768                "+0001", "+0001", "+00:01", "+0001", "+00:01",
769                "+0001"
770            },
771            // -77777
772            {
773                "-0001", "-0001", "-00:01", "-000117", "-00:01:17",
774                "-0001", "-0001", "-00:01", "-000117", "-00:01:17",
775                "-000117"
776            },
777            // 1800000
778            {
779                "+0030", "+0030", "+00:30", "+0030", "+00:30",
780                "+0030", "+0030", "+00:30", "+0030", "+00:30",
781                "+0030"
782            },
783            // -3600000
784            {
785                "-01", "-0100", "-01:00", "-0100", "-01:00",
786                "-01", "-0100", "-01:00", "-0100", "-01:00",
787                "-0100"
788            },
789            // 36000000
790            {
791                "+10", "+1000", "+10:00", "+1000", "+10:00",
792                "+10", "+1000", "+10:00", "+1000", "+10:00",
793                "+1000"
794            },
795            // -37800000
796            {
797                "-1030", "-1030", "-10:30", "-1030", "-10:30",
798                "-1030", "-1030", "-10:30", "-1030", "-10:30",
799                "-1030"
800            },
801            // -37845000
802            {
803                "-1030", "-1030", "-10:30", "-103045", "-10:30:45",
804                "-1030", "-1030", "-10:30", "-103045", "-10:30:45",
805                "-103045"
806            },
807            // 108000000
808            {
809                null, null, null, null, null,
810                null, null, null, null, null,
811                null
812            }
813        };
814
815        final String[] PATTERN = {
816            "X", "XX", "XXX", "XXXX", "XXXXX", "x", "xx", "xxx", "xxxx", "xxxxx",
817            "Z", // equivalent to "xxxx"
818        };
819
820        final int[] MIN_OFFSET_UNIT = {
821            60000, 60000, 60000, 1000, 1000, 60000, 60000, 60000, 1000, 1000,
822            1000,
823        };
824
825        // Formatting
826        SimpleDateFormat sdf = new SimpleDateFormat();
827        Date d = new Date();
828
829        for (int i = 0; i < OFFSET.length; i++) {
830            SimpleTimeZone tz = new SimpleTimeZone(OFFSET[i], "Zone Offset:" + String.valueOf(OFFSET[i]) + "ms");
831            sdf.setTimeZone(tz);
832            for (int j = 0; j < PATTERN.length; j++) {
833                sdf.applyPattern(PATTERN[j]);
834                try {
835                    String result = sdf.format(d);
836                    if (!result.equals(ISO_STR[i][j])) {
837                        errln("FAIL: pattern=" + PATTERN[j] + ", offset=" + OFFSET[i] + " -> "
838                            + result + " (expected: " + ISO_STR[i][j] + ")");
839                    }
840                } catch (IllegalArgumentException e) {
841                    if (ISO_STR[i][j] != null) {
842                        errln("FAIL: IAE thrown for pattern=" + PATTERN[j] + ", offset=" + OFFSET[i]
843                                + " (expected: " + ISO_STR[i][j] + ")");
844                    }
845                }
846            }
847        }
848
849        // Parsing
850        SimpleTimeZone bogusTZ = new SimpleTimeZone(-1, "Zone Offset: -1ms");
851        for (int i = 0; i < ISO_STR.length; i++) {
852            for (int j = 0; j < ISO_STR[i].length; j++) {
853                if (ISO_STR[i][j] == null) {
854                    continue;
855                }
856                ParsePosition pos = new ParsePosition(0);
857                Calendar outcal = Calendar.getInstance(bogusTZ);
858                sdf.applyPattern(PATTERN[j]);
859
860                sdf.parse(ISO_STR[i][j], outcal, pos);
861
862                if (pos.getIndex() != ISO_STR[i][j].length()) {
863                    errln("FAIL: Failed to parse the entire input string: " + ISO_STR[i][j]);
864                    continue;
865                }
866
867                TimeZone outtz = outcal.getTimeZone();
868                int outOffset = outtz.getRawOffset();
869                int adjustedOffset = OFFSET[i] / MIN_OFFSET_UNIT[j] * MIN_OFFSET_UNIT[j];
870
871                if (outOffset != adjustedOffset) {
872                    errln("FAIL: Incorrect offset:" + outOffset + "ms for input string: " + ISO_STR[i][j]
873                            + " (expected:" + adjustedOffset + "ms)");
874                }
875            }
876        }
877    }
878
879    @Test
880    public void TestFormat() {
881        final Date dateJan = new Date(1358208000000L);  // 2013-01-15T00:00:00Z
882        final Date dateJul = new Date(1373846400000L);  // 2013-07-15T00:00:00Z
883
884        final Object[][] TESTDATA = {
885            {
886                "en",
887                "America/Los_Angeles",
888                dateJan,
889                Style.GENERIC_LOCATION,
890                "Los Angeles Time",
891                TimeType.UNKNOWN
892            },
893            {
894                "en",
895                "America/Los_Angeles",
896                dateJan,
897                Style.GENERIC_LONG,
898                "Pacific Time",
899                TimeType.UNKNOWN
900            },
901            {
902                "en",
903                "America/Los_Angeles",
904                dateJan,
905                Style.SPECIFIC_LONG,
906                "Pacific Standard Time",
907                TimeType.STANDARD
908            },
909            {
910                "en",
911                "America/Los_Angeles",
912                dateJul,
913                Style.SPECIFIC_LONG,
914                "Pacific Daylight Time",
915                TimeType.DAYLIGHT
916            },
917            {
918                "ja",
919                "America/Los_Angeles",
920                dateJan,
921                Style.ZONE_ID,
922                "America/Los_Angeles",
923                TimeType.UNKNOWN
924            },
925            {
926                "fr",
927                "America/Los_Angeles",
928                dateJul,
929                Style.ZONE_ID_SHORT,
930                "uslax",
931                TimeType.UNKNOWN
932            },
933            {
934                "en",
935                "America/Los_Angeles",
936                dateJan,
937                Style.EXEMPLAR_LOCATION,
938                "Los Angeles",
939                TimeType.UNKNOWN
940            },
941            {
942                "ja",
943                "Asia/Tokyo",
944                dateJan,
945                Style.GENERIC_LONG,
946                "\u65E5\u672C\u6A19\u6E96\u6642",   // "日本標準時"
947                TimeType.UNKNOWN
948            },
949        };
950
951        for (Object[] testCase : TESTDATA) {
952            TimeZone tz = TimeZone.getTimeZone((String)testCase[1]);
953            Output<TimeType> timeType = new Output<TimeType>();
954
955            ULocale uloc = new ULocale((String)testCase[0]);
956            TimeZoneFormat tzfmt = TimeZoneFormat.getInstance(uloc);
957            String out = tzfmt.format((Style)testCase[3], tz, ((Date)testCase[2]).getTime(), timeType);
958
959            if (!out.equals(testCase[4]) || timeType.value != testCase[5]) {
960                errln("Format result for [locale=" + testCase[0] + ",tzid=" + testCase[1] + ",date=" + testCase[2]
961                        + ",style=" + testCase[3] + "]: expected [output=" + testCase[4] + ",type=" + testCase[5]
962                        + "]; actual [output=" + out + ",type=" + timeType.value + "]");
963            }
964
965            // with equivalent Java Locale
966            Locale loc = uloc.toLocale();
967            tzfmt = TimeZoneFormat.getInstance(loc);
968            out = tzfmt.format((Style)testCase[3], tz, ((Date)testCase[2]).getTime(), timeType);
969
970            if (!out.equals(testCase[4]) || timeType.value != testCase[5]) {
971                errln("Format result for [locale(Java)=" + testCase[0] + ",tzid=" + testCase[1] + ",date=" + testCase[2]
972                        + ",style=" + testCase[3] + "]: expected [output=" + testCase[4] + ",type=" + testCase[5]
973                        + "]; actual [output=" + out + ",type=" + timeType.value + "]");
974            }
975        }
976    }
977
978    @Test
979    public void TestFormatTZDBNames() {
980        final Date dateJan = new Date(1358208000000L);  // 2013-01-15T00:00:00Z
981        final Date dateJul = new Date(1373846400000L);  // 2013-07-15T00:00:00Z
982
983        final Object[][] TESTDATA = {
984            {
985                "en",
986                "America/Chicago",
987                dateJan,
988                Style.SPECIFIC_SHORT,
989                "CST",
990                TimeType.STANDARD
991            },
992            {
993                "en",
994                "Asia/Shanghai",
995                dateJan,
996                Style.SPECIFIC_SHORT,
997                "CST",
998                TimeType.STANDARD
999            },
1000            {
1001                "zh_Hans",
1002                "Asia/Shanghai",
1003                dateJan,
1004                Style.SPECIFIC_SHORT,
1005                "CST",
1006                TimeType.STANDARD
1007            },
1008            {
1009                "en",
1010                "America/Los_Angeles",
1011                dateJul,
1012                Style.SPECIFIC_LONG,
1013                "GMT-07:00",    // No long display names
1014                TimeType.DAYLIGHT
1015            },
1016            {
1017                "ja",
1018                "America/Los_Angeles",
1019                dateJul,
1020                Style.SPECIFIC_SHORT,
1021                "PDT",
1022                TimeType.DAYLIGHT
1023            },
1024            {
1025                "en",
1026                "Australia/Sydney",
1027                dateJan,
1028                Style.SPECIFIC_SHORT,
1029                "AEDT",
1030                TimeType.DAYLIGHT
1031            },
1032            {
1033                "en",
1034                "Australia/Sydney",
1035                dateJul,
1036                Style.SPECIFIC_SHORT,
1037                "AEST",
1038                TimeType.STANDARD
1039            },
1040        };
1041
1042        for (Object[] testCase : TESTDATA) {
1043            ULocale loc = new ULocale((String)testCase[0]);
1044            TimeZoneFormat tzfmt = TimeZoneFormat.getInstance(loc).cloneAsThawed();
1045            TimeZoneNames tzdbNames = TimeZoneNames.getTZDBInstance(loc);
1046            tzfmt.setTimeZoneNames(tzdbNames);
1047
1048            TimeZone tz = TimeZone.getTimeZone((String)testCase[1]);
1049            Output<TimeType> timeType = new Output<TimeType>();
1050            String out = tzfmt.format((Style)testCase[3], tz, ((Date)testCase[2]).getTime(), timeType);
1051
1052            if (!out.equals(testCase[4]) || timeType.value != testCase[5]) {
1053                errln("Format result for [locale=" + testCase[0] + ",tzid=" + testCase[1] + ",date=" + testCase[2]
1054                        + ",style=" + testCase[3] + "]: expected [output=" + testCase[4] + ",type=" + testCase[5]
1055                        + "]; actual [output=" + out + ",type=" + timeType.value + "]");
1056            }
1057        }
1058    }
1059
1060    // Tests format(Object, StringBuffer, FieldPosition):StringBuffer method
1061    // inherited from Format class
1062    @Test
1063    public void TestInheritedFormat() {
1064        TimeZone tz = TimeZone.getTimeZone("America/Los_Angeles");
1065        Calendar cal = Calendar.getInstance(tz);
1066        cal.setTimeInMillis(1459187377690L); // Mar 28, 2016
1067
1068        StringBuffer sb = new StringBuffer();
1069        FieldPosition fp = new FieldPosition(DateFormat.Field.TIME_ZONE);
1070
1071        TimeZoneFormat fmt = TimeZoneFormat.getInstance(ULocale.ENGLISH);
1072
1073        // Test formatting a non-timezone related object
1074        try {
1075            fmt.format(new Object(), sb, fp);
1076            errln("ERROR: format non-timezone related object failed");
1077        } catch (IllegalArgumentException e) { /* Expected */ }
1078
1079        // Test formatting a TimeZone object
1080        sb = new StringBuffer();
1081        fmt.format(tz, sb, fp);
1082        // When formatting a TimeZone object the formatter uses the current date.
1083        String fmtOutput = tz.inDaylightTime(new Date()) ? "GMT-07:00" : "GMT-08:00";
1084        if (!sb.toString().equals(fmtOutput)) {
1085            errln("ERROR: format TimerZone object failed. Expected: " + fmtOutput + ", actual: " + sb);
1086        }
1087
1088        // Test formatting a Calendar object
1089        sb = new StringBuffer();
1090        fmt.format(cal, sb, fp);
1091        if (!sb.toString().equals("GMT-07:00")) {
1092            errln("ERROR: format Calendar object failed. Expected: GMT-07:00, actual: " + sb);
1093        }
1094    }
1095
1096    // This is a test case of Ticket#11487.
1097    // Because the problem is reproduced for the very first time,
1098    // the reported problem cannot be reproduced with regular test
1099    // execution. Run this test alone reproduced the problem before
1100    // the fix was merged.
1101    @Test
1102    public void TestTZDBNamesThreading() {
1103        final TZDBTimeZoneNames names = new TZDBTimeZoneNames(ULocale.ENGLISH);
1104        final AtomicInteger found = new AtomicInteger();
1105        List<Thread> threads = new ArrayList<Thread>();
1106        final int numIteration = 1000;
1107
1108        try {
1109            for (int i = 0; i < numIteration; i++) {
1110                Thread thread = new Thread() {
1111                    @Override
1112                    public void run() {
1113                        int resultSize = names.find("GMT", 0, EnumSet.allOf(NameType.class)).size();
1114                        if (resultSize > 0) {
1115                            found.incrementAndGet();
1116                        }
1117                    }
1118                };
1119                thread.start();
1120                threads.add(thread);
1121            }
1122
1123            for(Thread thread: threads) {
1124                thread.join();
1125            }
1126        } catch (Throwable t) {
1127            errln(t.toString());
1128        }
1129
1130        if (found.intValue() != numIteration) {
1131            errln("Incorrect count: " + found.toString() + ", expected: " + numIteration);
1132        }
1133    }
1134
1135    @Test
1136    public void TestGetDisplayNames() {
1137        long date = System.currentTimeMillis();
1138        NameType[] types = new NameType[]{
1139                NameType.LONG_STANDARD, NameType.LONG_DAYLIGHT,
1140                NameType.SHORT_STANDARD, NameType.SHORT_DAYLIGHT
1141        };
1142        Set<String> zones = ZoneMeta.getAvailableIDs(SystemTimeZoneType.ANY, null, null);
1143
1144        int casesTested = 0;
1145        Random rnd = new Random(2016);
1146        for (ULocale uloc : ULocale.getAvailableLocales()) {
1147            if (rnd.nextDouble() > 0.01) { continue; }
1148            for (String zone : zones) {
1149                if (rnd.nextDouble() > 0.01) { continue; }
1150                casesTested++;
1151
1152                // Test default TimeZoneNames (uses an overridden getDisplayNames)
1153                {
1154                    TimeZoneNames tznames = TimeZoneNames.getInstance(uloc);
1155                    tznames.loadAllDisplayNames();
1156                    String[] result = new String[types.length];
1157                    tznames.getDisplayNames(zone, types, date, result, 0);
1158                    for (int i=0; i<types.length; i++) {
1159                        NameType type = types[i];
1160                        String expected = result[i];
1161                        String actual = tznames.getDisplayName(zone, type, date);
1162                        assertEquals("TimeZoneNames: getDisplayNames() returns different result than getDisplayName()"
1163                                + " for " + zone + " in locale " + uloc, expected, actual);
1164                    }
1165                    // Coverage for empty call to getDisplayNames
1166                    tznames.getDisplayNames(null, null, 0, null, 0);
1167                }
1168
1169                // Test TZDBTimeZoneNames (uses getDisplayNames from abstract class)
1170                {
1171                    TimeZoneNames tznames = new TZDBTimeZoneNames(uloc);
1172                    tznames.loadAllDisplayNames();
1173                    String[] result = new String[types.length];
1174                    tznames.getDisplayNames(zone, types, date, result, 0);
1175                    for (int i=0; i<types.length; i++) {
1176                        NameType type = types[i];
1177                        String expected = result[i];
1178                        String actual = tznames.getDisplayName(zone, type, date);
1179                        assertEquals("TZDBTimeZoneNames: getDisplayNames() returns different result than getDisplayName()"
1180                                + " for " + zone + " in locale " + uloc, expected, actual);
1181                    }
1182                    // Coverage for empty call to getDisplayNames
1183                    tznames.getDisplayNames(null, null, 0, null, 0);
1184                }
1185            }
1186        }
1187
1188        assertTrue("No cases were tested", casesTested > 0);
1189    }
1190
1191    class TimeZoneNamesInheriter extends TimeZoneNames {
1192        private static final long serialVersionUID = 1L;
1193
1194        @Override
1195        public Set<String> getAvailableMetaZoneIDs() {
1196            return null;
1197        }
1198
1199        @Override
1200        public Set<String> getAvailableMetaZoneIDs(String tzID) {
1201            return null;
1202        }
1203
1204        @Override
1205        public String getMetaZoneID(String tzID, long date) {
1206            return null;
1207        }
1208
1209        @Override
1210        public String getReferenceZoneID(String mzID, String region) {
1211            return null;
1212        }
1213
1214        @Override
1215        public String getMetaZoneDisplayName(String mzID, NameType type) {
1216            return null;
1217        }
1218
1219        @Override
1220        public String getTimeZoneDisplayName(String tzID, NameType type) {
1221            return null;
1222        }
1223    }
1224
1225    // Coverage for default implementation and abstract methods in base class.
1226    @Test
1227    public void TestDefaultTimeZoneNames() {
1228        long date = System.currentTimeMillis();
1229        TimeZoneNames.Factory factory;
1230        try {
1231            Class cls = Class.forName("com.ibm.icu.text.TimeZoneNames$DefaultTimeZoneNames$FactoryImpl");
1232            factory = (Factory) cls.newInstance();
1233        } catch (Exception e) {
1234            errln("Could not create class DefaultTimeZoneNames.FactoryImpl: " + e.getClass() + ": " + e.getMessage());
1235            return;
1236        }
1237        TimeZoneNames tzn = factory.getTimeZoneNames(ULocale.ENGLISH);
1238        assertEquals("Abstract: getAvailableMetaZoneIDs()",
1239                tzn.getAvailableMetaZoneIDs(), Collections.emptySet());
1240        assertEquals("Abstract: getAvailableMetaZoneIDs(String tzID)",
1241                tzn.getAvailableMetaZoneIDs("America/Chicago"), Collections.emptySet());
1242        assertEquals("Abstract: getMetaZoneID(String tzID, long date)",
1243                tzn.getMetaZoneID("America/Chicago", date), null);
1244        assertEquals("Abstract: getReferenceZoneID(String mzID, String region)",
1245                tzn.getReferenceZoneID("America_Central", "IT"), null);
1246        assertEquals("Abstract: getMetaZoneDisplayName(String mzID, NameType type)",
1247                tzn.getMetaZoneDisplayName("America_Central", NameType.LONG_DAYLIGHT), null);
1248        assertEquals("Abstract: getTimeZoneDisplayName(String mzID, NameType type)",
1249                tzn.getTimeZoneDisplayName("America/Chicago", NameType.LONG_DAYLIGHT), null);
1250        assertEquals("Abstract: find(CharSequence text, int start, EnumSet<NameType> nameTypes)",
1251                tzn.find("foo", 0, EnumSet.noneOf(NameType.class)), Collections.emptyList());
1252
1253        // Other abstract-class methods that aren't covered
1254        tzn = new TimeZoneNamesInheriter();
1255        try {
1256            tzn.find(null, 0, null);
1257        } catch (UnsupportedOperationException e) {
1258            assertEquals("find() exception", "The method is not implemented in TimeZoneNames base class.", e.getMessage());
1259        }
1260    }
1261
1262    // Basic get/set test for methods not being called otherwise.
1263    @Test
1264    public void TestAPI() {
1265        TimeZoneFormat tzfmtEn = TimeZoneFormat.getInstance(ULocale.ENGLISH);
1266        TimeZoneFormat tzfmtAr = TimeZoneFormat.getInstance(new ULocale("ar")).cloneAsThawed();
1267        TimeZoneNames tzn = TimeZoneNames.getInstance(Locale.ENGLISH);
1268
1269        String digits = tzfmtEn.getGMTOffsetDigits();
1270        tzfmtAr.setGMTOffsetDigits(digits);
1271        if (!digits.equals(tzfmtAr.getGMTOffsetDigits())) {
1272            errln("ERROR: get/set GMTOffsetDigits failed");
1273        }
1274
1275        String pattern = tzfmtEn.getGMTOffsetPattern(GMTOffsetPatternType.POSITIVE_H);
1276        tzfmtAr.setGMTOffsetPattern(GMTOffsetPatternType.POSITIVE_H, pattern);
1277        if (!pattern.equals(tzfmtAr.getGMTOffsetPattern(GMTOffsetPatternType.POSITIVE_H))) {
1278            errln("ERROR: get/set GMTOffsetPattern failed");
1279        }
1280
1281        String zeroFmt = tzfmtEn.getGMTZeroFormat();
1282        tzfmtAr.setGMTZeroFormat(zeroFmt);
1283        if (!zeroFmt.equals(tzfmtAr.getGMTZeroFormat())) {
1284            errln("ERROR: get/set GMTZeroFormat failed");
1285        }
1286
1287        Set<String> allAvailableMZIDs = tzn.getAvailableMetaZoneIDs();
1288        if (allAvailableMZIDs.size() < 150 || !allAvailableMZIDs.contains("America_Central")) {
1289            errln("ERROR: getAvailableMetaZoneIDs() did not return expected value");
1290        }
1291
1292        Set<String> kinshasaAvailableMZIDs = tzn.getAvailableMetaZoneIDs("Africa/Kinshasa");
1293        if (!kinshasaAvailableMZIDs.contains("Africa_Western") || kinshasaAvailableMZIDs.contains("America_Central")) {
1294            errln("ERROR: getAvailableMetaZoneIDs('Africa/Kinshasa') did not return expected value");
1295        }
1296
1297        try {
1298            new TimeZoneNames.MatchInfo(null, null, null, -1);
1299            assertTrue("MatchInfo doesn't throw IllegalArgumentException", false);
1300        } catch (IllegalArgumentException e) {
1301            assertEquals("MatchInfo constructor exception", "nameType is null", e.getMessage());
1302        }
1303
1304        try {
1305            new TimeZoneNames.MatchInfo(NameType.LONG_GENERIC, null, null, -1);
1306            assertTrue("MatchInfo doesn't throw IllegalArgumentException", false);
1307        } catch (IllegalArgumentException e) {
1308            assertEquals("MatchInfo constructor exception", "Either tzID or mzID must be available", e.getMessage());
1309        }
1310
1311        try {
1312            new TimeZoneNames.MatchInfo(NameType.LONG_GENERIC, "America/Chicago", null, -1);
1313            assertTrue("MatchInfo doesn't throw IllegalArgumentException", false);
1314        } catch (IllegalArgumentException e) {
1315            assertEquals("MatchInfo constructor exception", "matchLength must be positive value", e.getMessage());
1316        }
1317    }
1318}
1319