1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * This code is free software; you can redistribute it and/or modify it 5 * under the terms of the GNU General Public License version 2 only, as 6 * published by the Free Software Foundation. The Android Open Source 7 * Project designates this particular file as subject to the "Classpath" 8 * exception as provided by The Android Open Source Project in the LICENSE 9 * file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 */ 21 22 package java.time.zone; 23 24 import android.icu.util.AnnualTimeZoneRule; 25 import android.icu.util.BasicTimeZone; 26 import android.icu.util.DateTimeRule; 27 import android.icu.util.InitialTimeZoneRule; 28 import android.icu.util.TimeZone; 29 import android.icu.util.TimeZoneRule; 30 import android.icu.util.TimeZoneTransition; 31 import java.time.DayOfWeek; 32 import java.time.LocalTime; 33 import java.time.Month; 34 import java.time.ZoneOffset; 35 import java.util.ArrayList; 36 import java.util.Collections; 37 import java.util.HashSet; 38 import java.util.List; 39 import java.util.NavigableMap; 40 import java.util.Set; 41 import java.util.TreeMap; 42 import java.util.concurrent.TimeUnit; 43 import libcore.util.BasicLruCache; 44 45 /** 46 * A ZoneRulesProvider that generates rules from ICU4J TimeZones. 47 * This provider ensures that classes in {@link java.time} use the same time zone information 48 * as ICU4J. 49 */ 50 public class IcuZoneRulesProvider extends ZoneRulesProvider { 51 52 // Arbitrary upper limit to number of transitions including the final rules. 53 private static final int MAX_TRANSITIONS = 10000; 54 55 private static final int SECONDS_IN_DAY = 24 * 60 * 60; 56 57 private final BasicLruCache<String, ZoneRules> cache = new ZoneRulesCache(8); 58 59 @Override provideZoneIds()60 protected Set<String> provideZoneIds() { 61 Set<String> zoneIds = TimeZone.getAvailableIDs(TimeZone.SystemTimeZoneType.ANY, null, null); 62 zoneIds = new HashSet<>(zoneIds); 63 // java.time assumes ZoneId that start with "GMT" fit the pattern "GMT+HH:mm:ss" which these 64 // do not. Since they are equivalent to GMT, just remove these aliases. 65 zoneIds.remove("GMT+0"); 66 zoneIds.remove("GMT-0"); 67 return zoneIds; 68 } 69 70 @Override provideRules(String zoneId, boolean forCaching)71 protected ZoneRules provideRules(String zoneId, boolean forCaching) { 72 // Ignore forCaching, as this is a static provider. 73 return cache.get(zoneId); 74 } 75 76 @Override provideVersions(String zoneId)77 protected NavigableMap<String, ZoneRules> provideVersions(String zoneId) { 78 return new TreeMap<>( 79 Collections.singletonMap(TimeZone.getTZDataVersion(), 80 provideRules(zoneId, /* forCaching */ false))); 81 } 82 83 /* 84 * This implementation is only tested with BasicTimeZone objects and depends on 85 * implementation details of that class: 86 * 87 * 0. TimeZone.getFrozenTimeZone() always returns a BasicTimeZone object. 88 * 1. The first rule is always an InitialTimeZoneRule (guaranteed by spec). 89 * 2. AnnualTimeZoneRules are only used as "final rules". 90 * 3. The final rules are either 0 or 2 AnnualTimeZoneRules 91 * 4. The final rules have endYear set to MAX_YEAR. 92 * 5. Each transition generated by the rules changes either the raw offset, the total offset 93 * or both. 94 * 6. There is a non-immense number of transitions for any rule before the final rules apply 95 * (enforced via the arbitrary limit defined in MAX_TRANSITIONS). 96 * 97 * Assumptions #5 and #6 are not strictly required for this code to work, but hold for the 98 * the data and code at the time of implementation. If they were broken they would indicate 99 * an incomplete understanding of how ICU TimeZoneRules are used which would probably mean that 100 * this code needs to be updated. 101 * 102 * These assumptions are verified using the verify() method where appropriate. 103 */ generateZoneRules(String zoneId)104 static ZoneRules generateZoneRules(String zoneId) { 105 TimeZone timeZone = TimeZone.getFrozenTimeZone(zoneId); 106 // Assumption #0 107 verify(timeZone instanceof BasicTimeZone, zoneId, 108 "Unexpected time zone class " + timeZone.getClass()); 109 BasicTimeZone tz = (BasicTimeZone) timeZone; 110 TimeZoneRule[] rules = tz.getTimeZoneRules(); 111 // Assumption #1 112 InitialTimeZoneRule initial = (InitialTimeZoneRule) rules[0]; 113 114 ZoneOffset baseStandardOffset = millisToOffset(initial.getRawOffset()); 115 ZoneOffset baseWallOffset = 116 millisToOffset((initial.getRawOffset() + initial.getDSTSavings())); 117 118 List<ZoneOffsetTransition> standardOffsetTransitionList = new ArrayList<>(); 119 List<ZoneOffsetTransition> transitionList = new ArrayList<>(); 120 List<ZoneOffsetTransitionRule> lastRules = new ArrayList<>(); 121 122 int preLastDstSavings = 0; 123 AnnualTimeZoneRule last1 = null; 124 AnnualTimeZoneRule last2 = null; 125 126 TimeZoneTransition transition = tz.getNextTransition(Long.MIN_VALUE, false); 127 int transitionCount = 1; 128 // This loop has two possible exit conditions (in normal operation): 129 // 1. for zones that end with a static value and have no ongoing DST changes, it will exit 130 // via the normal condition (transition != null) 131 // 2. for zones with ongoing DST changes (represented by a "final zone" in ICU4J, and by 132 // "last rules" in java.time) the "break transitionLoop" will be used to exit the loop. 133 transitionLoop: 134 while (transition != null) { 135 TimeZoneRule from = transition.getFrom(); 136 TimeZoneRule to = transition.getTo(); 137 boolean hadEffect = false; 138 if (from.getRawOffset() != to.getRawOffset()) { 139 standardOffsetTransitionList.add(new ZoneOffsetTransition( 140 TimeUnit.MILLISECONDS.toSeconds(transition.getTime()), 141 millisToOffset(from.getRawOffset()), 142 millisToOffset(to.getRawOffset()))); 143 hadEffect = true; 144 } 145 int fromTotalOffset = from.getRawOffset() + from.getDSTSavings(); 146 int toTotalOffset = to.getRawOffset() + to.getDSTSavings(); 147 if (fromTotalOffset != toTotalOffset) { 148 transitionList.add(new ZoneOffsetTransition( 149 TimeUnit.MILLISECONDS.toSeconds(transition.getTime()), 150 millisToOffset(fromTotalOffset), 151 millisToOffset(toTotalOffset))); 152 hadEffect = true; 153 } 154 // Assumption #5 155 verify(hadEffect, zoneId, "Transition changed neither total nor raw offset."); 156 if (to instanceof AnnualTimeZoneRule) { 157 // The presence of an AnnualTimeZoneRule is taken as an indication of a final rule. 158 if (last1 == null) { 159 preLastDstSavings = from.getDSTSavings(); 160 last1 = (AnnualTimeZoneRule) to; 161 // Assumption #4 162 verify(last1.getEndYear() == AnnualTimeZoneRule.MAX_YEAR, zoneId, 163 "AnnualTimeZoneRule is not permanent."); 164 } else { 165 last2 = (AnnualTimeZoneRule) to; 166 // Assumption #4 167 verify(last2.getEndYear() == AnnualTimeZoneRule.MAX_YEAR, zoneId, 168 "AnnualTimeZoneRule is not permanent."); 169 170 // Assumption #3 171 transition = tz.getNextTransition(transition.getTime(), false); 172 verify(transition.getTo() == last1, zoneId, 173 "Unexpected rule after 2 AnnualTimeZoneRules."); 174 break transitionLoop; 175 } 176 } else { 177 // Assumption #2 178 verify(last1 == null, zoneId, "Unexpected rule after AnnualTimeZoneRule."); 179 } 180 verify(transitionCount <= MAX_TRANSITIONS, zoneId, 181 "More than " + MAX_TRANSITIONS + " transitions."); 182 transition = tz.getNextTransition(transition.getTime(), false); 183 transitionCount++; 184 } 185 if (last1 != null) { 186 // Assumption #3 187 verify(last2 != null, zoneId, "Only one AnnualTimeZoneRule."); 188 lastRules.add(toZoneOffsetTransitionRule(last1, preLastDstSavings)); 189 lastRules.add(toZoneOffsetTransitionRule(last2, last1.getDSTSavings())); 190 } 191 192 return ZoneRules.of(baseStandardOffset, baseWallOffset, standardOffsetTransitionList, 193 transitionList, lastRules); 194 } 195 196 /** 197 * Verify an assumption about the zone rules. 198 * 199 * @param check 200 * {@code true} if the assumption holds, {@code false} otherwise. 201 * @param zoneId 202 * Zone ID for which to check. 203 * @param message 204 * Error description of a failed check. 205 * @throws ZoneRulesException 206 * If and only if {@code check} is {@code false}. 207 */ verify(boolean check, String zoneId, String message)208 private static void verify(boolean check, String zoneId, String message) { 209 if (!check) { 210 throw new ZoneRulesException( 211 String.format("Failed verification of zone %s: %s", zoneId, message)); 212 } 213 } 214 215 /** 216 * Transform an {@link AnnualTimeZoneRule} into an equivalent {@link ZoneOffsetTransitionRule}. 217 * This is only used for the "final rules". 218 * 219 * @param rule 220 * The rule to transform. 221 * @param dstSavingMillisBefore 222 * The DST offset before the first transition in milliseconds. 223 */ toZoneOffsetTransitionRule( AnnualTimeZoneRule rule, int dstSavingMillisBefore)224 private static ZoneOffsetTransitionRule toZoneOffsetTransitionRule( 225 AnnualTimeZoneRule rule, int dstSavingMillisBefore) { 226 DateTimeRule dateTimeRule = rule.getRule(); 227 // Calendar.JANUARY is 0, transform it into a proper Month. 228 Month month = Month.JANUARY.plus(dateTimeRule.getRuleMonth()); 229 int dayOfMonthIndicator; 230 // Calendar.SUNDAY is 1, transform it into a proper DayOfWeek. 231 DayOfWeek dayOfWeek = DayOfWeek.SATURDAY.plus(dateTimeRule.getRuleDayOfWeek()); 232 switch (dateTimeRule.getDateRuleType()) { 233 case DateTimeRule.DOM: 234 // Transition always on a specific day of the month. 235 dayOfMonthIndicator = dateTimeRule.getRuleDayOfMonth(); 236 dayOfWeek = null; 237 break; 238 case DateTimeRule.DOW_GEQ_DOM: 239 // ICU representation matches java.time representation. 240 dayOfMonthIndicator = dateTimeRule.getRuleDayOfMonth(); 241 break; 242 case DateTimeRule.DOW_LEQ_DOM: 243 // java.time uses a negative dayOfMonthIndicator to represent "Sun<=X" or "lastSun" 244 // rules. ICU uses this constant and the normal day. So "lastSun" in January would 245 // ruleDayOfMonth = 31 in ICU and dayOfMonthIndicator = -1 in java.time. 246 dayOfMonthIndicator = -month.maxLength() + dateTimeRule.getRuleDayOfMonth() - 1; 247 break; 248 case DateTimeRule.DOW: 249 // DOW is unspecified in the documentation and seems to never be used. 250 throw new ZoneRulesException("Date rule type DOW is unsupported"); 251 default: 252 throw new ZoneRulesException( 253 "Unexpected date rule type: " + dateTimeRule.getDateRuleType()); 254 } 255 // Cast to int is save, as input is int. 256 int secondOfDay = (int) TimeUnit.MILLISECONDS.toSeconds(dateTimeRule.getRuleMillisInDay()); 257 LocalTime time; 258 boolean timeEndOfDay; 259 if (secondOfDay == SECONDS_IN_DAY) { 260 time = LocalTime.MIDNIGHT; 261 timeEndOfDay = true; 262 } else { 263 time = LocalTime.ofSecondOfDay(secondOfDay); 264 timeEndOfDay = false; 265 } 266 ZoneOffsetTransitionRule.TimeDefinition timeDefinition; 267 switch (dateTimeRule.getTimeRuleType()) { 268 case DateTimeRule.WALL_TIME: 269 timeDefinition = ZoneOffsetTransitionRule.TimeDefinition.WALL; 270 break; 271 case DateTimeRule.STANDARD_TIME: 272 timeDefinition = ZoneOffsetTransitionRule.TimeDefinition.STANDARD; 273 break; 274 case DateTimeRule.UTC_TIME: 275 timeDefinition = ZoneOffsetTransitionRule.TimeDefinition.UTC; 276 break; 277 default: 278 throw new ZoneRulesException( 279 "Unexpected time rule type " + dateTimeRule.getTimeRuleType()); 280 } 281 ZoneOffset standardOffset = millisToOffset(rule.getRawOffset()); 282 ZoneOffset offsetBefore = millisToOffset(rule.getRawOffset() + dstSavingMillisBefore); 283 ZoneOffset offsetAfter = millisToOffset( 284 rule.getRawOffset() + rule.getDSTSavings()); 285 return ZoneOffsetTransitionRule.of( 286 month, dayOfMonthIndicator, dayOfWeek, time, timeEndOfDay, timeDefinition, 287 standardOffset, offsetBefore, offsetAfter); 288 } 289 millisToOffset(int offset)290 private static ZoneOffset millisToOffset(int offset) { 291 // Cast to int is save, as input is int. 292 return ZoneOffset.ofTotalSeconds((int) TimeUnit.MILLISECONDS.toSeconds(offset)); 293 } 294 295 private static class ZoneRulesCache extends BasicLruCache<String, ZoneRules> { 296 ZoneRulesCache(int maxSize)297 ZoneRulesCache(int maxSize) { 298 super(maxSize); 299 } 300 301 @Override create(String zoneId)302 protected ZoneRules create(String zoneId) { 303 String canonicalId = TimeZone.getCanonicalID(zoneId); 304 if (!canonicalId.equals(zoneId)) { 305 // Return the same object as the canonical one, to avoid wasting space, but cache 306 // it under the non-cannonical name as well, to avoid future getCanonicalID calls. 307 return get(canonicalId); 308 } 309 return generateZoneRules(zoneId); 310 } 311 } 312 } 313