| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. The Android Open Source |
| * Project designates this particular file as subject to the "Classpath" |
| * exception as provided by The Android Open Source Project in the LICENSE |
| * file that accompanied this code. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| */ |
| |
| package java.time.zone; |
| |
| import android.icu.util.AnnualTimeZoneRule; |
| import android.icu.util.BasicTimeZone; |
| import android.icu.util.DateTimeRule; |
| import android.icu.util.InitialTimeZoneRule; |
| import android.icu.util.TimeZone; |
| import android.icu.util.TimeZoneRule; |
| import android.icu.util.TimeZoneTransition; |
| import java.time.DayOfWeek; |
| import java.time.LocalTime; |
| import java.time.Month; |
| import java.time.ZoneOffset; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.NavigableMap; |
| import java.util.Set; |
| import java.util.TreeMap; |
| import java.util.concurrent.TimeUnit; |
| import libcore.util.BasicLruCache; |
| |
| /** |
| * A ZoneRulesProvider that generates rules from ICU4J TimeZones. |
| * This provider ensures that classes in {@link java.time} use the same time zone information |
| * as ICU4J. |
| */ |
| public class IcuZoneRulesProvider extends ZoneRulesProvider { |
| |
| // Arbitrary upper limit to number of transitions including the final rules. |
| private static final int MAX_TRANSITIONS = 10000; |
| |
| private static final int SECONDS_IN_DAY = 24 * 60 * 60; |
| |
| private final BasicLruCache<String, ZoneRules> cache = new ZoneRulesCache(8); |
| |
| @Override |
| protected Set<String> provideZoneIds() { |
| Set<String> zoneIds = TimeZone.getAvailableIDs(TimeZone.SystemTimeZoneType.ANY, null, null); |
| zoneIds = new HashSet<>(zoneIds); |
| // java.time assumes ZoneId that start with "GMT" fit the pattern "GMT+HH:mm:ss" which these |
| // do not. Since they are equivalent to GMT, just remove these aliases. |
| zoneIds.remove("GMT+0"); |
| zoneIds.remove("GMT-0"); |
| return zoneIds; |
| } |
| |
| @Override |
| protected ZoneRules provideRules(String zoneId, boolean forCaching) { |
| // Ignore forCaching, as this is a static provider. |
| return cache.get(zoneId); |
| } |
| |
| @Override |
| protected NavigableMap<String, ZoneRules> provideVersions(String zoneId) { |
| return new TreeMap<>( |
| Collections.singletonMap(TimeZone.getTZDataVersion(), |
| provideRules(zoneId, /* forCaching */ false))); |
| } |
| |
| /* |
| * This implementation is only tested with BasicTimeZone objects and depends on |
| * implementation details of that class: |
| * |
| * 0. TimeZone.getFrozenTimeZone() always returns a BasicTimeZone object. |
| * 1. The first rule is always an InitialTimeZoneRule (guaranteed by spec). |
| * 2. AnnualTimeZoneRules are only used as "final rules". |
| * 3. The final rules are either 0 or 2 AnnualTimeZoneRules |
| * 4. The final rules have endYear set to MAX_YEAR. |
| * 5. Each transition generated by the rules changes either the raw offset, the total offset |
| * or both. |
| * 6. There is a non-immense number of transitions for any rule before the final rules apply |
| * (enforced via the arbitrary limit defined in MAX_TRANSITIONS). |
| * |
| * Assumptions #5 and #6 are not strictly required for this code to work, but hold for the |
| * the data and code at the time of implementation. If they were broken they would indicate |
| * an incomplete understanding of how ICU TimeZoneRules are used which would probably mean that |
| * this code needs to be updated. |
| * |
| * These assumptions are verified using the verify() method where appropriate. |
| */ |
| static ZoneRules generateZoneRules(String zoneId) { |
| TimeZone timeZone = TimeZone.getFrozenTimeZone(zoneId); |
| // Assumption #0 |
| verify(timeZone instanceof BasicTimeZone, zoneId, |
| "Unexpected time zone class " + timeZone.getClass()); |
| BasicTimeZone tz = (BasicTimeZone) timeZone; |
| TimeZoneRule[] rules = tz.getTimeZoneRules(); |
| // Assumption #1 |
| InitialTimeZoneRule initial = (InitialTimeZoneRule) rules[0]; |
| |
| ZoneOffset baseStandardOffset = millisToOffset(initial.getRawOffset()); |
| ZoneOffset baseWallOffset = |
| millisToOffset((initial.getRawOffset() + initial.getDSTSavings())); |
| |
| List<ZoneOffsetTransition> standardOffsetTransitionList = new ArrayList<>(); |
| List<ZoneOffsetTransition> transitionList = new ArrayList<>(); |
| List<ZoneOffsetTransitionRule> lastRules = new ArrayList<>(); |
| |
| int preLastDstSavings = 0; |
| AnnualTimeZoneRule last1 = null; |
| AnnualTimeZoneRule last2 = null; |
| |
| TimeZoneTransition transition = tz.getNextTransition(Long.MIN_VALUE, false); |
| int transitionCount = 1; |
| // This loop has two possible exit conditions (in normal operation): |
| // 1. for zones that end with a static value and have no ongoing DST changes, it will exit |
| // via the normal condition (transition != null) |
| // 2. for zones with ongoing DST changes (represented by a "final zone" in ICU4J, and by |
| // "last rules" in java.time) the "break transitionLoop" will be used to exit the loop. |
| transitionLoop: |
| while (transition != null) { |
| TimeZoneRule from = transition.getFrom(); |
| TimeZoneRule to = transition.getTo(); |
| boolean hadEffect = false; |
| if (from.getRawOffset() != to.getRawOffset()) { |
| standardOffsetTransitionList.add(new ZoneOffsetTransition( |
| TimeUnit.MILLISECONDS.toSeconds(transition.getTime()), |
| millisToOffset(from.getRawOffset()), |
| millisToOffset(to.getRawOffset()))); |
| hadEffect = true; |
| } |
| int fromTotalOffset = from.getRawOffset() + from.getDSTSavings(); |
| int toTotalOffset = to.getRawOffset() + to.getDSTSavings(); |
| if (fromTotalOffset != toTotalOffset) { |
| transitionList.add(new ZoneOffsetTransition( |
| TimeUnit.MILLISECONDS.toSeconds(transition.getTime()), |
| millisToOffset(fromTotalOffset), |
| millisToOffset(toTotalOffset))); |
| hadEffect = true; |
| } |
| // Assumption #5 |
| verify(hadEffect, zoneId, "Transition changed neither total nor raw offset."); |
| if (to instanceof AnnualTimeZoneRule) { |
| // The presence of an AnnualTimeZoneRule is taken as an indication of a final rule. |
| if (last1 == null) { |
| preLastDstSavings = from.getDSTSavings(); |
| last1 = (AnnualTimeZoneRule) to; |
| // Assumption #4 |
| verify(last1.getEndYear() == AnnualTimeZoneRule.MAX_YEAR, zoneId, |
| "AnnualTimeZoneRule is not permanent."); |
| } else { |
| last2 = (AnnualTimeZoneRule) to; |
| // Assumption #4 |
| verify(last2.getEndYear() == AnnualTimeZoneRule.MAX_YEAR, zoneId, |
| "AnnualTimeZoneRule is not permanent."); |
| |
| // Assumption #3 |
| transition = tz.getNextTransition(transition.getTime(), false); |
| verify(transition.getTo() == last1, zoneId, |
| "Unexpected rule after 2 AnnualTimeZoneRules."); |
| break transitionLoop; |
| } |
| } else { |
| // Assumption #2 |
| verify(last1 == null, zoneId, "Unexpected rule after AnnualTimeZoneRule."); |
| } |
| verify(transitionCount <= MAX_TRANSITIONS, zoneId, |
| "More than " + MAX_TRANSITIONS + " transitions."); |
| transition = tz.getNextTransition(transition.getTime(), false); |
| transitionCount++; |
| } |
| if (last1 != null) { |
| // Assumption #3 |
| verify(last2 != null, zoneId, "Only one AnnualTimeZoneRule."); |
| lastRules.add(toZoneOffsetTransitionRule(last1, preLastDstSavings)); |
| lastRules.add(toZoneOffsetTransitionRule(last2, last1.getDSTSavings())); |
| } |
| |
| return ZoneRules.of(baseStandardOffset, baseWallOffset, standardOffsetTransitionList, |
| transitionList, lastRules); |
| } |
| |
| /** |
| * Verify an assumption about the zone rules. |
| * |
| * @param check |
| * {@code true} if the assumption holds, {@code false} otherwise. |
| * @param zoneId |
| * Zone ID for which to check. |
| * @param message |
| * Error description of a failed check. |
| * @throws ZoneRulesException |
| * If and only if {@code check} is {@code false}. |
| */ |
| private static void verify(boolean check, String zoneId, String message) { |
| if (!check) { |
| throw new ZoneRulesException( |
| String.format("Failed verification of zone %s: %s", zoneId, message)); |
| } |
| } |
| |
| /** |
| * Transform an {@link AnnualTimeZoneRule} into an equivalent {@link ZoneOffsetTransitionRule}. |
| * This is only used for the "final rules". |
| * |
| * @param rule |
| * The rule to transform. |
| * @param dstSavingMillisBefore |
| * The DST offset before the first transition in milliseconds. |
| */ |
| private static ZoneOffsetTransitionRule toZoneOffsetTransitionRule( |
| AnnualTimeZoneRule rule, int dstSavingMillisBefore) { |
| DateTimeRule dateTimeRule = rule.getRule(); |
| // Calendar.JANUARY is 0, transform it into a proper Month. |
| Month month = Month.JANUARY.plus(dateTimeRule.getRuleMonth()); |
| int dayOfMonthIndicator; |
| // Calendar.SUNDAY is 1, transform it into a proper DayOfWeek. |
| DayOfWeek dayOfWeek = DayOfWeek.SATURDAY.plus(dateTimeRule.getRuleDayOfWeek()); |
| switch (dateTimeRule.getDateRuleType()) { |
| case DateTimeRule.DOM: |
| // Transition always on a specific day of the month. |
| dayOfMonthIndicator = dateTimeRule.getRuleDayOfMonth(); |
| dayOfWeek = null; |
| break; |
| case DateTimeRule.DOW_GEQ_DOM: |
| // ICU representation matches java.time representation. |
| dayOfMonthIndicator = dateTimeRule.getRuleDayOfMonth(); |
| break; |
| case DateTimeRule.DOW_LEQ_DOM: |
| // java.time uses a negative dayOfMonthIndicator to represent "Sun<=X" or "lastSun" |
| // rules. ICU uses this constant and the normal day. So "lastSun" in January would |
| // ruleDayOfMonth = 31 in ICU and dayOfMonthIndicator = -1 in java.time. |
| dayOfMonthIndicator = -month.maxLength() + dateTimeRule.getRuleDayOfMonth() - 1; |
| break; |
| case DateTimeRule.DOW: |
| // DOW is unspecified in the documentation and seems to never be used. |
| throw new ZoneRulesException("Date rule type DOW is unsupported"); |
| default: |
| throw new ZoneRulesException( |
| "Unexpected date rule type: " + dateTimeRule.getDateRuleType()); |
| } |
| // Cast to int is save, as input is int. |
| int secondOfDay = (int) TimeUnit.MILLISECONDS.toSeconds(dateTimeRule.getRuleMillisInDay()); |
| LocalTime time; |
| boolean timeEndOfDay; |
| if (secondOfDay == SECONDS_IN_DAY) { |
| time = LocalTime.MIDNIGHT; |
| timeEndOfDay = true; |
| } else { |
| time = LocalTime.ofSecondOfDay(secondOfDay); |
| timeEndOfDay = false; |
| } |
| ZoneOffsetTransitionRule.TimeDefinition timeDefinition; |
| switch (dateTimeRule.getTimeRuleType()) { |
| case DateTimeRule.WALL_TIME: |
| timeDefinition = ZoneOffsetTransitionRule.TimeDefinition.WALL; |
| break; |
| case DateTimeRule.STANDARD_TIME: |
| timeDefinition = ZoneOffsetTransitionRule.TimeDefinition.STANDARD; |
| break; |
| case DateTimeRule.UTC_TIME: |
| timeDefinition = ZoneOffsetTransitionRule.TimeDefinition.UTC; |
| break; |
| default: |
| throw new ZoneRulesException( |
| "Unexpected time rule type " + dateTimeRule.getTimeRuleType()); |
| } |
| ZoneOffset standardOffset = millisToOffset(rule.getRawOffset()); |
| ZoneOffset offsetBefore = millisToOffset(rule.getRawOffset() + dstSavingMillisBefore); |
| ZoneOffset offsetAfter = millisToOffset( |
| rule.getRawOffset() + rule.getDSTSavings()); |
| return ZoneOffsetTransitionRule.of( |
| month, dayOfMonthIndicator, dayOfWeek, time, timeEndOfDay, timeDefinition, |
| standardOffset, offsetBefore, offsetAfter); |
| } |
| |
| private static ZoneOffset millisToOffset(int offset) { |
| // Cast to int is save, as input is int. |
| return ZoneOffset.ofTotalSeconds((int) TimeUnit.MILLISECONDS.toSeconds(offset)); |
| } |
| |
| private static class ZoneRulesCache extends BasicLruCache<String, ZoneRules> { |
| |
| ZoneRulesCache(int maxSize) { |
| super(maxSize); |
| } |
| |
| @Override |
| protected ZoneRules create(String zoneId) { |
| String canonicalId = TimeZone.getCanonicalID(zoneId); |
| if (!canonicalId.equals(zoneId)) { |
| // Return the same object as the canonical one, to avoid wasting space, but cache |
| // it under the non-cannonical name as well, to avoid future getCanonicalID calls. |
| return get(canonicalId); |
| } |
| return generateZoneRules(zoneId); |
| } |
| } |
| } |