1 /*
2  * Copyright (c) 2012, 2018, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.
8  *
9  * This code is distributed in the hope that it will be useful, but WITHOUT
10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12  * version 2 for more details (a copy is included in the LICENSE file that
13  * accompanied this code).
14  *
15  * You should have received a copy of the GNU General Public License version
16  * 2 along with this work; if not, write to the Free Software Foundation,
17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18  *
19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20  * or visit www.oracle.com if you need additional information or have any
21  * questions.
22  */
23 
24 package test.java.time.format;
25 
26 import static org.testng.Assert.assertEquals;
27 
28 import android.platform.test.annotations.LargeTest;
29 
30 import java.text.DateFormatSymbols;
31 import java.time.ZoneId;
32 import java.time.ZonedDateTime;
33 import java.time.format.DecimalStyle;
34 import java.time.format.DateTimeFormatter;
35 import java.time.format.DateTimeFormatterBuilder;
36 import java.time.format.TextStyle;
37 import java.time.temporal.ChronoField;
38 import java.time.temporal.TemporalQueries;
39 import java.time.zone.ZoneRulesProvider;
40 import java.util.Arrays;
41 import java.util.Date;
42 import java.util.HashSet;
43 import java.util.Locale;
44 import java.util.Random;
45 import java.util.Set;
46 import java.util.TimeZone;
47 import jdk.test.lib.RandomFactory;
48 
49 import org.testng.annotations.DataProvider;
50 import org.testng.annotations.Test;
51 
52 /*
53  * @test
54  * @bug 8081022 8151876 8166875 8189784 8206980
55  * @key randomness
56  */
57 
58 /**
59  * Test ZoneTextPrinterParser
60  */
61 @Test
62 public class TestZoneTextPrinterParser extends AbstractTestPrinterParser {
63 
getFormatter(Locale locale, TextStyle style)64     protected static DateTimeFormatter getFormatter(Locale locale, TextStyle style) {
65         return new DateTimeFormatterBuilder().appendZoneText(style)
66                                              .toFormatter(locale)
67                                              .withDecimalStyle(DecimalStyle.of(locale));
68     }
69 
70     @LargeTest
test_printText()71     public void test_printText() {
72         Random r = RandomFactory.getRandom();
73         // Android-changed: only run one iteration.
74         int N = 1;
75         Locale[] locales = Locale.getAvailableLocales();
76         Set<String> zids = ZoneRulesProvider.getAvailableZoneIds();
77         ZonedDateTime zdt = ZonedDateTime.now();
78 
79         //System.out.printf("locale==%d, timezone=%d%n", locales.length, zids.size());
80         while (N-- > 0) {
81             zdt = zdt.withDayOfYear(r.nextInt(365) + 1)
82                      .with(ChronoField.SECOND_OF_DAY, r.nextInt(86400));
83             // Android-changed: loop over locales first to speed up test. TimeZoneNames are cached
84             // per locale, but the cache only holds the most recently used locales.
85             /*
86             for (String zid : zids) {
87                 if (zid.equals("ROC") || zid.startsWith("Etc/GMT")) {
88                     continue;      // TBD: match jdk behavior?
89                 }
90                 zdt = zdt.withZoneSameLocal(ZoneId.of(zid));
91                 TimeZone tz = TimeZone.getTimeZone(zid);
92                 boolean isDST = tz.inDaylightTime(new Date(zdt.toInstant().toEpochMilli()));
93                 for (Locale locale : locales) {
94                     boolean isDST = tz.inDaylightTime(new Date(zdt.toInstant().toEpochMilli()));
95                     String longDisplayName = tz.getDisplayName(isDST, TimeZone.LONG, locale);
96                     String shortDisplayName = tz.getDisplayName(isDST, TimeZone.SHORT, locale);
97                     if ((longDisplayName.startsWith("GMT+") && shortDisplayName.startsWith("GMT+"))
98                             || (longDisplayName.startsWith("GMT-") && shortDisplayName.startsWith("GMT-"))) {
99                         printText(locale, zdt, TextStyle.FULL, tz, tz.getID());
100                         printText(locale, zdt, TextStyle.SHORT, tz, tz.getID());
101                         continue;
102                     }
103              */
104             for (Locale locale : locales) {
105                 // Android-changed: "ji" isn't correctly aliased to "yi", see http//b/8634320.
106                 if (locale.getLanguage().equals("ji")) {
107                     continue;
108                 }
109                 for (String zid : zids) {
110                     if (zid.equals("ROC") || zid.startsWith("Etc/GMT")) {
111                         continue;      // TBD: match jdk behavior?
112                     }
113                     // Android-changed (http://b/33197219): TimeZone.getDisplayName() for
114                     // non-canonical time zones are not correct.
115                     if (!zid.equals(getSystemCanonicalID(zid))) {
116                         continue;
117                     }
118                     zdt = zdt.withZoneSameLocal(ZoneId.of(zid));
119                     TimeZone tz = TimeZone.getTimeZone(zid);
120                     // Android-changed: We don't have long names for GMT.
121                     if (tz.getID().equals("GMT")) {
122                         continue;
123                     }
124                     boolean isDST = tz.inDaylightTime(new Date(zdt.toInstant().toEpochMilli()));
125                     printText(locale, zdt, TextStyle.FULL, tz,
126                         tz.getDisplayName(isDST, TimeZone.LONG, locale));
127                     printText(locale, zdt, TextStyle.SHORT, tz,
128                         tz.getDisplayName(isDST, TimeZone.SHORT, locale));
129                 }
130             }
131         }
132     }
133 
134     // BEGIN Android-added: Get non-custom system canonical time zone Id from ICU.
getSystemCanonicalID(String zid)135     private static String getSystemCanonicalID(String zid) {
136         if (android.icu.util.TimeZone.UNKNOWN_ZONE_ID.equals(zid)) {
137             return zid;
138         }
139         boolean[] isSystemID = { false };
140         String canonicalID = android.icu.util.TimeZone.getCanonicalID(zid, isSystemID);
141         if (canonicalID == null || !isSystemID[0]) {
142             return null;
143         }
144         return canonicalID;
145     }
146     // END Android-added: Get non-custom system canonical time zone Id from ICU.
147 
printText(Locale locale, ZonedDateTime zdt, TextStyle style, TimeZone zone, String expected)148     private void printText(Locale locale, ZonedDateTime zdt, TextStyle style, TimeZone zone, String expected) {
149         String result = getFormatter(locale, style).format(zdt);
150         // Android-changed: TimeZone.getDisplayName() will never return "GMT".
151         if (result.startsWith("GMT") && expected.equals("GMT+00:00")) {
152             return;
153         }
154         if (!result.equals(expected)) {
155             if (result.equals("FooLocation")) { // from rules provider test if same vm
156                 return;
157             }
158             System.out.println("----------------");
159             System.out.printf("tdz[%s]%n", zdt.toString());
160             System.out.printf("[%-5s, %5s] :[%s]%n", locale.toString(), style.toString(),result);
161             System.out.printf(" %5s, %5s  :[%s] %s%n", "", "", expected, zone);
162         }
163         assertEquals(result, expected);
164     }
165 
166     // Android-changed: disable test as it doesn't assert anything and produces a lot of output.
167     @Test(enabled = false)
test_ParseText()168     public void test_ParseText() {
169         Locale[] locales = new Locale[] { Locale.ENGLISH, Locale.JAPANESE, Locale.FRENCH };
170         Set<String> zids = ZoneRulesProvider.getAvailableZoneIds();
171         for (Locale locale : locales) {
172             parseText(zids, locale, TextStyle.FULL, false);
173             parseText(zids, locale, TextStyle.FULL, true);
174             parseText(zids, locale, TextStyle.SHORT, false);
175             parseText(zids, locale, TextStyle.SHORT, true);
176         }
177     }
178 
179     private static Set<ZoneId> preferred = new HashSet<>(Arrays.asList(new ZoneId[] {
180         ZoneId.of("EST", ZoneId.SHORT_IDS),
181         ZoneId.of("Asia/Taipei"),
182         ZoneId.of("Asia/Macau"),
183         ZoneId.of("CET"),
184     }));
185 
186     private static Set<ZoneId> preferred_s = new HashSet<>(Arrays.asList(new ZoneId[] {
187          ZoneId.of("EST", ZoneId.SHORT_IDS),
188          ZoneId.of("CET"),
189          ZoneId.of("Australia/South"),
190          ZoneId.of("Australia/West"),
191          ZoneId.of("Asia/Shanghai"),
192     }));
193 
194     private static Set<ZoneId> none = new HashSet<>();
195 
196     @DataProvider(name="preferredZones")
data_preferredZones()197     Object[][] data_preferredZones() {
198         // Android-changed: Differences in time zone name handling.
199         // Android and java.time (via the RI) have differences in how they handle Time Zone Names.
200         // - Android doesn't use IANA abbreviates (usually 3-letter abbreviations) except where they
201         //   are widely used in a given locale (so CST will not resolve to "Chinese Standard Time").
202         // - Android doesn't provide long names for zones like "CET". Only the Olson IDs like
203         //   "Europe/London" have names attached to them.
204         // - When no preferred zones are provided then no guarantee is made about the specific zone
205         //   returned.
206         // - Android uses the display name "Taipei Standard Time" as CLDR does.
207         // Basically Android time zone parsing sticks strictly to what can be done with the data
208         // provided by IANA and CLDR and avoids introducing additional values (like specific order
209         // and additional names) to those.
210         return new Object[][] {
211             // {"America/New_York", "Eastern Standard Time", none,      Locale.ENGLISH, TextStyle.FULL},
212 //          {"EST",              "Eastern Standard Time", preferred, Locale.ENGLISH, TextStyle.FULL},
213             // {"Europe/Paris",     "Central European Time", none,      Locale.ENGLISH, TextStyle.FULL},
214 //          {"CET",              "Central European Time", preferred, Locale.ENGLISH, TextStyle.FULL}, no three-letter ID in CLDR
215             // {"Asia/Shanghai",    "China Standard Time",   none,      Locale.ENGLISH, TextStyle.FULL},
216             {"Asia/Macau",       "China Standard Time",   preferred, Locale.ENGLISH, TextStyle.FULL},
217             // {"Asia/Taipei",      "China Standard Time",   preferred, Locale.ENGLISH, TextStyle.FULL},
218             // {"America/Chicago",  "CST",                   none,      Locale.ENGLISH, TextStyle.SHORT},
219             // {"Asia/Taipei",      "CST",                   preferred, Locale.ENGLISH, TextStyle.SHORT},
220             // Australia/South is a valid synonym for Australia/Adelaide, so this test will pass.
221             {"Australia/South",  "ACST",                  preferred_s, new Locale("en", "AU"), TextStyle.SHORT},
222             // {"America/Chicago",  "CDT",                   none,        Locale.ENGLISH, TextStyle.SHORT},
223             // {"Asia/Shanghai",    "CDT",                   preferred_s, Locale.ENGLISH, TextStyle.SHORT},
224             // {"America/Juneau",   "AKST",                  none,      Locale.ENGLISH, TextStyle.SHORT},
225             // {"America/Juneau",   "AKDT",                  none,      Locale.ENGLISH, TextStyle.SHORT},
226             {"Pacific/Honolulu", "HST",                   none,      Locale.ENGLISH, TextStyle.SHORT},
227             // {"America/Halifax",  "AST",                   none,      Locale.ENGLISH, TextStyle.SHORT},
228             {"Z",                "Z",                     none,      Locale.ENGLISH, TextStyle.SHORT},
229             {"Z",                "Z",                     none,      Locale.US,      TextStyle.SHORT},
230             {"Z",                "Z",                     none,      Locale.CANADA,  TextStyle.SHORT},
231        };
232     }
233 
234     @Test(dataProvider="preferredZones")
test_ParseText(String expected, String text, Set<ZoneId> preferred, Locale locale, TextStyle style)235     public void test_ParseText(String expected, String text, Set<ZoneId> preferred, Locale locale, TextStyle style) {
236         DateTimeFormatter fmt = new DateTimeFormatterBuilder().appendZoneText(style, preferred)
237                                                               .toFormatter(locale)
238                                                               .withDecimalStyle(DecimalStyle.of(locale));
239 
240         String ret = fmt.parse(text, TemporalQueries.zone()).getId();
241 
242         System.out.printf("[%-5s %s] %24s -> %s(%s)%n",
243                           locale.toString(),
244                           style == TextStyle.FULL ? " full" :"short",
245                           text, ret, expected);
246 
247         assertEquals(ret, expected);
248 
249     }
250 
251 
parseText(Set<String> zids, Locale locale, TextStyle style, boolean ci)252     private void parseText(Set<String> zids, Locale locale, TextStyle style, boolean ci) {
253         System.out.println("---------------------------------------");
254         DateTimeFormatter fmt = getFormatter(locale, style, ci);
255         for (String[] names : new DateFormatSymbols(locale).getZoneStrings()) {
256             if (!zids.contains(names[0])) {
257                 continue;
258             }
259             String zid = names[0];
260             String expected = ZoneName.toZid(zid, locale);
261 
262             parse(fmt, zid, expected, zid, locale, style, ci);
263             int i = style == TextStyle.FULL ? 1 : 2;
264             for (; i < names.length; i += 2) {
265                 parse(fmt, zid, expected, names[i], locale, style, ci);
266             }
267         }
268     }
269 
parse(DateTimeFormatter fmt, String zid, String expected, String text, Locale locale, TextStyle style, boolean ci)270     private void parse(DateTimeFormatter fmt,
271                        String zid, String expected, String text,
272                        Locale locale, TextStyle style, boolean ci) {
273         if (ci) {
274             text = text.toUpperCase();
275         }
276         String ret = fmt.parse(text, TemporalQueries.zone()).getId();
277         // TBD: need an excluding list
278         // assertEquals(...);
279         if (ret.equals(expected) ||
280             ret.equals(zid) ||
281             ret.equals(ZoneName.toZid(zid)) ||
282             ret.equals(expected.replace("UTC", "UCT"))) {
283             return;
284         }
285         System.out.printf("[%-5s %s %s %16s] %24s -> %s(%s)%n",
286                           locale.toString(),
287                           ci ? "ci" : "  ",
288                           style == TextStyle.FULL ? " full" :"short",
289                           zid, text, ret, expected);
290     }
291 
getFormatter(Locale locale, TextStyle style, boolean ci)292     private DateTimeFormatter getFormatter(Locale locale, TextStyle style, boolean ci) {
293         DateTimeFormatterBuilder db = new DateTimeFormatterBuilder();
294         if (ci) {
295             db = db.parseCaseInsensitive();
296         }
297         return db.appendZoneText(style)
298                  .toFormatter(locale)
299                  .withDecimalStyle(DecimalStyle.of(locale));
300     }
301 
302 }
303