1 /* 2 * Copyright (C) 2020 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.icu.util; 18 19 import android.icu.impl.Grego; 20 import android.icu.util.AnnualTimeZoneRule; 21 import android.icu.util.BasicTimeZone; 22 import android.icu.util.DateTimeRule; 23 import android.icu.util.InitialTimeZoneRule; 24 import android.icu.util.TimeArrayTimeZoneRule; 25 import android.icu.util.TimeZone; 26 import android.icu.util.TimeZoneRule; 27 import android.icu.util.TimeZoneTransition; 28 29 import libcore.api.IntraCoreApi; 30 31 import java.time.DayOfWeek; 32 import java.time.Instant; 33 import java.time.LocalDateTime; 34 import java.time.LocalTime; 35 import java.time.Month; 36 import java.time.ZoneOffset; 37 import java.time.zone.ZoneOffsetTransition; 38 import java.time.zone.ZoneOffsetTransitionRule; 39 import java.time.zone.ZoneRules; 40 import java.time.zone.ZoneRulesException; 41 import java.util.ArrayList; 42 import java.util.Collections; 43 import java.util.List; 44 import java.util.Map; 45 import java.util.NavigableMap; 46 import java.util.TreeMap; 47 48 49 /** 50 * Provide extra functionalities on top of {@link TimeZone} public APIs. 51 * 52 * @hide 53 */ 54 @IntraCoreApi 55 public class ExtendedTimeZone { 56 57 private final TimeZone timezone; 58 ExtendedTimeZone(String id)59 private ExtendedTimeZone(String id) { 60 timezone = TimeZone.getTimeZone(id); 61 } 62 63 // The API which calls an implementation in android.icu does not use nullability annotation 64 // because the upstream can't guarantee the stability. See http://b/140196694. 65 /** 66 * Returns an instance from the time zone ID. Note that the returned instance could be shared. 67 * 68 * @see TimeZone#getTimeZone(String) for the more information. 69 * @hide 70 */ 71 @IntraCoreApi getInstance(String id)72 public static ExtendedTimeZone getInstance(String id) { 73 return new ExtendedTimeZone(id); 74 } 75 76 /** 77 * Clears the default time zone in ICU4J. When next {@link TimeZone#getDefault()} is called, 78 * ICU4J will re-initialize the default time zone from the value obtained from the libcore's 79 * {@link java.util.TimeZone#getDefault()}. 80 * 81 * This API is useful for libcore's {@link java.util.TimeZone#setDefault(java.util.TimeZone)} to 82 * break the cycle of synchronizing the default time zone between libcore and ICU4J. 83 * 84 * @hide 85 */ 86 @IntraCoreApi clearDefaultTimeZone()87 public static void clearDefaultTimeZone() { 88 TimeZone.setICUDefault(null); 89 } 90 91 /** 92 * Returns the underlying {@link TimeZone} instance. 93 * 94 * @hide 95 */ 96 @IntraCoreApi getTimeZone()97 public TimeZone getTimeZone() { 98 return timezone; 99 } 100 101 /** 102 * Returns a {@link ZoneRules} instance for this time zone. 103 * 104 * @throws ZoneRulesException if the internal rules can't be parsed correctly, or it's not 105 * implemented for the subtype of {@link TimeZone}. 106 * 107 * @implNote This implementations relies on {@link BasicTimeZone#getTimeZoneRules()} in the 108 * following way: 109 * Returned array starts with {@code InitialTimeZoneRule}, followed by {@code 110 * TimeArrayTimeZoneRule}, and, if available, ends with {@code AnnualTimeZoneRule}. 111 * @hide 112 */ 113 @IntraCoreApi createZoneRules()114 public ZoneRules createZoneRules() { 115 if (!(timezone instanceof BasicTimeZone)) { 116 throw zoneRulesException("timezone is " 117 + timezone.getClass().getCanonicalName() 118 + " which is not instance of BasicTimeZone"); 119 } 120 121 BasicTimeZone basicTimeZone = (BasicTimeZone) timezone; 122 123 TimeZoneRule[] timeZoneRules = basicTimeZone.getTimeZoneRules(); 124 125 if (timeZoneRules.length == 0) { 126 throw zoneRulesException("Got 0 time zone rules"); 127 } 128 129 ZoneOffset baseStandardOffset = null; 130 ZoneOffset baseWallOffset = null; 131 132 NavigableMap<Long, TimeArrayTimeZoneRule> rulesByStartTime = new TreeMap<>(); 133 boolean hasRecurringRules = false; 134 135 for (TimeZoneRule timeZoneRule : timeZoneRules) { 136 if (timeZoneRule instanceof InitialTimeZoneRule) { 137 InitialTimeZoneRule initialTimeZoneRule = (InitialTimeZoneRule) timeZoneRule; 138 baseStandardOffset = standardOffset(initialTimeZoneRule); 139 baseWallOffset = fullOffset(initialTimeZoneRule); 140 } else if (timeZoneRule instanceof TimeArrayTimeZoneRule) { 141 TimeArrayTimeZoneRule timeArrayTimeZoneRule = (TimeArrayTimeZoneRule) timeZoneRule; 142 143 for (long startTime : timeArrayTimeZoneRule.getStartTimes()) { 144 rulesByStartTime.put( 145 utcStartTime(startTime, timeArrayTimeZoneRule), timeArrayTimeZoneRule); 146 } 147 } else if (timeZoneRule instanceof AnnualTimeZoneRule) { 148 // Order of AnnualTimeZoneRule-s in BasicTimeZone#getTimeZoneRules is not 149 // specified, they will be fetched using different API. 150 hasRecurringRules = true; 151 } else { 152 throw zoneRulesException( 153 "Unrecognized time zone rule " + timeZoneRule.getClass() + "."); 154 } 155 } 156 157 // Keep in mind that transitionList is not superset of standardOffsetTransitionList. 158 // transitionList keeps track of wall clock changes, but it might remain the same after 159 // standard offset change if DST was changed too. 160 List<ZoneOffsetTransition> standardOffsetTransitionList = new ArrayList<>(); 161 List<ZoneOffsetTransition> transitionList = new ArrayList<>(); 162 163 ZoneOffset lastStandardOffset = baseStandardOffset; 164 ZoneOffset lastWallOffset = baseWallOffset; 165 166 for (Map.Entry<Long, TimeArrayTimeZoneRule> entry : rulesByStartTime.entrySet()) { 167 long startTime = entry.getKey(); 168 TimeArrayTimeZoneRule timeZoneRule = entry.getValue(); 169 170 ZoneOffset ruleStandardOffset = standardOffset(timeZoneRule); 171 172 if (!ruleStandardOffset.equals(lastStandardOffset)) { 173 // ZoneRules needs changes in standard offsets only as an argument. 174 // ZoneOffsetTransition requires before and after offsets to be different, so wall 175 // clock offset can't be used as beforeOffset(it can be equal to afterOffset). Using 176 // previous standard offset seems to be the only reasonable choice left. 177 // As of 2021 transition and beforeOffset arguments are used to calculate UTC offset 178 // of the switch date and previous standard offset will do the trick. 179 ZoneOffsetTransition zoneOffsetTransition = 180 ZoneOffsetTransition.of( 181 localDateTime(startTime, lastStandardOffset), 182 lastStandardOffset, 183 ruleStandardOffset); 184 185 standardOffsetTransitionList.add(zoneOffsetTransition); 186 lastStandardOffset = ruleStandardOffset; 187 } 188 189 ZoneOffset ruleWallOffset = fullOffset(timeZoneRule); 190 191 // ZoneOffsetTransition tracks only changes in full offset - if raw and DST offsets 192 // sum is not changed after a transition, such transition is not tracked by ZoneRules. 193 // ICU does not squash such transitions. 194 if (!lastWallOffset.equals(ruleWallOffset)) { 195 ZoneOffsetTransition zoneOffsetTransition = 196 ZoneOffsetTransition.of( 197 localDateTime(startTime, lastWallOffset), 198 lastWallOffset, 199 ruleWallOffset); 200 201 transitionList.add(zoneOffsetTransition); 202 lastWallOffset = ruleWallOffset; 203 } 204 } 205 206 List<ZoneOffsetTransitionRule> lastRules = new ArrayList<>(); 207 208 if (hasRecurringRules) { 209 List<AnnualTimeZoneRule> annualTimeZoneRules = new ArrayList<>(); 210 211 // ZoneOffsetTransitionRule requires beforeOffset. As total offset in 212 // TimeArrayTimeZoneRule may differ from offset of the last recurring rule 213 // we apply all available recurring rule once. It is possible to build lastRules 214 // in loop below, but doing it in separate loop simplifies code significantly. 215 TimeZoneTransition firstTransitionToAnnualRule = basicTimeZone 216 .getNextTransition(rulesByStartTime.lastKey(), false /* inclusive */); 217 AnnualTimeZoneRule firstAnnualRule = 218 (AnnualTimeZoneRule) firstTransitionToAnnualRule.getTo(); 219 AnnualTimeZoneRule currentTimeZoneRule = firstAnnualRule; 220 long currentUnixEpochTime = firstTransitionToAnnualRule.getTime(); 221 222 do { 223 annualTimeZoneRules.add(currentTimeZoneRule); 224 225 if (annualTimeZoneRules.size() > 16) { 226 throw zoneRulesException("More than 16 annual transitions found."); 227 } 228 229 ZoneOffset ruleStandardOffset = standardOffset(currentTimeZoneRule); 230 231 if (!lastStandardOffset.equals(ruleStandardOffset)) { 232 standardOffsetTransitionList.add( 233 ZoneOffsetTransition.of( 234 localDateTime(currentUnixEpochTime, lastStandardOffset), 235 lastStandardOffset, 236 ruleStandardOffset 237 )); 238 lastStandardOffset = ruleStandardOffset; 239 } 240 241 int currentYear = 242 Instant.ofEpochMilli(currentUnixEpochTime).atOffset(lastWallOffset).getYear(); 243 ZoneOffsetTransition recurringRuleTransition = 244 createZoneOffsetTransitionRule( 245 currentTimeZoneRule, 246 lastStandardOffset, 247 lastWallOffset) 248 .createTransition(currentYear); 249 250 // After introduction of first annual rule wall offset may not change. 251 if (!lastWallOffset.equals(recurringRuleTransition.getOffsetAfter())) { 252 transitionList.add(ZoneOffsetTransition.of( 253 localDateTime(currentUnixEpochTime, lastWallOffset), 254 lastWallOffset, 255 recurringRuleTransition.getOffsetAfter())); 256 lastWallOffset = recurringRuleTransition.getOffsetAfter(); 257 } 258 259 TimeZoneTransition nextTransition = 260 basicTimeZone.getNextTransition(currentUnixEpochTime, false /* inclusive */); 261 currentUnixEpochTime = nextTransition.getTime(); 262 currentTimeZoneRule = (AnnualTimeZoneRule) nextTransition.getTo(); 263 264 if (currentTimeZoneRule == null) { 265 throw zoneRulesException("No transitions after " 266 + currentUnixEpochTime + " for a timezone with recurring rules"); 267 } 268 } while (!currentTimeZoneRule.isEquivalentTo(firstAnnualRule)); 269 270 // All annual rules use the same standard offset and wall offset is always updated on 271 // transition. 272 // The initial value of lastWallOffset is the wall offset of the last recurring 273 // AnnualTimeZoneRule. 274 for (AnnualTimeZoneRule annualTimeZoneRule : annualTimeZoneRules) { 275 ZoneOffsetTransitionRule zoneOffsetTransitionRule = 276 createZoneOffsetTransitionRule( 277 annualTimeZoneRule, lastStandardOffset, lastWallOffset); 278 279 lastWallOffset = zoneOffsetTransitionRule.getOffsetAfter(); 280 281 lastRules.add(zoneOffsetTransitionRule); 282 } 283 284 // ZoneRules does not specify it, but internally it expects lastRules to be sorted 285 // (see ZoneRules#getOffset) in the order they will happen within a year. For example, 286 // if rule A starts in October 2021, and rule B starts in March 2022, expected order 287 // is [B, A]. 288 // We assume that for any year that order is fixed, even though it is possible 289 // to build set of rules where order depends on a given year. 290 // annualTimeZoneRules stores rules in the order they happened, so we just need to find 291 // a break in startYear. 292 int firstRuleIndex = 0; 293 while (firstRuleIndex < annualTimeZoneRules.size() 294 && firstAnnualRule.getStartYear() 295 == annualTimeZoneRules.get(firstRuleIndex).getStartYear()) { 296 ++firstRuleIndex; 297 } 298 299 Collections.rotate(lastRules, -firstRuleIndex); 300 } 301 302 return ZoneRules.of( 303 baseStandardOffset, 304 baseWallOffset, 305 standardOffsetTransitionList, 306 transitionList, 307 lastRules); 308 } 309 310 311 /** 312 * Converts {@link AnnualTimeZoneRule} to {@link ZoneOffsetTransitionRule}. Switch date may be 313 * represented relative to UTC, wall clock, or standard offset. For the latter 2 cases 314 * {@code lastWallOffset} and {@code lastStandardOffset} are used. 315 * 316 * @param annualTimeZoneRule rule to be converted 317 * @param lastStandardOffset standard offset of a rule which preceded {@code 318 * annualTimeZoneRule} 319 * @param lastWallOffset wall offset of a rule which preceded {@code annualTimeZoneRule} 320 */ createZoneOffsetTransitionRule( AnnualTimeZoneRule annualTimeZoneRule, ZoneOffset lastStandardOffset, ZoneOffset lastWallOffset)321 private ZoneOffsetTransitionRule createZoneOffsetTransitionRule( 322 AnnualTimeZoneRule annualTimeZoneRule, 323 ZoneOffset lastStandardOffset, 324 ZoneOffset lastWallOffset) { 325 DateTimeRule dateTimeRule = annualTimeZoneRule.getRule(); 326 Month month = Month.of(dateTimeRule.getRuleMonth() + 1); 327 final DayOfWeek dayOfWeek; 328 final int dayOfMonthIndicator; 329 switch (dateTimeRule.getDateRuleType()) { 330 case DateTimeRule.DOM: 331 dayOfMonthIndicator = dateTimeRule.getRuleDayOfMonth(); 332 dayOfWeek = null; 333 break; 334 case DateTimeRule.DOW: 335 int weekInMonth = dateTimeRule.getRuleWeekInMonth(); 336 if (weekInMonth > 0) { 337 dayOfMonthIndicator = (weekInMonth - 1) * 7 + 1; 338 } else if (weekInMonth < 0) { 339 dayOfMonthIndicator = (weekInMonth + 1) * 7 - 1; 340 } else { 341 throw zoneRulesException("Invalid DateTimeRule in " 342 + annualTimeZoneRule + 343 ". Non-zero weekInMonth expected in " + dateTimeRule); 344 } 345 dayOfWeek = dayOfWeek(dateTimeRule); 346 break; 347 case DateTimeRule.DOW_GEQ_DOM: 348 dayOfMonthIndicator = dateTimeRule.getRuleDayOfMonth(); 349 dayOfWeek = dayOfWeek(dateTimeRule); 350 break; 351 case DateTimeRule.DOW_LEQ_DOM: 352 // java.time.ZoneRules uses negative numbers to indicate that switch date should 353 // come before certain date. Using leap year so that lastSun like rule will 354 // always work correctly. 355 dayOfMonthIndicator = 356 dateTimeRule.getRuleDayOfMonth() - month.maxLength() - 1; 357 dayOfWeek = dayOfWeek(dateTimeRule); 358 break; 359 default: 360 throw zoneRulesException("Unexpected dateTimeRule.dateRuleType=" 361 + dateTimeRule.getTimeRuleType() + " in " + annualTimeZoneRule); 362 } 363 364 final boolean timeEndOfDay; 365 final LocalTime switchDateTime; 366 if (dateTimeRule.getRuleMillisInDay() == Grego.MILLIS_PER_DAY) { 367 timeEndOfDay = true; 368 switchDateTime = LocalTime.MIDNIGHT; 369 } else { 370 timeEndOfDay = false; 371 switchDateTime = LocalTime.ofNanoOfDay(dateTimeRule.getRuleMillisInDay() * 1_000_000L); 372 } 373 374 ZoneOffsetTransitionRule.TimeDefinition timeDefinition = timeDefinition(annualTimeZoneRule); 375 376 // JavaDoc for standardOffset tells that it should be "offset in force at the cutover". 377 // It's not clear what offset is in effect at the cutover moment, but zic format assumes 378 // that only DST is changed in annual rules and standard offset is handled differently. 379 return ZoneOffsetTransitionRule.of(month, 380 dayOfMonthIndicator, 381 dayOfWeek, 382 switchDateTime, 383 timeEndOfDay, 384 timeDefinition, 385 lastStandardOffset, 386 lastWallOffset, 387 fullOffset(annualTimeZoneRule)); 388 } 389 timeDefinition( AnnualTimeZoneRule annualTimeZoneRule)390 private ZoneOffsetTransitionRule.TimeDefinition timeDefinition( 391 AnnualTimeZoneRule annualTimeZoneRule) { 392 DateTimeRule dateTimeRule = annualTimeZoneRule.getRule(); 393 switch (dateTimeRule.getTimeRuleType()) { 394 case DateTimeRule.STANDARD_TIME: 395 return ZoneOffsetTransitionRule.TimeDefinition.STANDARD; 396 case DateTimeRule.UTC_TIME: 397 return ZoneOffsetTransitionRule.TimeDefinition.UTC; 398 case DateTimeRule.WALL_TIME: 399 return ZoneOffsetTransitionRule.TimeDefinition.WALL; 400 default: 401 throw zoneRulesException( 402 "Unexpected dateTimeRule.timeRuleType=" + dateTimeRule.getTimeRuleType() 403 + " in AnnualTimeZoneRule: " + annualTimeZoneRule); 404 } 405 } 406 utcStartTime(long startTime, TimeArrayTimeZoneRule timeZoneRule)407 private long utcStartTime(long startTime, TimeArrayTimeZoneRule timeZoneRule) { 408 switch (timeZoneRule.getTimeType()) { 409 case DateTimeRule.UTC_TIME: 410 return startTime; 411 case DateTimeRule.STANDARD_TIME: 412 return startTime - timeZoneRule.getRawOffset(); 413 case DateTimeRule.WALL_TIME: 414 return startTime - timeZoneRule.getRawOffset() - timeZoneRule.getDSTSavings(); 415 default: 416 throw zoneRulesException("Unexpected timeType in " + timeZoneRule); 417 } 418 } 419 zoneRulesException(String message)420 private ZoneRulesException zoneRulesException(String message) { 421 return new ZoneRulesException("Failed to build ZoneRules for " + timezone.getID() + 422 ". " + message); 423 } 424 localDateTime(long epochMillis, ZoneOffset zoneOffset)425 private static LocalDateTime localDateTime(long epochMillis, ZoneOffset zoneOffset) { 426 return Instant.ofEpochMilli(epochMillis) 427 .atOffset(zoneOffset) 428 .toLocalDateTime(); 429 } 430 dayOfWeek(DateTimeRule dateTimeRule)431 private static DayOfWeek dayOfWeek(DateTimeRule dateTimeRule) { 432 return DayOfWeek.SUNDAY.plus(dateTimeRule.getRuleDayOfWeek() - 1); 433 } 434 standardOffset(TimeZoneRule timeZoneRule)435 private static ZoneOffset standardOffset(TimeZoneRule timeZoneRule) { 436 return toOffset(timeZoneRule.getRawOffset()); 437 } 438 fullOffset(TimeZoneRule timeZoneRule)439 private static ZoneOffset fullOffset(TimeZoneRule timeZoneRule) { 440 return toOffset(timeZoneRule.getRawOffset() + timeZoneRule.getDSTSavings()); 441 } 442 toOffset(int rawOffset)443 private static ZoneOffset toOffset(int rawOffset) { 444 return ZoneOffset.ofTotalSeconds(rawOffset / 1_000); 445 } 446 } 447