• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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 
17 package com.android.i18n.test.timezone;
18 
19 import org.junit.Test;
20 
21 import android.icu.testsharding.MainTestShard;
22 import android.icu.util.TimeZone;
23 
24 import java.time.Instant;
25 import java.util.ArrayList;
26 import java.util.Arrays;
27 import java.util.List;
28 import java.util.Objects;
29 import java.util.function.Function;
30 import java.util.stream.Collectors;
31 import com.android.i18n.timezone.CountryTimeZones;
32 import com.android.i18n.timezone.CountryTimeZones.OffsetResult;
33 import com.android.i18n.timezone.CountryTimeZones.TimeZoneMapping;
34 
35 import static org.junit.Assert.assertEquals;
36 import static org.junit.Assert.assertFalse;
37 import static org.junit.Assert.assertNull;
38 import static org.junit.Assert.assertTrue;
39 import static org.junit.Assert.fail;
40 
41 @MainTestShard
42 public class CountryTimeZonesTest {
43 
44     private static final int HOUR_MILLIS = 60 * 60 * 1000;
45 
46     private static final String INVALID_TZ_ID = "Moon/Tranquility_Base";
47 
48     // Zones used in the tests. NY_TZ and LON_TZ chosen because they never overlap but both have
49     // DST.
50     private static final TimeZone NY_TZ = TimeZone.getTimeZone("America/New_York");
51     private static final TimeZone LON_TZ = TimeZone.getTimeZone("Europe/London");
52     // A zone that matches LON_TZ for WHEN_NO_DST. It does not have DST so differs for WHEN_DST.
53     private static final TimeZone REYK_TZ = TimeZone.getTimeZone("Atlantic/Reykjavik");
54     // Another zone that matches LON_TZ for WHEN_NO_DST. It does not have DST so differs for
55     // WHEN_DST.
56     private static final TimeZone UTC_TZ = TimeZone.getTimeZone("Etc/UTC");
57 
58     // 22nd July 2017, 13:14:15 UTC (DST time in all the timezones used in these tests that observe
59     // DST).
60     private static final long WHEN_DST = 1500729255000L;
61     // 22nd January 2018, 13:14:15 UTC (non-DST time in all timezones used in these tests).
62     private static final long WHEN_NO_DST = 1516626855000L;
63 
64     // The offset applied to most zones during DST.
65     private static final int NORMAL_DST_ADJUSTMENT = HOUR_MILLIS;
66 
67     private static final int LON_NO_DST_TOTAL_OFFSET = 0;
68     private static final int LON_DST_TOTAL_OFFSET = LON_NO_DST_TOTAL_OFFSET
69             + NORMAL_DST_ADJUSTMENT;
70 
71     private static final int NY_NO_DST_TOTAL_OFFSET = -5 * HOUR_MILLIS;
72     private static final int NY_DST_TOTAL_OFFSET = NY_NO_DST_TOTAL_OFFSET
73             + NORMAL_DST_ADJUSTMENT;
74 
75     @Test
createValidated()76     public void createValidated() throws Exception {
77         CountryTimeZones countryTimeZones = CountryTimeZones.createValidated(
78                 "gb", "Europe/London", false /* defaultTimeZoneBoost */,
79                 true /* everUsesUtc */, timeZoneMappings("Europe/London"), "test");
80         assertTrue(countryTimeZones.matchesCountryCode("gb"));
81         assertEquals("Europe/London", countryTimeZones.getDefaultTimeZoneId());
82         assertZoneEquals(zone("Europe/London"), countryTimeZones.getDefaultTimeZone());
83         assertEquals(timeZoneMappings("Europe/London"), countryTimeZones.getTimeZoneMappings());
84         assertEquals(timeZoneMappings("Europe/London"),
85                 countryTimeZones.getEffectiveTimeZoneMappingsAt(0 /* whenMillis */));
86     }
87 
88     @Test
createValidated_nullDefault()89     public void createValidated_nullDefault() throws Exception {
90         CountryTimeZones countryTimeZones = CountryTimeZones.createValidated(
91                 "gb", null, false /* defaultTimeZoneBoost */, true /* everUsesUtc */,
92                 timeZoneMappings("Europe/London"), "test");
93         assertNull(countryTimeZones.getDefaultTimeZoneId());
94         assertNull(countryTimeZones.getDefaultTimeZone());
95     }
96 
97     @Test
createValidated_invalidDefault()98     public void createValidated_invalidDefault() throws Exception {
99         CountryTimeZones countryTimeZones = CountryTimeZones.createValidated(
100                 "gb", INVALID_TZ_ID, false /* defaultTimeZoneBoost */, true /* everUsesUtc */,
101                 timeZoneMappings("Europe/London", INVALID_TZ_ID), "test");
102         assertNull(countryTimeZones.getDefaultTimeZoneId());
103         assertNull(countryTimeZones.getDefaultTimeZone());
104         assertEquals(timeZoneMappings("Europe/London"), countryTimeZones.getTimeZoneMappings());
105         assertEquals(timeZoneMappings("Europe/London"),
106                 countryTimeZones.getEffectiveTimeZoneMappingsAt(0 /* whenMillis */));
107     }
108 
109     @Test
createValidated_unknownTimeZoneIdIgnored()110     public void createValidated_unknownTimeZoneIdIgnored() throws Exception {
111         CountryTimeZones countryTimeZones = CountryTimeZones.createValidated(
112                 "gb", "Europe/London", false /* defaultTimeZoneBoost */, true /* everUsesUtc */,
113                 timeZoneMappings("Unknown_Id", "Europe/London"), "test");
114         assertEquals(timeZoneMappings("Europe/London"), countryTimeZones.getTimeZoneMappings());
115         assertEquals(timeZoneMappings("Europe/London"),
116                 countryTimeZones.getEffectiveTimeZoneMappingsAt(0 /* whenMillis */));
117     }
118 
119     @Test
matchesCountryCode()120     public void matchesCountryCode() throws Exception {
121         CountryTimeZones countryTimeZones = CountryTimeZones.createValidated(
122                 "gb", "Europe/London", false /* defaultTimeZoneBoost */, true /* everUsesUtc */,
123                 timeZoneMappings("Europe/London"), "test");
124         assertTrue(countryTimeZones.matchesCountryCode("GB"));
125         assertTrue(countryTimeZones.matchesCountryCode("Gb"));
126         assertTrue(countryTimeZones.matchesCountryCode("gB"));
127     }
128 
129     @Test
structuresAreImmutable()130     public void structuresAreImmutable() throws Exception {
131         CountryTimeZones countryTimeZones = CountryTimeZones.createValidated(
132                 "gb", "Europe/London", false /* defaultTimeZoneBoost */, true /* everUsesUtc */,
133                 timeZoneMappings("Europe/London"), "test");
134 
135         assertImmutableTimeZone(countryTimeZones.getDefaultTimeZone());
136 
137         List<TimeZoneMapping> timeZoneMappings = countryTimeZones.getTimeZoneMappings();
138         assertEquals(1, timeZoneMappings.size());
139         assertImmutableList(timeZoneMappings);
140 
141         List<TimeZoneMapping> effectiveTimeZoneMappings =
142                 countryTimeZones.getEffectiveTimeZoneMappingsAt(0 /* whenMillis */);
143         assertEquals(1, effectiveTimeZoneMappings.size());
144         assertImmutableList(effectiveTimeZoneMappings);
145     }
146 
147     @Test
lookupByOffsetWithBias_oneCandidate()148     public void lookupByOffsetWithBias_oneCandidate() throws Exception {
149         CountryTimeZones countryTimeZones = CountryTimeZones.createValidated(
150                 "gb", "Europe/London", false /* defaultTimeZoneBoost */, true /* everUsesUtc */,
151                 timeZoneMappings("Europe/London"), "test");
152 
153         OffsetResult lonMatch = new OffsetResult(LON_TZ, true /* oneMatch */);
154 
155         // Placeholder constants to improve test case readability.
156         final Boolean isDst = true;
157         final Boolean notDst = false;
158         final Boolean unkIsDst = null;
159         final TimeZone noBias = null;
160         final OffsetResult noMatch = null;
161 
162         Object[][] testCases = new Object[][] {
163                 // totalOffsetMillis, isDst, whenMillis, bias, expectedMatch
164 
165                 // The parameters match the zone: total offset and time.
166                 { LON_DST_TOTAL_OFFSET, unkIsDst, WHEN_DST, noBias, lonMatch },
167                 { LON_NO_DST_TOTAL_OFFSET, unkIsDst, WHEN_NO_DST, noBias, lonMatch },
168 
169                 // The parameters match the zone: total offset, isDst and time.
170                 { LON_DST_TOTAL_OFFSET, isDst, WHEN_DST, noBias, lonMatch },
171                 { LON_NO_DST_TOTAL_OFFSET, notDst, WHEN_NO_DST, noBias, lonMatch },
172 
173                 // Some lookup failure cases where the total offset, isDst and time do not match the
174                 // zone.
175                 { LON_DST_TOTAL_OFFSET, isDst, WHEN_NO_DST, noBias, noMatch },
176                 { LON_DST_TOTAL_OFFSET, notDst, WHEN_NO_DST, noBias, noMatch },
177                 { LON_NO_DST_TOTAL_OFFSET, isDst, WHEN_DST, noBias, noMatch },
178                 { LON_NO_DST_TOTAL_OFFSET, isDst, WHEN_NO_DST, noBias, noMatch },
179                 { LON_DST_TOTAL_OFFSET, notDst, WHEN_DST, noBias, noMatch },
180                 { LON_NO_DST_TOTAL_OFFSET, notDst, WHEN_DST, noBias, noMatch },
181 
182                 // Some bias cases below.
183 
184                 // The bias is irrelevant here: it matches what would be returned anyway.
185                 { LON_DST_TOTAL_OFFSET, isDst, WHEN_DST, LON_TZ, lonMatch },
186                 { LON_NO_DST_TOTAL_OFFSET, notDst, WHEN_NO_DST, LON_TZ, lonMatch },
187 
188                 // A sample of a non-matching case with bias.
189                 { LON_DST_TOTAL_OFFSET, isDst, WHEN_NO_DST, LON_TZ, noMatch },
190 
191                 // The bias should be ignored: it doesn't match any of the country's zones.
192                 { LON_DST_TOTAL_OFFSET, isDst, WHEN_DST, NY_TZ, lonMatch },
193 
194                 // The bias should still be ignored even though it matches the offset information
195                 // given it doesn't match any of the country's zones.
196                 { NY_DST_TOTAL_OFFSET, isDst, WHEN_DST, NY_TZ, noMatch },
197         };
198         executeLookupByOffsetWithBiasTestCases(countryTimeZones, testCases);
199     }
200 
201     @Test
lookupByOffsetWithBias_multipleNonOverlappingCandidates()202     public void lookupByOffsetWithBias_multipleNonOverlappingCandidates() throws Exception {
203         CountryTimeZones countryTimeZones = CountryTimeZones.createValidated(
204                 "xx", "Europe/London", false /* defaultTimeZoneBoost */, true /* everUsesUtc */,
205                 timeZoneMappings("America/New_York", "Europe/London"), "test");
206 
207         OffsetResult lonMatch = new OffsetResult(LON_TZ, true /* oneMatch */);
208         OffsetResult nyMatch = new OffsetResult(NY_TZ, true /* oneMatch */);
209 
210         // Placeholder constants to improve test case readability.
211         final Boolean isDst = true;
212         final Boolean notDst = false;
213         final Boolean unkIsDst = null;
214         final TimeZone noBias = null;
215         final OffsetResult noMatch = null;
216 
217         Object[][] testCases = new Object[][] {
218                 // totalOffsetMillis, isDst, dstOffsetMillis, whenMillis, bias, expectedMatch
219 
220                 // The parameters match the zone: total offset and time.
221                 { LON_DST_TOTAL_OFFSET, unkIsDst, WHEN_DST, noBias, lonMatch },
222                 { LON_NO_DST_TOTAL_OFFSET, unkIsDst, WHEN_NO_DST, noBias, lonMatch },
223                 { NY_NO_DST_TOTAL_OFFSET, unkIsDst, WHEN_NO_DST, noBias, nyMatch },
224                 { NY_DST_TOTAL_OFFSET, unkIsDst, WHEN_DST, noBias, nyMatch },
225 
226                 // The parameters match the zone: total offset, isDst and time.
227                 { LON_DST_TOTAL_OFFSET, isDst, WHEN_DST, noBias, lonMatch },
228                 { LON_NO_DST_TOTAL_OFFSET, notDst, WHEN_NO_DST, noBias, lonMatch },
229                 { NY_DST_TOTAL_OFFSET, isDst, WHEN_DST, noBias, nyMatch },
230                 { NY_NO_DST_TOTAL_OFFSET, notDst, WHEN_NO_DST, noBias, nyMatch },
231 
232                 // Some lookup failure cases where the total offset, isDst and time do not match the
233                 // zone. This is a sample, not complete.
234                 { LON_DST_TOTAL_OFFSET, isDst, WHEN_NO_DST, noBias, noMatch },
235                 { LON_DST_TOTAL_OFFSET, unkIsDst, WHEN_NO_DST, noBias, noMatch },
236                 { LON_DST_TOTAL_OFFSET, notDst, WHEN_DST, noBias, noMatch },
237                 { LON_NO_DST_TOTAL_OFFSET, isDst, WHEN_DST, noBias, noMatch },
238                 { LON_NO_DST_TOTAL_OFFSET, isDst, WHEN_NO_DST, noBias, noMatch },
239                 { LON_NO_DST_TOTAL_OFFSET, unkIsDst, WHEN_DST, noBias, noMatch },
240                 { LON_NO_DST_TOTAL_OFFSET, notDst, WHEN_DST, noBias, noMatch },
241 
242                 // Some bias cases below.
243 
244                 // The bias is irrelevant here: it matches what would be returned anyway.
245                 { LON_DST_TOTAL_OFFSET, isDst, WHEN_DST, LON_TZ, lonMatch },
246                 { LON_DST_TOTAL_OFFSET, unkIsDst, WHEN_DST, LON_TZ, lonMatch },
247                 { LON_NO_DST_TOTAL_OFFSET, unkIsDst, WHEN_NO_DST, LON_TZ, lonMatch },
248 
249                 // A sample of non-matching cases with bias.
250                 { LON_NO_DST_TOTAL_OFFSET, isDst, WHEN_NO_DST, LON_TZ, noMatch },
251                 { LON_DST_TOTAL_OFFSET, isDst, WHEN_NO_DST, LON_TZ, noMatch },
252                 { LON_DST_TOTAL_OFFSET, unkIsDst, WHEN_NO_DST, LON_TZ, noMatch },
253 
254                 // The bias should be ignored: it matches a zone, but the offset is wrong so
255                 // should not be considered a match.
256                 { LON_DST_TOTAL_OFFSET, isDst, WHEN_DST, NY_TZ, lonMatch },
257                 { LON_DST_TOTAL_OFFSET, unkIsDst, WHEN_DST, NY_TZ, lonMatch },
258         };
259         executeLookupByOffsetWithBiasTestCases(countryTimeZones, testCases);
260     }
261 
262     // This is an artificial case very similar to America/Denver and America/Phoenix in the US: both
263     // have the same offset for 6 months of the year but diverge. Australia/Lord_Howe too.
264     @Test
lookupByOffsetWithBias_multipleOverlappingCandidates()265     public void lookupByOffsetWithBias_multipleOverlappingCandidates() throws Exception {
266         // Three zones that have the same offset for some of the year. Europe/London changes
267         // offset WHEN_DST, the others do not.
268         CountryTimeZones countryTimeZones = CountryTimeZones.createValidated(
269                 "xx", "Europe/London", false /* defaultTimeZoneBoost */, true /* everUsesUtc */,
270                 timeZoneMappings("Atlantic/Reykjavik", "Europe/London", "Etc/UTC"), "test");
271 
272         // Placeholder constants to improve test case readability.
273         final Boolean isDst = true;
274         final Boolean notDst = false;
275         final Boolean unkIsDst = null;
276         final TimeZone noBias = null;
277         final OffsetResult noMatch = null;
278 
279         // This is the no-DST offset for LON_TZ, REYK_TZ. UTC_TZ.
280         final int noDstTotalOffset = LON_NO_DST_TOTAL_OFFSET;
281         // This is the DST offset for LON_TZ.
282         final int dstTotalOffset = LON_DST_TOTAL_OFFSET;
283 
284         OffsetResult lonOnlyMatch = new OffsetResult(LON_TZ, true /* oneMatch */);
285         OffsetResult lonBestMatch = new OffsetResult(LON_TZ, false /* oneMatch */);
286         OffsetResult reykBestMatch = new OffsetResult(REYK_TZ, false /* oneMatch */);
287         OffsetResult utcBestMatch = new OffsetResult(UTC_TZ, false /* oneMatch */);
288 
289         Object[][] testCases = new Object[][] {
290                 // totalOffsetMillis, isDst, dstOffsetMillis, whenMillis, bias, expectedMatch
291 
292                 // The parameters match one zone: total offset and time.
293                 { dstTotalOffset, unkIsDst, WHEN_DST, noBias, lonOnlyMatch },
294                 { dstTotalOffset, unkIsDst, WHEN_DST, noBias, lonOnlyMatch },
295 
296                 // The parameters match several zones: total offset and time.
297                 { noDstTotalOffset, unkIsDst, WHEN_NO_DST, noBias, reykBestMatch },
298                 { noDstTotalOffset, unkIsDst, WHEN_DST, noBias, reykBestMatch },
299 
300                 // The parameters match one zone: total offset, isDst and time.
301                 { dstTotalOffset, isDst, WHEN_DST, noBias, lonOnlyMatch },
302                 { dstTotalOffset, isDst, WHEN_DST, noBias, lonOnlyMatch },
303 
304                 { noDstTotalOffset, notDst, WHEN_NO_DST, noBias, reykBestMatch },
305                 { noDstTotalOffset, notDst, WHEN_DST, noBias, reykBestMatch },
306 
307                 // Some lookup failure cases where the total offset, isDst and time do not match any
308                 // zone.
309                 { dstTotalOffset, isDst, WHEN_NO_DST, noBias, noMatch },
310                 { dstTotalOffset, unkIsDst, WHEN_NO_DST, noBias, noMatch },
311                 { noDstTotalOffset, isDst, WHEN_NO_DST, noBias, noMatch },
312                 { noDstTotalOffset, isDst, WHEN_DST, noBias, noMatch },
313 
314                 // Some bias cases below.
315 
316                 // Multiple zones match but Reykjavik is the bias.
317                 { noDstTotalOffset, notDst, WHEN_NO_DST, REYK_TZ, reykBestMatch },
318 
319                 // Multiple zones match but London is the bias.
320                 { noDstTotalOffset, notDst, WHEN_NO_DST, LON_TZ, lonBestMatch },
321 
322                 // Multiple zones match but UTC is the bias.
323                 { noDstTotalOffset, notDst, WHEN_NO_DST, UTC_TZ, utcBestMatch },
324 
325                 // The bias should be ignored: it matches a zone, but the offset is wrong so
326                 // should not be considered a match.
327                 { LON_DST_TOTAL_OFFSET, isDst, WHEN_DST, REYK_TZ, lonOnlyMatch },
328                 { LON_DST_TOTAL_OFFSET, unkIsDst, WHEN_DST, REYK_TZ, lonOnlyMatch },
329         };
330         executeLookupByOffsetWithBiasTestCases(countryTimeZones, testCases);
331     }
332 
executeLookupByOffsetWithBiasTestCases( CountryTimeZones countryTimeZones, Object[][] testCases)333     private static void executeLookupByOffsetWithBiasTestCases(
334             CountryTimeZones countryTimeZones, Object[][] testCases) {
335 
336         List<String> failures = new ArrayList<>();
337         for (int i = 0; i < testCases.length; i++) {
338             Object[] testCase = testCases[i];
339             int totalOffsetMillis = (int) testCase[0];
340             Boolean isDst = (Boolean) testCase[1];
341             long whenMillis = (Long) testCase[2];
342             TimeZone bias = (TimeZone) testCase[3];
343             OffsetResult expectedMatch = (OffsetResult) testCase[4];
344 
345             OffsetResult actualMatch;
346             if (isDst == null) {
347                 actualMatch = countryTimeZones.lookupByOffsetWithBias(
348                         whenMillis, bias, totalOffsetMillis);
349             } else {
350                 actualMatch = countryTimeZones.lookupByOffsetWithBias(
351                         whenMillis, bias, totalOffsetMillis, isDst);
352             }
353 
354             if (!offsetResultEquals(expectedMatch, actualMatch)) {
355                 Function<TimeZone, String> timeZoneFormatter =
356                         x -> x == null ? "null" : x.getID();
357                 Function<OffsetResult, String> offsetResultFormatter =
358                         x -> x == null ? "null"
359                                 : "{" + x.getTimeZone().getID() + ", " + x.isOnlyMatch() + "}";
360                 failures.add("Fail: case=" + i
361                         + ", totalOffsetMillis=" + totalOffsetMillis
362                         + ", isDst=" + isDst
363                         + ", whenMillis=" + whenMillis
364                         + ", bias=" + timeZoneFormatter.apply(bias)
365                         + ", expectedMatch=" + offsetResultFormatter.apply(expectedMatch)
366                         + ", actualMatch=" + offsetResultFormatter.apply(actualMatch)
367                         + "\n");
368             }
369         }
370         if (!failures.isEmpty()) {
371             fail("Failed:\n" + failures);
372         }
373     }
374 
375     @Test
getEffectiveTimeZonesAt_noZones()376     public void getEffectiveTimeZonesAt_noZones() {
377         CountryTimeZones countryTimeZones = CountryTimeZones.createValidated(
378                 "xx", "Europe/London", false /* defaultTimeZoneBoost */, true /* everUsesUtc */,
379                 timeZoneMappings(), "test");
380         assertEquals(timeZoneMappings(),
381                 countryTimeZones.getEffectiveTimeZoneMappingsAt(0 /* whenMillis */));
382         assertEquals(timeZoneMappings(),
383                 countryTimeZones.getEffectiveTimeZoneMappingsAt(Long.MIN_VALUE));
384         assertEquals(timeZoneMappings(),
385                 countryTimeZones.getEffectiveTimeZoneMappingsAt(Long.MAX_VALUE));
386     }
387 
388     @Test
getEffectiveTimeZonesAt_oneZone()389     public void getEffectiveTimeZonesAt_oneZone() {
390         CountryTimeZones countryTimeZones = CountryTimeZones.createValidated(
391                 "xx", "Europe/London", false /* defaultTimeZoneBoost */, true /* everUsesUtc */,
392                 timeZoneMappings("Europe/London"), "test");
393         assertEquals(timeZoneMappings("Europe/London"),
394                 countryTimeZones.getEffectiveTimeZoneMappingsAt(0));
395         assertEquals(timeZoneMappings("Europe/London"),
396                 countryTimeZones.getEffectiveTimeZoneMappingsAt(Long.MIN_VALUE));
397         assertEquals(timeZoneMappings("Europe/London"),
398                 countryTimeZones.getEffectiveTimeZoneMappingsAt(Long.MAX_VALUE));
399     }
400 
401     @Test
getEffectiveTimeZonesAt_filtering()402     public void getEffectiveTimeZonesAt_filtering() {
403         TimeZoneMapping alwaysUsed = timeZoneMapping("Europe/London", null /* notUsedAfter */);
404 
405         long mappingNotUsedAfterMillis = 0L;
406         TimeZoneMapping notAlwaysUsed = timeZoneMapping("Europe/Paris",
407                 mappingNotUsedAfterMillis /* notUsedAfter */);
408 
409         List<TimeZoneMapping> timeZoneMappings = list(alwaysUsed, notAlwaysUsed);
410         CountryTimeZones countryTimeZones = CountryTimeZones.createValidated(
411                 "xx", "Europe/London", false /* defaultTimeZoneBoost */, true /* everUsesUtc */,
412                 timeZoneMappings, "test");
413 
414         // Before and at mappingNotUsedAfterMillis, both mappings are "effective".
415         assertEquals(list(alwaysUsed, notAlwaysUsed),
416                 countryTimeZones.getEffectiveTimeZoneMappingsAt(Long.MIN_VALUE));
417         assertEquals(list(alwaysUsed, notAlwaysUsed),
418                 countryTimeZones.getEffectiveTimeZoneMappingsAt(mappingNotUsedAfterMillis));
419 
420         // The following should filter the second mapping because it's not "effective" after
421         // mappingNotUsedAfterMillis.
422         assertEquals(list(alwaysUsed),
423                 countryTimeZones.getEffectiveTimeZoneMappingsAt(mappingNotUsedAfterMillis + 1));
424         assertEquals(list(alwaysUsed),
425                 countryTimeZones.getEffectiveTimeZoneMappingsAt(Long.MAX_VALUE));
426     }
427 
428     @Test
hasUtcZone_everUseUtcHintOverridesZoneInformation()429     public void hasUtcZone_everUseUtcHintOverridesZoneInformation() {
430         // The country has a single zone. Europe/London uses UTC in Winter.
431         CountryTimeZones countryTimeZones = CountryTimeZones.createValidated(
432                 "xx", "Etc/UTC", false /* defaultTimeZoneBoost */, false /* everUsesUtc */,
433                 timeZoneMappings("Etc/UTC"), "test");
434         assertFalse(countryTimeZones.hasUtcZone(WHEN_DST));
435         assertFalse(countryTimeZones.hasUtcZone(WHEN_NO_DST));
436     }
437 
438     @Test
hasUtcZone_singleZone()439     public void hasUtcZone_singleZone() {
440         // The country has a single zone. Europe/London uses UTC in Winter.
441         CountryTimeZones countryTimeZones = CountryTimeZones.createValidated(
442                 "xx", "Europe/London", false /* defaultTimeZoneBoost */, true /* everUsesUtc */,
443                 timeZoneMappings("Europe/London"), "test");
444         assertFalse(countryTimeZones.hasUtcZone(WHEN_DST));
445         assertTrue(countryTimeZones.hasUtcZone(WHEN_NO_DST));
446     }
447 
448     @Test
hasUtcZone_multipleZonesWithUtc()449     public void hasUtcZone_multipleZonesWithUtc() {
450         // The country has multiple zones. Europe/London uses UTC in Winter.
451         CountryTimeZones countryTimeZones = CountryTimeZones.createValidated(
452                 "xx", "America/Los_Angeles", false /* defaultTimeZoneBoost */,
453                 true /* everUsesUtc */,
454                 timeZoneMappings("America/Los_Angeles", "America/New_York", "Europe/London"),
455                 "test");
456         assertFalse(countryTimeZones.hasUtcZone(WHEN_DST));
457         assertTrue(countryTimeZones.hasUtcZone(WHEN_NO_DST));
458     }
459 
460     @Test
hasUtcZone_multipleZonesWithoutUtc()461     public void hasUtcZone_multipleZonesWithoutUtc() {
462         // The country has multiple zones, none of which use UTC.
463         CountryTimeZones countryTimeZones = CountryTimeZones.createValidated(
464                 "xx", "Europe/Paris", false /* defaultTimeZoneBoost */, false /* everUsesUtc */,
465                 timeZoneMappings("America/Los_Angeles", "America/New_York", "Europe/Paris"),
466                 "test");
467         assertFalse(countryTimeZones.hasUtcZone(WHEN_DST));
468         assertFalse(countryTimeZones.hasUtcZone(WHEN_NO_DST));
469     }
470 
471     @Test
hasUtcZone_emptyZones()472     public void hasUtcZone_emptyZones() {
473         // The country has no valid zones.
474         CountryTimeZones countryTimeZones = CountryTimeZones.createValidated(
475                 "xx", INVALID_TZ_ID, false /* defaultTimeZoneBoost */, false /* everUsesUtc */,
476                 timeZoneMappings(INVALID_TZ_ID), "test");
477         assertTrue(countryTimeZones.getTimeZoneMappings().isEmpty());
478         assertFalse(countryTimeZones.hasUtcZone(WHEN_DST));
479         assertFalse(countryTimeZones.hasUtcZone(WHEN_NO_DST));
480     }
481 
482     @Test
timeZoneMapping_getTimeZone_badZoneId()483     public void timeZoneMapping_getTimeZone_badZoneId() {
484         TimeZoneMapping timeZoneMapping =
485                 TimeZoneMapping.createForTests("DOES_NOT_EXIST", true, 1234L, list());
486         try {
487             timeZoneMapping.getTimeZone();
488             fail();
489         } catch (RuntimeException expected) {
490         }
491     }
492 
493     @Test
timeZoneMapping_getTimeZone_validZoneId()494     public void timeZoneMapping_getTimeZone_validZoneId() {
495         TimeZoneMapping timeZoneMapping =
496                 TimeZoneMapping.createForTests("Europe/London", true, 1234L, list());
497         TimeZone timeZone = timeZoneMapping.getTimeZone();
498         assertTrue(timeZone.isFrozen());
499         assertEquals("Europe/London", timeZone.getID());
500     }
501 
502     @Test
timeZoneMapping_isShownInPickerAfter_notHidden()503     public void timeZoneMapping_isShownInPickerAfter_notHidden() {
504         long notUsedAfter = 1234;
505         TimeZoneMapping timeZoneMapping =
506                 TimeZoneMapping.createForTests(
507                         "Europe/London", true /* showInPicker */, notUsedAfter, list());
508 
509         assertTrue(timeZoneMapping.isShownInPickerAt(
510                 Instant.ofEpochMilli(notUsedAfter - 1_000)));
511         assertTrue(timeZoneMapping.isShownInPickerAt(Instant.ofEpochMilli(notUsedAfter)));
512         assertFalse(timeZoneMapping.isShownInPickerAt(
513                 Instant.ofEpochMilli(notUsedAfter + 1_000)));
514     }
515 
516     @Test
timeZoneMapping_isShownInPickerAfter_hiddenTimeZone()517     public void timeZoneMapping_isShownInPickerAfter_hiddenTimeZone() {
518         long notUsedAfter = 1234;
519         TimeZoneMapping hiddenTimeZoneMapping =
520                 TimeZoneMapping.createForTests(
521                         "Moon/Secret_base", false /* showInPicker */, notUsedAfter, list());
522 
523         assertFalse(hiddenTimeZoneMapping.isShownInPickerAt(
524                 Instant.ofEpochMilli(notUsedAfter - 1_000)));
525         assertFalse(hiddenTimeZoneMapping.isShownInPickerAt(Instant.ofEpochMilli(notUsedAfter)));
526         assertFalse(hiddenTimeZoneMapping.isShownInPickerAt(
527                 Instant.ofEpochMilli(notUsedAfter + 1_000)));
528     }
529 
assertImmutableTimeZone(TimeZone timeZone)530     private void assertImmutableTimeZone(TimeZone timeZone) {
531         try {
532             timeZone.setRawOffset(1000);
533             fail();
534         } catch (UnsupportedOperationException expected) {
535         }
536     }
537 
assertImmutableList(List<X> list)538     private static <X> void assertImmutableList(List<X> list) {
539         try {
540             list.add(null);
541             fail();
542         } catch (UnsupportedOperationException expected) {
543         }
544     }
545 
assertZoneEquals(TimeZone expected, TimeZone actual)546     private static void assertZoneEquals(TimeZone expected, TimeZone actual) {
547         // TimeZone.equals() only checks the ID, but that's ok for these tests.
548         assertEquals(expected, actual);
549     }
550 
offsetResultEquals(OffsetResult expected, OffsetResult actual)551     private static boolean offsetResultEquals(OffsetResult expected, OffsetResult actual) {
552         return expected == actual
553                 || (expected != null && actual != null
554                 && Objects.equals(expected.getTimeZone().getID(), actual.getTimeZone().getID())
555                 && expected.isOnlyMatch() == actual.isOnlyMatch());
556     }
557 
558     /**
559      * Creates a list of default {@link TimeZoneMapping} objects with the specified time zone IDs.
560      */
timeZoneMapping(String timeZoneId, Long notUsedAfterMillis)561     private static TimeZoneMapping timeZoneMapping(String timeZoneId, Long notUsedAfterMillis) {
562         return TimeZoneMapping.createForTests(
563                         timeZoneId, true /* picker */, notUsedAfterMillis, list());
564     }
565 
566     /**
567      * Creates a list of default {@link TimeZoneMapping} objects with the specified time zone IDs.
568      */
timeZoneMappings(String... timeZoneIds)569     private static List<TimeZoneMapping> timeZoneMappings(String... timeZoneIds) {
570         return Arrays.stream(timeZoneIds)
571                 .map(x -> timeZoneMapping(x, null /* notUsedAfter */))
572                 .collect(Collectors.toList());
573     }
574 
zone(String id)575     private static TimeZone zone(String id) {
576         return TimeZone.getFrozenTimeZone(id);
577     }
578 
list(X... xes)579     private static <X> List<X> list(X... xes) {
580         return Arrays.asList(xes);
581     }
582 }
583