| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.icu.util; |
| |
| import android.icu.impl.Grego; |
| import android.icu.util.AnnualTimeZoneRule; |
| import android.icu.util.BasicTimeZone; |
| import android.icu.util.DateTimeRule; |
| import android.icu.util.InitialTimeZoneRule; |
| import android.icu.util.TimeArrayTimeZoneRule; |
| import android.icu.util.TimeZone; |
| import android.icu.util.TimeZoneRule; |
| import android.icu.util.TimeZoneTransition; |
| |
| import libcore.api.IntraCoreApi; |
| |
| import java.time.DayOfWeek; |
| import java.time.Instant; |
| import java.time.LocalDateTime; |
| import java.time.LocalTime; |
| import java.time.Month; |
| import java.time.ZoneOffset; |
| import java.time.zone.ZoneOffsetTransition; |
| import java.time.zone.ZoneOffsetTransitionRule; |
| import java.time.zone.ZoneRules; |
| import java.time.zone.ZoneRulesException; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.NavigableMap; |
| import java.util.TreeMap; |
| |
| |
| /** |
| * Provide extra functionalities on top of {@link TimeZone} public APIs. |
| * |
| * @hide |
| */ |
| @IntraCoreApi |
| public class ExtendedTimeZone { |
| |
| private final TimeZone timezone; |
| |
| private ExtendedTimeZone(String id) { |
| timezone = TimeZone.getTimeZone(id); |
| } |
| |
| // The API which calls an implementation in android.icu does not use nullability annotation |
| // because the upstream can't guarantee the stability. See http://b/140196694. |
| /** |
| * Returns an instance from the time zone ID. Note that the returned instance could be shared. |
| * |
| * @see TimeZone#getTimeZone(String) for the more information. |
| * @hide |
| */ |
| @IntraCoreApi |
| public static ExtendedTimeZone getInstance(String id) { |
| return new ExtendedTimeZone(id); |
| } |
| |
| /** |
| * Clears the default time zone in ICU4J. When next {@link TimeZone#getDefault()} is called, |
| * ICU4J will re-initialize the default time zone from the value obtained from the libcore's |
| * {@link java.util.TimeZone#getDefault()}. |
| * |
| * This API is useful for libcore's {@link java.util.TimeZone#setDefault(java.util.TimeZone)} to |
| * break the cycle of synchronizing the default time zone between libcore and ICU4J. |
| * |
| * @hide |
| */ |
| @IntraCoreApi |
| public static void clearDefaultTimeZone() { |
| TimeZone.setICUDefault(null); |
| } |
| |
| /** |
| * Returns the underlying {@link TimeZone} instance. |
| * |
| * @hide |
| */ |
| @IntraCoreApi |
| public TimeZone getTimeZone() { |
| return timezone; |
| } |
| |
| /** |
| * Returns a {@link ZoneRules} instance for this time zone. |
| * |
| * @throws ZoneRulesException if the internal rules can't be parsed correctly, or it's not |
| * implemented for the subtype of {@link TimeZone}. |
| * |
| * @implNote This implementations relies on {@link BasicTimeZone#getTimeZoneRules()} in the |
| * following way: |
| * Returned array starts with {@code InitialTimeZoneRule}, followed by {@code |
| * TimeArrayTimeZoneRule}, and, if available, ends with {@code AnnualTimeZoneRule}. |
| * @hide |
| */ |
| @IntraCoreApi |
| public ZoneRules createZoneRules() { |
| if (!(timezone instanceof BasicTimeZone)) { |
| throw zoneRulesException("timezone is " |
| + timezone.getClass().getCanonicalName() |
| + " which is not instance of BasicTimeZone"); |
| } |
| |
| BasicTimeZone basicTimeZone = (BasicTimeZone) timezone; |
| |
| TimeZoneRule[] timeZoneRules = basicTimeZone.getTimeZoneRules(); |
| |
| if (timeZoneRules.length == 0) { |
| throw zoneRulesException("Got 0 time zone rules"); |
| } |
| |
| ZoneOffset baseStandardOffset = null; |
| ZoneOffset baseWallOffset = null; |
| |
| NavigableMap<Long, TimeArrayTimeZoneRule> rulesByStartTime = new TreeMap<>(); |
| boolean hasRecurringRules = false; |
| |
| for (TimeZoneRule timeZoneRule : timeZoneRules) { |
| if (timeZoneRule instanceof InitialTimeZoneRule) { |
| InitialTimeZoneRule initialTimeZoneRule = (InitialTimeZoneRule) timeZoneRule; |
| baseStandardOffset = standardOffset(initialTimeZoneRule); |
| baseWallOffset = fullOffset(initialTimeZoneRule); |
| } else if (timeZoneRule instanceof TimeArrayTimeZoneRule) { |
| TimeArrayTimeZoneRule timeArrayTimeZoneRule = (TimeArrayTimeZoneRule) timeZoneRule; |
| |
| for (long startTime : timeArrayTimeZoneRule.getStartTimes()) { |
| rulesByStartTime.put( |
| utcStartTime(startTime, timeArrayTimeZoneRule), timeArrayTimeZoneRule); |
| } |
| } else if (timeZoneRule instanceof AnnualTimeZoneRule) { |
| // Order of AnnualTimeZoneRule-s in BasicTimeZone#getTimeZoneRules is not |
| // specified, they will be fetched using different API. |
| hasRecurringRules = true; |
| } else { |
| throw zoneRulesException( |
| "Unrecognized time zone rule " + timeZoneRule.getClass() + "."); |
| } |
| } |
| |
| // Keep in mind that transitionList is not superset of standardOffsetTransitionList. |
| // transitionList keeps track of wall clock changes, but it might remain the same after |
| // standard offset change if DST was changed too. |
| List<ZoneOffsetTransition> standardOffsetTransitionList = new ArrayList<>(); |
| List<ZoneOffsetTransition> transitionList = new ArrayList<>(); |
| |
| ZoneOffset lastStandardOffset = baseStandardOffset; |
| ZoneOffset lastWallOffset = baseWallOffset; |
| |
| for (Map.Entry<Long, TimeArrayTimeZoneRule> entry : rulesByStartTime.entrySet()) { |
| long startTime = entry.getKey(); |
| TimeArrayTimeZoneRule timeZoneRule = entry.getValue(); |
| |
| ZoneOffset ruleStandardOffset = standardOffset(timeZoneRule); |
| |
| if (!ruleStandardOffset.equals(lastStandardOffset)) { |
| // ZoneRules needs changes in standard offsets only as an argument. |
| // ZoneOffsetTransition requires before and after offsets to be different, so wall |
| // clock offset can't be used as beforeOffset(it can be equal to afterOffset). Using |
| // previous standard offset seems to be the only reasonable choice left. |
| // As of 2021 transition and beforeOffset arguments are used to calculate UTC offset |
| // of the switch date and previous standard offset will do the trick. |
| ZoneOffsetTransition zoneOffsetTransition = |
| ZoneOffsetTransition.of( |
| localDateTime(startTime, lastStandardOffset), |
| lastStandardOffset, |
| ruleStandardOffset); |
| |
| standardOffsetTransitionList.add(zoneOffsetTransition); |
| lastStandardOffset = ruleStandardOffset; |
| } |
| |
| ZoneOffset ruleWallOffset = fullOffset(timeZoneRule); |
| |
| // ZoneOffsetTransition tracks only changes in full offset - if raw and DST offsets |
| // sum is not changed after a transition, such transition is not tracked by ZoneRules. |
| // ICU does not squash such transitions. |
| if (!lastWallOffset.equals(ruleWallOffset)) { |
| ZoneOffsetTransition zoneOffsetTransition = |
| ZoneOffsetTransition.of( |
| localDateTime(startTime, lastWallOffset), |
| lastWallOffset, |
| ruleWallOffset); |
| |
| transitionList.add(zoneOffsetTransition); |
| lastWallOffset = ruleWallOffset; |
| } |
| } |
| |
| List<ZoneOffsetTransitionRule> lastRules = new ArrayList<>(); |
| |
| if (hasRecurringRules) { |
| List<AnnualTimeZoneRule> annualTimeZoneRules = new ArrayList<>(); |
| |
| // ZoneOffsetTransitionRule requires beforeOffset. As total offset in |
| // TimeArrayTimeZoneRule may differ from offset of the last recurring rule |
| // we apply all available recurring rule once. It is possible to build lastRules |
| // in loop below, but doing it in separate loop simplifies code significantly. |
| TimeZoneTransition firstTransitionToAnnualRule = basicTimeZone |
| .getNextTransition(rulesByStartTime.lastKey(), false /* inclusive */); |
| AnnualTimeZoneRule firstAnnualRule = |
| (AnnualTimeZoneRule) firstTransitionToAnnualRule.getTo(); |
| AnnualTimeZoneRule currentTimeZoneRule = firstAnnualRule; |
| long currentUnixEpochTime = firstTransitionToAnnualRule.getTime(); |
| |
| do { |
| annualTimeZoneRules.add(currentTimeZoneRule); |
| |
| if (annualTimeZoneRules.size() > 16) { |
| throw zoneRulesException("More than 16 annual transitions found."); |
| } |
| |
| ZoneOffset ruleStandardOffset = standardOffset(currentTimeZoneRule); |
| |
| if (!lastStandardOffset.equals(ruleStandardOffset)) { |
| standardOffsetTransitionList.add( |
| ZoneOffsetTransition.of( |
| localDateTime(currentUnixEpochTime, lastStandardOffset), |
| lastStandardOffset, |
| ruleStandardOffset |
| )); |
| lastStandardOffset = ruleStandardOffset; |
| } |
| |
| int currentYear = |
| Instant.ofEpochMilli(currentUnixEpochTime).atOffset(lastWallOffset).getYear(); |
| ZoneOffsetTransition recurringRuleTransition = |
| createZoneOffsetTransitionRule( |
| currentTimeZoneRule, |
| lastStandardOffset, |
| lastWallOffset) |
| .createTransition(currentYear); |
| |
| // After introduction of first annual rule wall offset may not change. |
| if (!lastWallOffset.equals(recurringRuleTransition.getOffsetAfter())) { |
| transitionList.add(ZoneOffsetTransition.of( |
| localDateTime(currentUnixEpochTime, lastWallOffset), |
| lastWallOffset, |
| recurringRuleTransition.getOffsetAfter())); |
| lastWallOffset = recurringRuleTransition.getOffsetAfter(); |
| } |
| |
| TimeZoneTransition nextTransition = |
| basicTimeZone.getNextTransition(currentUnixEpochTime, false /* inclusive */); |
| currentUnixEpochTime = nextTransition.getTime(); |
| currentTimeZoneRule = (AnnualTimeZoneRule) nextTransition.getTo(); |
| |
| if (currentTimeZoneRule == null) { |
| throw zoneRulesException("No transitions after " |
| + currentUnixEpochTime + " for a timezone with recurring rules"); |
| } |
| } while (!currentTimeZoneRule.isEquivalentTo(firstAnnualRule)); |
| |
| // All annual rules use the same standard offset and wall offset is always updated on |
| // transition. |
| // The initial value of lastWallOffset is the wall offset of the last recurring |
| // AnnualTimeZoneRule. |
| for (AnnualTimeZoneRule annualTimeZoneRule : annualTimeZoneRules) { |
| ZoneOffsetTransitionRule zoneOffsetTransitionRule = |
| createZoneOffsetTransitionRule( |
| annualTimeZoneRule, lastStandardOffset, lastWallOffset); |
| |
| lastWallOffset = zoneOffsetTransitionRule.getOffsetAfter(); |
| |
| lastRules.add(zoneOffsetTransitionRule); |
| } |
| |
| // ZoneRules does not specify it, but internally it expects lastRules to be sorted |
| // (see ZoneRules#getOffset) in the order they will happen within a year. For example, |
| // if rule A starts in October 2021, and rule B starts in March 2022, expected order |
| // is [B, A]. |
| // We assume that for any year that order is fixed, even though it is possible |
| // to build set of rules where order depends on a given year. |
| // annualTimeZoneRules stores rules in the order they happened, so we just need to find |
| // a break in startYear. |
| int firstRuleIndex = 0; |
| while (firstRuleIndex < annualTimeZoneRules.size() |
| && firstAnnualRule.getStartYear() |
| == annualTimeZoneRules.get(firstRuleIndex).getStartYear()) { |
| ++firstRuleIndex; |
| } |
| |
| Collections.rotate(lastRules, -firstRuleIndex); |
| } |
| |
| return ZoneRules.of( |
| baseStandardOffset, |
| baseWallOffset, |
| standardOffsetTransitionList, |
| transitionList, |
| lastRules); |
| } |
| |
| |
| /** |
| * Converts {@link AnnualTimeZoneRule} to {@link ZoneOffsetTransitionRule}. Switch date may be |
| * represented relative to UTC, wall clock, or standard offset. For the latter 2 cases |
| * {@code lastWallOffset} and {@code lastStandardOffset} are used. |
| * |
| * @param annualTimeZoneRule rule to be converted |
| * @param lastStandardOffset standard offset of a rule which preceded {@code |
| * annualTimeZoneRule} |
| * @param lastWallOffset wall offset of a rule which preceded {@code annualTimeZoneRule} |
| */ |
| private ZoneOffsetTransitionRule createZoneOffsetTransitionRule( |
| AnnualTimeZoneRule annualTimeZoneRule, |
| ZoneOffset lastStandardOffset, |
| ZoneOffset lastWallOffset) { |
| DateTimeRule dateTimeRule = annualTimeZoneRule.getRule(); |
| Month month = Month.of(dateTimeRule.getRuleMonth() + 1); |
| final DayOfWeek dayOfWeek; |
| final int dayOfMonthIndicator; |
| switch (dateTimeRule.getDateRuleType()) { |
| case DateTimeRule.DOM: |
| dayOfMonthIndicator = dateTimeRule.getRuleDayOfMonth(); |
| dayOfWeek = null; |
| break; |
| case DateTimeRule.DOW: |
| int weekInMonth = dateTimeRule.getRuleWeekInMonth(); |
| if (weekInMonth > 0) { |
| dayOfMonthIndicator = (weekInMonth - 1) * 7 + 1; |
| } else if (weekInMonth < 0) { |
| dayOfMonthIndicator = (weekInMonth + 1) * 7 - 1; |
| } else { |
| throw zoneRulesException("Invalid DateTimeRule in " |
| + annualTimeZoneRule + |
| ". Non-zero weekInMonth expected in " + dateTimeRule); |
| } |
| dayOfWeek = dayOfWeek(dateTimeRule); |
| break; |
| case DateTimeRule.DOW_GEQ_DOM: |
| dayOfMonthIndicator = dateTimeRule.getRuleDayOfMonth(); |
| dayOfWeek = dayOfWeek(dateTimeRule); |
| break; |
| case DateTimeRule.DOW_LEQ_DOM: |
| // java.time.ZoneRules uses negative numbers to indicate that switch date should |
| // come before certain date. Using leap year so that lastSun like rule will |
| // always work correctly. |
| dayOfMonthIndicator = |
| dateTimeRule.getRuleDayOfMonth() - month.maxLength() - 1; |
| dayOfWeek = dayOfWeek(dateTimeRule); |
| break; |
| default: |
| throw zoneRulesException("Unexpected dateTimeRule.dateRuleType=" |
| + dateTimeRule.getTimeRuleType() + " in " + annualTimeZoneRule); |
| } |
| |
| final boolean timeEndOfDay; |
| final LocalTime switchDateTime; |
| if (dateTimeRule.getRuleMillisInDay() == Grego.MILLIS_PER_DAY) { |
| timeEndOfDay = true; |
| switchDateTime = LocalTime.MIDNIGHT; |
| } else { |
| timeEndOfDay = false; |
| switchDateTime = LocalTime.ofNanoOfDay(dateTimeRule.getRuleMillisInDay() * 1_000_000L); |
| } |
| |
| ZoneOffsetTransitionRule.TimeDefinition timeDefinition = timeDefinition(annualTimeZoneRule); |
| |
| // JavaDoc for standardOffset tells that it should be "offset in force at the cutover". |
| // It's not clear what offset is in effect at the cutover moment, but zic format assumes |
| // that only DST is changed in annual rules and standard offset is handled differently. |
| return ZoneOffsetTransitionRule.of(month, |
| dayOfMonthIndicator, |
| dayOfWeek, |
| switchDateTime, |
| timeEndOfDay, |
| timeDefinition, |
| lastStandardOffset, |
| lastWallOffset, |
| fullOffset(annualTimeZoneRule)); |
| } |
| |
| private ZoneOffsetTransitionRule.TimeDefinition timeDefinition( |
| AnnualTimeZoneRule annualTimeZoneRule) { |
| DateTimeRule dateTimeRule = annualTimeZoneRule.getRule(); |
| switch (dateTimeRule.getTimeRuleType()) { |
| case DateTimeRule.STANDARD_TIME: |
| return ZoneOffsetTransitionRule.TimeDefinition.STANDARD; |
| case DateTimeRule.UTC_TIME: |
| return ZoneOffsetTransitionRule.TimeDefinition.UTC; |
| case DateTimeRule.WALL_TIME: |
| return ZoneOffsetTransitionRule.TimeDefinition.WALL; |
| default: |
| throw zoneRulesException( |
| "Unexpected dateTimeRule.timeRuleType=" + dateTimeRule.getTimeRuleType() |
| + " in AnnualTimeZoneRule: " + annualTimeZoneRule); |
| } |
| } |
| |
| private long utcStartTime(long startTime, TimeArrayTimeZoneRule timeZoneRule) { |
| switch (timeZoneRule.getTimeType()) { |
| case DateTimeRule.UTC_TIME: |
| return startTime; |
| case DateTimeRule.STANDARD_TIME: |
| return startTime - timeZoneRule.getRawOffset(); |
| case DateTimeRule.WALL_TIME: |
| return startTime - timeZoneRule.getRawOffset() - timeZoneRule.getDSTSavings(); |
| default: |
| throw zoneRulesException("Unexpected timeType in " + timeZoneRule); |
| } |
| } |
| |
| private ZoneRulesException zoneRulesException(String message) { |
| return new ZoneRulesException("Failed to build ZoneRules for " + timezone.getID() + |
| ". " + message); |
| } |
| |
| private static LocalDateTime localDateTime(long epochMillis, ZoneOffset zoneOffset) { |
| return Instant.ofEpochMilli(epochMillis) |
| .atOffset(zoneOffset) |
| .toLocalDateTime(); |
| } |
| |
| private static DayOfWeek dayOfWeek(DateTimeRule dateTimeRule) { |
| return DayOfWeek.SUNDAY.plus(dateTimeRule.getRuleDayOfWeek() - 1); |
| } |
| |
| private static ZoneOffset standardOffset(TimeZoneRule timeZoneRule) { |
| return toOffset(timeZoneRule.getRawOffset()); |
| } |
| |
| private static ZoneOffset fullOffset(TimeZoneRule timeZoneRule) { |
| return toOffset(timeZoneRule.getRawOffset() + timeZoneRule.getDSTSavings()); |
| } |
| |
| private static ZoneOffset toOffset(int rawOffset) { |
| return ZoneOffset.ofTotalSeconds(rawOffset / 1_000); |
| } |
| } |