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