• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package android.tzdata.mts;
17 
18 import static org.junit.Assert.assertEquals;
19 import static org.junit.Assert.assertFalse;
20 import static org.junit.Assert.assertTrue;
21 
22 import android.icu.text.TimeZoneNames;
23 
24 import org.junit.Test;
25 
26 import java.time.ZoneId;
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.Collection;
30 import java.util.Date;
31 import java.util.List;
32 import java.util.Locale;
33 import java.util.Set;
34 import java.util.TimeZone;
35 import java.util.concurrent.TimeUnit;
36 
37 /**
38  * Tests relating to time zone rules that could be changed by the time zone data module. These are
39  * intended to prove that a time zone data module update hasn't broken behavior. Since time zone
40  * rule mutate over time this test could be quite brittle, so it is suggested that only a few
41  * examples are tested.
42  */
43 public class TimeZoneRulesTest {
44 
45     @Test
preHistoricInDaylightTime()46     public void preHistoricInDaylightTime() {
47         // A zone that lacks an explicit transition at Integer.MIN_VALUE with zic 2023a and 2024b
48         // data.
49         TimeZone tz = TimeZone.getTimeZone("CET");
50 
51         long firstTransitionTimeMillis = -1693702800000L; // Apr 30, 1916 23:00:00 GMT
52         assertEquals(7200000L, tz.getOffset(firstTransitionTimeMillis));
53         assertTrue(tz.inDaylightTime(new Date(firstTransitionTimeMillis)));
54 
55         long beforeFirstTransitionTimeMillis = firstTransitionTimeMillis - 1;
56         assertEquals(3600000L, tz.getOffset(beforeFirstTransitionTimeMillis));
57         assertFalse(tz.inDaylightTime(new Date(beforeFirstTransitionTimeMillis)));
58     }
59 
60     @Test
getDisplayNameShort_nonHourOffsets()61     public void getDisplayNameShort_nonHourOffsets() {
62         TimeZone iranTz = TimeZone.getTimeZone("Asia/Tehran");
63         assertEquals("GMT+03:30", iranTz.getDisplayName(false, TimeZone.SHORT, Locale.UK));
64 
65         TimeZone chathamTz = TimeZone.getTimeZone("Pacific/Chatham");
66         assertEquals("GMT+12:45", chathamTz.getDisplayName(false, TimeZone.SHORT, Locale.UK));
67         assertEquals("GMT+13:45", chathamTz.getDisplayName(true, TimeZone.SHORT, Locale.UK));
68     }
69 
70     @Test
minimalTransitionZones()71     public void minimalTransitionZones() throws Exception {
72         // Zones with minimal transitions, historical or future, seem ideal for testing.
73         // UTC is also included, although it may be implemented differently from the others.
74         String[] ids = new String[] { "Africa/Bujumbura", "Indian/Cocos", "Pacific/Wake", "UTC" };
75         for (String id : ids) {
76             TimeZone tz = TimeZone.getTimeZone(id);
77             assertFalse(tz.useDaylightTime());
78             assertFalse(tz.inDaylightTime(new Date(Integer.MIN_VALUE)));
79             assertFalse(tz.inDaylightTime(new Date(0)));
80             assertFalse(tz.inDaylightTime(new Date(Integer.MAX_VALUE)));
81             int currentOffset = tz.getOffset(new Date(0).getTime());
82             assertEquals(currentOffset, tz.getOffset(new Date(Integer.MIN_VALUE).getTime()));
83             assertEquals(currentOffset, tz.getOffset(new Date(Integer.MAX_VALUE).getTime()));
84         }
85     }
86 
87     @Test
getDSTSavings()88     public void getDSTSavings() throws Exception {
89       assertEquals(0, TimeZone.getTimeZone("UTC").getDSTSavings());
90       assertEquals(3600000, TimeZone.getTimeZone("America/Los_Angeles").getDSTSavings());
91       assertEquals(1800000, TimeZone.getTimeZone("Australia/Lord_Howe").getDSTSavings());
92     }
93 
94     // http://b/7955614 and http://b/8026776.
95     @Test
displayNames()96     public void displayNames() throws Exception {
97         checkDisplayNames(Locale.US);
98     }
99 
100     @Test
displayNames_nonUS()101     public void displayNames_nonUS() throws Exception {
102         // run checkDisplayNames with an arbitrary set of Locales.
103         checkDisplayNames(Locale.CHINESE);
104         checkDisplayNames(Locale.FRENCH);
105         checkDisplayNames(Locale.forLanguageTag("bn-BD"));
106     }
107 
checkDisplayNames(Locale locale)108     private static void checkDisplayNames(Locale locale) throws Exception {
109         // Check that there are no time zones that use DST but have the same display name for
110         // both standard and daylight time.
111         StringBuilder failures = new StringBuilder();
112         for (String id : TimeZone.getAvailableIDs()) {
113             TimeZone tz = TimeZone.getTimeZone(id);
114             String longDst = tz.getDisplayName(true, TimeZone.LONG, locale);
115             String longStd = tz.getDisplayName(false, TimeZone.LONG, locale);
116             String shortDst = tz.getDisplayName(true, TimeZone.SHORT, locale);
117             String shortStd = tz.getDisplayName(false, TimeZone.SHORT, locale);
118 
119             if (tz.useDaylightTime()) {
120                 // The long std and dst strings must differ!
121                 if (longDst.equals(longStd)) {
122                     failures.append(String.format("\n%20s: LD='%s' LS='%s'!",
123                                                   id, longDst, longStd));
124                 }
125                 // The short std and dst strings must differ!
126                 if (shortDst.equals(shortStd)) {
127                     failures.append(String.format("\n%20s: SD='%s' SS='%s'!",
128                                                   id, shortDst, shortStd));
129                 }
130 
131                 // If the short std matches the long dst, or the long std matches the short dst,
132                 // it probably means we have a time zone that icu4c doesn't believe has ever
133                 // observed dst.
134                 if (shortStd.equals(longDst)) {
135                     failures.append(String.format("\n%20s: SS='%s' LD='%s'!",
136                                                   id, shortStd, longDst));
137                 }
138                 if (longStd.equals(shortDst)) {
139                     failures.append(String.format("\n%20s: LS='%s' SD='%s'!",
140                                                   id, longStd, shortDst));
141                 }
142 
143                 // The long and short dst strings must differ!
144                 if (longDst.equals(shortDst) && !longDst.startsWith("GMT")) {
145                   failures.append(String.format("\n%20s: LD='%s' SD='%s'!",
146                                                 id, longDst, shortDst));
147                 }
148             }
149 
150             // Confidence check that whenever a display name is just a GMT string that it's the
151             // right GMT string.
152             String gmtDst = formatGmtString(tz, true);
153             String gmtStd = formatGmtString(tz, false);
154             if (isGmtString(longDst) && !longDst.equals(gmtDst)) {
155                 failures.append(String.format("\n%s: LD %s", id, longDst));
156             }
157             if (isGmtString(longStd) && !longStd.equals(gmtStd)) {
158                 failures.append(String.format("\n%s: LS %s", id, longStd));
159             }
160             if (isGmtString(shortDst) && !shortDst.equals(gmtDst)) {
161                 failures.append(String.format("\n%s: SD %s", id, shortDst));
162             }
163             if (isGmtString(shortStd) && !shortStd.equals(gmtStd)) {
164                 failures.append(String.format("\n%s: SS %s", id, shortStd));
165             }
166         }
167         assertEquals("", failures.toString());
168     }
169 
isGmtString(String s)170     private static boolean isGmtString(String s) {
171         return s.startsWith("GMT+") || s.startsWith("GMT-");
172     }
173 
formatGmtString(TimeZone tz, boolean daylight)174     private static String formatGmtString(TimeZone tz, boolean daylight) {
175         int offset = tz.getRawOffset();
176         if (daylight) {
177             offset += tz.getDSTSavings();
178         }
179         offset /= 60000;
180         char sign = '+';
181         if (offset < 0) {
182             sign = '-';
183             offset = -offset;
184         }
185         return String.format("GMT%c%02d:%02d", sign, offset / 60, offset % 60);
186     }
187 
188     /**
189      * This test is to catch issues with the rules update process that could let the
190      * "negative DST" scheme enter the Android data set for either java.util.TimeZone or
191      * android.icu.util.TimeZone.
192      */
193     @Test
dstMeansSummer()194     public void dstMeansSummer() {
195         // Ireland was the original example that caused the default IANA upstream tzdata to contain
196         // a zone where DST is in the Winter (since tzdata 2018e, though it was tried in 2018a
197         // first). This change was made to historical and future transitions.
198         //
199         // The upstream reasoning went like this: "Irish *Standard* Time" is summer, so the other
200         // time must be the DST. So, DST is considered to be in the winter and the associated DST
201         // adjustment is negative from the standard time. In the old scheme "Irish Standard Time" /
202         // summer was just modeled as the DST in common with all other global time zones.
203         //
204         // Unfortunately, various users of formatting APIs assume standard and DST times are
205         // consistent and (effectively) that "DST" means "summer". We likely cannot adopt the
206         // concept of a winter DST without risking app compat issues.
207         //
208         // For example, getDisplayName(boolean daylight) has always returned the winter time for
209         // false, and the summer time for true. If we change this then it should be changed on a
210         // major release boundary, with improved APIs (e.g. a version of getDisplayName() that takes
211         // a millis), existing API behavior made dependent on target API version, and after fixing
212         // any platform code that makes incorrect assumptions about DST meaning "1 hour forward".
213 
214         final String timeZoneId = "Europe/Dublin";
215         final Locale locale = Locale.UK;
216         // 26 Oct 2015 01:00:00 GMT - one day after the start of "Greenwich Mean Time" in
217         // Europe/Dublin in 2015. An arbitrary historical example of winter in Ireland.
218         final long winterTimeMillis = 1445821200000L;
219         final String winterTimeName = "Greenwich Mean Time";
220         final int winterOffsetRawMillis = 0;
221         final int winterOffsetDstMillis = 0;
222 
223         // 30 Mar 2015 01:00:00 GMT - one day after the start of "Irish Standard Time" in
224         // Europe/Dublin in 2015. An arbitrary historical example of summer in Ireland.
225         final long summerTimeMillis = 1427677200000L;
226         final String summerTimeName = "Irish Standard Time";
227         final int summerOffsetRawMillis = 0;
228         final int summerOffsetDstMillis = (int) TimeUnit.HOURS.toMillis(1);
229 
230         // There is no common interface between java.util.TimeZone and android.icu.util.TimeZone
231         // so the tests are for each are effectively duplicated.
232 
233         // java.util.TimeZone
234         {
235             java.util.TimeZone timeZone = java.util.TimeZone.getTimeZone(timeZoneId);
236             assertTrue(timeZone.useDaylightTime());
237 
238             assertFalse(timeZone.inDaylightTime(new Date(winterTimeMillis)));
239             assertTrue(timeZone.inDaylightTime(new Date(summerTimeMillis)));
240 
241             assertEquals(winterOffsetRawMillis + winterOffsetDstMillis,
242                     timeZone.getOffset(winterTimeMillis));
243             assertEquals(summerOffsetRawMillis + summerOffsetDstMillis,
244                     timeZone.getOffset(summerTimeMillis));
245             assertEquals(winterTimeName,
246                     timeZone.getDisplayName(false /* daylight */, java.util.TimeZone.LONG,
247                             locale));
248             assertEquals(summerTimeName,
249                     timeZone.getDisplayName(true /* daylight */, java.util.TimeZone.LONG,
250                             locale));
251         }
252 
253         // android.icu.util.TimeZone
254         {
255             android.icu.util.TimeZone timeZone = android.icu.util.TimeZone.getTimeZone(timeZoneId);
256             assertTrue(timeZone.useDaylightTime());
257 
258             assertFalse(timeZone.inDaylightTime(new Date(winterTimeMillis)));
259             assertTrue(timeZone.inDaylightTime(new Date(summerTimeMillis)));
260 
261             assertEquals(winterOffsetRawMillis + winterOffsetDstMillis,
262                     timeZone.getOffset(winterTimeMillis));
263             assertEquals(summerOffsetRawMillis + summerOffsetDstMillis,
264                     timeZone.getOffset(summerTimeMillis));
265 
266             // These methods show the trouble we'd have if callers were to take the output from
267             // inDaylightTime() and pass it to getDisplayName().
268             assertEquals(winterTimeName,
269                     timeZone.getDisplayName(false /* daylight */, android.icu.util.TimeZone.LONG,
270                             locale));
271             assertEquals(summerTimeName,
272                     timeZone.getDisplayName(true /* daylight */, android.icu.util.TimeZone.LONG,
273                             locale));
274 
275             // APIs not identical to java.util.TimeZone tested below.
276             int[] offsets = new int[2];
277             timeZone.getOffset(winterTimeMillis, false /* local */, offsets);
278             assertEquals(winterOffsetRawMillis, offsets[0]);
279             assertEquals(winterOffsetDstMillis, offsets[1]);
280 
281             timeZone.getOffset(summerTimeMillis, false /* local */, offsets);
282             assertEquals(summerOffsetRawMillis, offsets[0]);
283             assertEquals(summerOffsetDstMillis, offsets[1]);
284         }
285 
286         // icu TimeZoneNames
287         TimeZoneNames timeZoneNames = TimeZoneNames.getInstance(locale);
288         // getDisplayName: date = winterTimeMillis
289         assertEquals(winterTimeName, timeZoneNames.getDisplayName(
290                 timeZoneId, TimeZoneNames.NameType.LONG_STANDARD, winterTimeMillis));
291         assertEquals(summerTimeName, timeZoneNames.getDisplayName(
292                 timeZoneId, TimeZoneNames.NameType.LONG_DAYLIGHT, winterTimeMillis));
293         // getDisplayName: date = summerTimeMillis
294         assertEquals(winterTimeName, timeZoneNames.getDisplayName(
295                 timeZoneId, TimeZoneNames.NameType.LONG_STANDARD, summerTimeMillis));
296         assertEquals(summerTimeName, timeZoneNames.getDisplayName(
297                 timeZoneId, TimeZoneNames.NameType.LONG_DAYLIGHT, summerTimeMillis));
298     }
299 
300     /**
301      * ICU's time zone IDs may be a superset of IDs available via other APIs.
302      */
303     @Test
timeZoneIdsKnown()304     public void timeZoneIdsKnown() {
305         // java.util
306         List<String> zoneInfoDbAvailableIds = Arrays.asList(java.util.TimeZone.getAvailableIDs());
307         checkZoneIdsAreKnownToIcu(zoneInfoDbAvailableIds);
308 
309         // java.time
310         checkZoneIdsAreKnownToIcu(ZoneId.getAvailableZoneIds());
311     }
312 
checkZoneIdsAreKnownToIcu(Collection<String> zoneInfoDbAvailableIds)313     private static void checkZoneIdsAreKnownToIcu(Collection<String> zoneInfoDbAvailableIds) {
314         // ICU has a known set of IDs. We want ANY because we don't want to filter to ICU's
315         // canonical IDs only.
316         Set<String> icuAvailableIds = android.icu.util.TimeZone.getAvailableIDs(
317                 android.icu.util.TimeZone.SystemTimeZoneType.ANY, null /* region */,
318                 null /* rawOffset */);
319 
320         List<String> nonIcuAvailableIds = new ArrayList<>();
321         List<String> creationFailureIds = new ArrayList<>();
322         List<String> noCanonicalLookupIds = new ArrayList<>();
323         List<String> nonSystemIds = new ArrayList<>();
324         for (String zoneInfoDbId : zoneInfoDbAvailableIds) {
325             if (!icuAvailableIds.contains(zoneInfoDbId)) {
326                 nonIcuAvailableIds.add(zoneInfoDbId);
327             }
328 
329             boolean[] isSystemId = new boolean[1];
330             String canonicalId = android.icu.util.TimeZone.getCanonicalID(zoneInfoDbId, isSystemId);
331             if (canonicalId == null) {
332                 noCanonicalLookupIds.add(zoneInfoDbId);
333             }
334             if (!isSystemId[0]) {
335                 nonSystemIds.add(zoneInfoDbId);
336             }
337 
338             android.icu.util.TimeZone icuTimeZone =
339                     android.icu.util.TimeZone.getTimeZone(zoneInfoDbId);
340             if (icuTimeZone.getID().equals(android.icu.util.TimeZone.UNKNOWN_ZONE_ID)) {
341                 creationFailureIds.add(zoneInfoDbId);
342             }
343         }
344         assertTrue("Non-ICU available IDs: " + nonIcuAvailableIds
345                         + ", creation failed IDs: " + creationFailureIds
346                         + ", non-system IDs: " + nonSystemIds
347                         + ", ids without canonical IDs: " + noCanonicalLookupIds,
348                 nonIcuAvailableIds.isEmpty()
349                         && creationFailureIds.isEmpty()
350                         && nonSystemIds.isEmpty()
351                         && noCanonicalLookupIds.isEmpty());
352     }
353 }
354