1/*
2 * Copyright (C) 2016 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 */
16package libcore.java.time.zone;
17
18import org.junit.Test;
19import org.junit.runner.RunWith;
20import org.junit.runners.Parameterized;
21import android.icu.util.BasicTimeZone;
22import android.icu.util.TimeZone;
23import android.icu.util.TimeZoneRule;
24import android.icu.util.TimeZoneTransition;
25import java.time.Duration;
26import java.time.Instant;
27import java.time.LocalDateTime;
28import java.time.Month;
29import java.time.ZoneOffset;
30import java.time.zone.ZoneOffsetTransition;
31import java.time.zone.ZoneRules;
32import java.time.zone.ZoneRulesProvider;
33import java.util.Set;
34
35import static org.junit.Assert.assertEquals;
36import static org.junit.Assert.assertFalse;
37import static org.junit.Assert.assertNull;
38
39/**
40 * Test the {@link java.time.zone.IcuZoneRulesProvider}.
41 *
42 * It is indirectly tested via static methods in {@link ZoneRulesProvider} as all the relevant
43 * methods are protected. This test verifies that the rules returned by that provider behave
44 * equivalently to the ICU rules from which they are created.
45 */
46@RunWith(Parameterized.class)
47public class IcuZoneRulesProviderTest {
48
49    @Parameterized.Parameters(name = "{0}")
50    public static Iterable<String> getZoneIds() {
51        Set<String> availableZoneIds = ZoneRulesProvider.getAvailableZoneIds();
52        assertFalse("no zones returned", availableZoneIds.isEmpty());
53        return availableZoneIds;
54    }
55
56    private final String zoneId;
57
58    public IcuZoneRulesProviderTest(final String zoneId) {
59        this.zoneId = zoneId;
60    }
61
62    /**
63     * Verifies that ICU and java.time return the same transitions before and after a pre-selected
64     * set of instants in time.
65     */
66    @Test
67    public void testTransitionsNearInstants() {
68        // An arbitrary set of instants at which to test the offsets in both implementations.
69        Instant[] instants = new Instant[] {
70                LocalDateTime.of(1900, Month.DECEMBER, 24, 12, 0).toInstant(ZoneOffset.UTC),
71                LocalDateTime.of(1970, Month.JANUARY, 1, 2, 3).toInstant(ZoneOffset.UTC),
72                LocalDateTime.of(1980, Month.FEBRUARY, 4, 5, 6).toInstant(ZoneOffset.UTC),
73                LocalDateTime.of(1990, Month.MARCH, 7, 8, 9).toInstant(ZoneOffset.UTC),
74                LocalDateTime.of(2000, Month.APRIL, 10, 11, 12).toInstant(ZoneOffset.UTC),
75                LocalDateTime.of(2016, Month.MAY, 13, 14, 15).toInstant(ZoneOffset.UTC),
76                LocalDateTime.of(2020, Month.JUNE, 16, 17, 18).toInstant(ZoneOffset.UTC),
77                LocalDateTime.of(2100, Month.JULY, 19, 20, 21).toInstant(ZoneOffset.UTC),
78                // yes, adding "now" makes the test time-dependent, but it also ensures that future
79                // updates don't break on the then-current date.
80                Instant.now()
81        };
82        // Coincidentally this test verifies that all zones can be converted to ZoneRules and
83        // don't violate any of the assumptions of IcuZoneRulesProvider.
84        ZoneRules rules = ZoneRulesProvider.getRules(zoneId, false);
85        BasicTimeZone timeZone = (BasicTimeZone) TimeZone.getTimeZone(zoneId);
86
87        int[] icuOffsets = new int[2];
88        for (Instant instant : instants) {
89            ZoneOffset offset = rules.getOffset(instant);
90            Duration daylightSavings = rules.getDaylightSavings(instant);
91            timeZone.getOffset(instant.toEpochMilli(), false, icuOffsets);
92
93            assertEquals("total offset for " + zoneId + " at " + instant,
94                    icuOffsets[1] + icuOffsets[0], offset.getTotalSeconds() * 1000);
95            assertEquals("dst offset for " + zoneId + " at " + instant,
96                    icuOffsets[1], daylightSavings.toMillis());
97
98            ZoneOffsetTransition jtTrans;
99            TimeZoneTransition icuTrans;
100
101            jtTrans = rules.nextTransition(instant);
102            icuTrans = timeZone.getNextTransition(instant.toEpochMilli(), false);
103            while (isIcuOnlyTransition(icuTrans)) {
104                icuTrans = timeZone.getNextTransition(icuTrans.getTime(), false);
105            }
106            assertEquivalent(icuTrans, jtTrans);
107
108            jtTrans = rules.previousTransition(instant);
109            icuTrans = timeZone.getPreviousTransition(instant.toEpochMilli(), false);
110            // Find previous "real" transition.
111            while (isIcuOnlyTransition(icuTrans)) {
112                icuTrans = timeZone.getPreviousTransition(icuTrans.getTime(), false);
113            }
114            assertEquivalent(icuTrans, jtTrans);
115        }
116    }
117
118    /**
119     * Verifies that ICU and java.time rules return the same transitions between 1900 and 2100.
120     */
121    @Test
122    public void testAllTransitions() {
123        final Instant start = LocalDateTime.of(1900, Month.JANUARY, 1, 12, 0)
124                .toInstant(ZoneOffset.UTC);
125        // Many timezones have ongoing DST changes, so they would generate transitions endlessly.
126        // Pick a far-future end date to stop comparing in that case.
127        final Instant end = LocalDateTime.of(2100, Month.DECEMBER, 31, 12, 0)
128                .toInstant(ZoneOffset.UTC);
129
130        ZoneRules rules = ZoneRulesProvider.getRules(zoneId, false);
131        BasicTimeZone timeZone = (BasicTimeZone) TimeZone.getTimeZone(zoneId);
132
133        Instant instant = start;
134        while (instant.isBefore(end)) {
135            ZoneOffsetTransition jtTrans;
136            TimeZoneTransition icuTrans;
137
138            jtTrans = rules.nextTransition(instant);
139            icuTrans = timeZone.getNextTransition(instant.toEpochMilli(), false);
140            while (isIcuOnlyTransition(icuTrans)) {
141                icuTrans = timeZone.getNextTransition(icuTrans.getTime(), false);
142            }
143            assertEquivalent(icuTrans, jtTrans);
144            if (jtTrans == null) {
145                break;
146            }
147            instant = jtTrans.getInstant();
148        }
149    }
150
151    /**
152     * Returns {@code true} iff this transition will only be returned by ICU code.
153     * ICU reports "no-op" transitions where the raw offset and the dst savings
154     * change by the same absolute value in opposite directions, java.time doesn't
155     * return them, so find the next "real" transition.
156     */
157    private static boolean isIcuOnlyTransition(TimeZoneTransition transition) {
158        if (transition == null) {
159            return false;
160        }
161        return transition.getFrom().getRawOffset() + transition.getFrom().getDSTSavings()
162                == transition.getTo().getRawOffset() + transition.getTo().getDSTSavings();
163    }
164
165    /**
166     * Asserts that the ICU {@link TimeZoneTransition} is equivalent to the java.time {@link
167     * ZoneOffsetTransition}.
168     */
169    private static void assertEquivalent(
170            TimeZoneTransition icuTransition, ZoneOffsetTransition jtTransition) {
171        if (icuTransition == null) {
172            assertNull(jtTransition);
173            return;
174        }
175        assertEquals("time of transition",
176                Instant.ofEpochMilli(icuTransition.getTime()), jtTransition.getInstant());
177        TimeZoneRule from = icuTransition.getFrom();
178        TimeZoneRule to = icuTransition.getTo();
179        assertEquals("offset before",
180                (from.getDSTSavings() + from.getRawOffset()) / 1000,
181                jtTransition.getOffsetBefore().getTotalSeconds());
182        assertEquals("offset after",
183                (to.getDSTSavings() + to.getRawOffset()) / 1000,
184                jtTransition.getOffsetAfter().getTotalSeconds());
185    }
186}
187