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