blob: 5a4e37d8d1723e18bc1fdf5c6a252529b202c680 [file] [log] [blame]
/*
* 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);
}
}
}