com.ibm.icu.util.RuleBasedTimeZone Maven / Gradle / Ivy
The newest version!
// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
*******************************************************************************
* Copyright (C) 2007-2014, International Business Machines Corporation and *
* others. All Rights Reserved. *
*******************************************************************************
*/
package com.ibm.icu.util;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Date;
import java.util.List;
import com.ibm.icu.impl.Grego;
/**
* RuleBasedTimeZone
is a concrete subclass of TimeZone
that allows users to define
* custom historic time transition rules.
*
* @see com.ibm.icu.util.TimeZoneRule
*
* @stable ICU 3.8
*/
public class RuleBasedTimeZone extends BasicTimeZone {
private static final long serialVersionUID = 7580833058949327935L;
private final InitialTimeZoneRule initialRule;
private List historicRules;
private AnnualTimeZoneRule[] finalRules;
private transient List historicTransitions;
private transient boolean upToDate;
/**
* Constructs a RuleBasedTimeZone
object with the ID and the
* InitialTimeZoneRule
*
* @param id The time zone ID.
* @param initialRule The initial time zone rule.
*
* @stable ICU 3.8
*/
public RuleBasedTimeZone(String id, InitialTimeZoneRule initialRule) {
super(id);
this.initialRule = initialRule;
}
/**
* Adds the TimeZoneRule
which represents time transitions.
* The TimeZoneRule
must have start times, that is, the result
* of {@link com.ibm.icu.util.TimeZoneRule#isTransitionRule()} must be true.
* Otherwise, IllegalArgumentException
is thrown.
*
* @param rule The TimeZoneRule
.
*
* @stable ICU 3.8
*/
public void addTransitionRule(TimeZoneRule rule) {
if (isFrozen()) {
throw new UnsupportedOperationException("Attempt to modify a frozen RuleBasedTimeZone instance.");
}
if (!rule.isTransitionRule()) {
throw new IllegalArgumentException("Rule must be a transition rule");
}
if (rule instanceof AnnualTimeZoneRule
&& ((AnnualTimeZoneRule)rule).getEndYear() == AnnualTimeZoneRule.MAX_YEAR) {
// One of the final rules applicable in future forever
if (finalRules == null) {
finalRules = new AnnualTimeZoneRule[2];
finalRules[0] = (AnnualTimeZoneRule)rule;
} else if (finalRules[1] == null) {
finalRules[1] = (AnnualTimeZoneRule)rule;
} else {
// Only a pair of AnnualTimeZoneRule is allowed.
throw new IllegalStateException("Too many final rules");
}
} else {
// If this is not a final rule, add it to the historic rule list
if (historicRules == null) {
historicRules = new ArrayList<>();
}
historicRules.add(rule);
}
// Mark dirty, so transitions are recalculated when offset information is
// accessed next time.
upToDate = false;
}
/**
* {@inheritDoc}
*
* @stable ICU 3.8
*/
@Override
public int getOffset(int era, int year, int month, int day, int dayOfWeek,
int milliseconds) {
if (era == GregorianCalendar.BC) {
// Convert to extended year
year = 1 - year;
}
long time = Grego.fieldsToDay(year, month, day) * Grego.MILLIS_PER_DAY + milliseconds;
int[] offsets = new int[2];
getOffset(time, true, LOCAL_DST, LOCAL_STD, offsets);
return (offsets[0] + offsets[1]);
}
/**
* {@inheritDoc}
*
* @stable ICU 3.8
*/
@Override
public void getOffset(long time, boolean local, int[] offsets) {
getOffset(time, local, LOCAL_FORMER, LOCAL_LATTER, offsets);
}
/**
* {@inheritDoc}
* @stable ICU 69
*/
@Override
public void getOffsetFromLocal(long date,
LocalOption nonExistingTimeOpt, LocalOption duplicatedTimeOpt, int[] offsets) {
int nonExistingTimeOptVal = getLocalOptionValue(nonExistingTimeOpt);
int duplicatedTimeOptVal = getLocalOptionValue(duplicatedTimeOpt);
getOffset(date, true, nonExistingTimeOptVal, duplicatedTimeOptVal, offsets);
}
/**
* {@inheritDoc}
*
* @stable ICU 3.8
*/
@Override
public int getRawOffset() {
// Note: This implementation returns standard GMT offset
// as of current time.
long now = System.currentTimeMillis();
int[] offsets = new int[2];
getOffset(now, false, offsets);
return offsets[0];
}
/**
* {@inheritDoc}
*
* @stable ICU 3.8
*/
@Override
public boolean inDaylightTime(Date date) {
int[] offsets = new int[2];
getOffset(date.getTime(), false, offsets);
return (offsets[1] != 0);
}
/**
* {@inheritDoc}
*
* @stable ICU 3.8
*/
@Override
///CLOVER:OFF
public void setRawOffset(int offsetMillis) {
// TODO: Do nothing for now..
throw new UnsupportedOperationException("setRawOffset in RuleBasedTimeZone is not supported.");
}
///CLOVER:ON
/**
* {@inheritDoc}
*
* @stable ICU 3.8
*/
@Override
public boolean useDaylightTime() {
// Note: This implementation returns true when
// daylight saving time is used as of now or
// after the next transition.
long now = System.currentTimeMillis();
int[] offsets = new int[2];
getOffset(now, false, offsets);
if (offsets[1] != 0) {
return true;
}
// If DST is not used now, check if DST is used after the next transition
TimeZoneTransition tt = getNextTransition(now, false);
if (tt != null && tt.getTo().getDSTSavings() != 0) {
return true;
}
return false;
}
/**
* {@inheritDoc}
* @stable ICU 49
*/
@Override
public boolean observesDaylightTime() {
long time = System.currentTimeMillis();
// Check if daylight saving time is observed now.
int[] offsets = new int[2];
getOffset(time, false, offsets);
if (offsets[1] != 0) {
return true;
}
// If DST is not used now, check if DST is used after each transition.
BitSet checkFinals = finalRules == null ? null : new BitSet(finalRules.length);
while (true) {
TimeZoneTransition tt = getNextTransition(time, false);
if (tt == null) {
// no more transition
break;
}
TimeZoneRule toRule = tt.getTo();
if (toRule.getDSTSavings() != 0) {
return true;
}
if (checkFinals != null) {
// final rules exist - check if we saw all of them
for (int i = 0; i < finalRules.length; i++) {
if (finalRules[i].equals(toRule)) {
checkFinals.set(i);
}
}
if (checkFinals.cardinality() == finalRules.length) {
// already saw all final rules
break;
}
}
time = tt.getTime();
}
return false;
}
/**
* {@inheritDoc}
*
* @stable ICU 3.8
*/
@Override
public boolean hasSameRules(TimeZone other) {
if (this == other) {
return true;
}
if (!(other instanceof RuleBasedTimeZone)) {
// We cannot reasonably compare rules in different types
return false;
}
RuleBasedTimeZone otherRBTZ = (RuleBasedTimeZone)other;
// initial rule
if (!initialRule.isEquivalentTo(otherRBTZ.initialRule)) {
return false;
}
// final rules
if (finalRules != null && otherRBTZ.finalRules != null) {
for (int i = 0; i < finalRules.length; i++) {
if (finalRules[i] == null && otherRBTZ.finalRules[i] == null) {
continue;
}
if (finalRules[i] != null && otherRBTZ.finalRules[i] != null
&& finalRules[i].isEquivalentTo(otherRBTZ.finalRules[i])) {
continue;
}
return false;
}
} else if (finalRules != null || otherRBTZ.finalRules != null) {
return false;
}
// historic rules
if (historicRules != null && otherRBTZ.historicRules != null) {
if (historicRules.size() != otherRBTZ.historicRules.size()) {
return false;
}
for (TimeZoneRule rule : historicRules) {
boolean foundSameRule = false;
for (TimeZoneRule orule : otherRBTZ.historicRules) {
if (rule.isEquivalentTo(orule)) {
foundSameRule = true;
break;
}
}
if (!foundSameRule) {
return false;
}
}
} else if (historicRules != null || otherRBTZ.historicRules != null) {
return false;
}
return true;
}
// BasicTimeZone methods
/**
* {@inheritDoc}
*
* @stable ICU 3.8
*/
@Override
public TimeZoneRule[] getTimeZoneRules() {
int size = 1;
if (historicRules != null) {
size += historicRules.size();
}
if (finalRules != null) {
if (finalRules[1] != null) {
size += 2;
} else {
size++;
}
}
TimeZoneRule[] rules = new TimeZoneRule[size];
rules[0] = initialRule;
int idx = 1;
if (historicRules != null) {
for (; idx < historicRules.size() + 1; idx++) {
rules[idx] = historicRules.get(idx - 1);
}
}
if (finalRules != null) {
rules[idx++] = finalRules[0];
if (finalRules[1] != null) {
rules[idx] = finalRules[1];
}
}
return rules;
}
/**
* {@inheritDoc}
*
* @stable ICU 3.8
*/
@Override
public TimeZoneTransition getNextTransition(long base, boolean inclusive) {
complete();
if (historicTransitions == null) {
return null;
}
boolean isFinal = false;
TimeZoneTransition result;
TimeZoneTransition tzt = historicTransitions.get(0);
long tt = tzt.getTime();
if (tt > base || (inclusive && tt == base)) {
result = tzt;
} else {
int idx = historicTransitions.size() - 1;
tzt = historicTransitions.get(idx);
tt = tzt.getTime();
if (inclusive && tt == base) {
result = tzt;
} else if (tt <= base) {
if (finalRules != null) {
// Find a transion time with finalRules
Date start0 = finalRules[0].getNextStart(base,
finalRules[1].getRawOffset(), finalRules[1].getDSTSavings(), inclusive);
Date start1 = finalRules[1].getNextStart(base,
finalRules[0].getRawOffset(), finalRules[0].getDSTSavings(), inclusive);
if (start1.after(start0)) {
tzt = new TimeZoneTransition(start0.getTime(), finalRules[1], finalRules[0]);
} else {
tzt = new TimeZoneTransition(start1.getTime(), finalRules[0], finalRules[1]);
}
result = tzt;
isFinal = true;
} else {
return null;
}
} else {
// Find a transition within the historic transitions
idx--;
TimeZoneTransition prev = tzt;
while (idx > 0) {
tzt = historicTransitions.get(idx);
tt = tzt.getTime();
if (tt < base || (!inclusive && tt == base)) {
break;
}
idx--;
prev = tzt;
}
result = prev;
}
}
// For now, this implementation ignore transitions with only zone name changes.
TimeZoneRule from = result.getFrom();
TimeZoneRule to = result.getTo();
if (from.getRawOffset() == to.getRawOffset()
&& from.getDSTSavings() == to.getDSTSavings()) {
// No offset changes. Try next one if not final
if (isFinal) {
return null;
} else {
result = getNextTransition(result.getTime(), false /* always exclusive */);
}
}
return result;
}
/**
* {@inheritDoc}
*
* @stable ICU 3.8
*/
@Override
public TimeZoneTransition getPreviousTransition(long base, boolean inclusive) {
complete();
if (historicTransitions == null) {
return null;
}
TimeZoneTransition result;
TimeZoneTransition tzt = historicTransitions.get(0);
long tt = tzt.getTime();
if (inclusive && tt == base) {
result = tzt;
} else if (tt >= base) {
return null;
} else {
int idx = historicTransitions.size() - 1;
tzt = historicTransitions.get(idx);
tt = tzt.getTime();
if (inclusive && tt == base) {
result = tzt;
} else if (tt < base) {
if (finalRules != null) {
// Find a transion time with finalRules
Date start0 = finalRules[0].getPreviousStart(base,
finalRules[1].getRawOffset(), finalRules[1].getDSTSavings(), inclusive);
Date start1 = finalRules[1].getPreviousStart(base,
finalRules[0].getRawOffset(), finalRules[0].getDSTSavings(), inclusive);
if (start1.before(start0)) {
tzt = new TimeZoneTransition(start0.getTime(), finalRules[1], finalRules[0]);
} else {
tzt = new TimeZoneTransition(start1.getTime(), finalRules[0], finalRules[1]);
}
}
result = tzt;
} else {
// Find a transition within the historic transitions
idx--;
while (idx >= 0) {
tzt = historicTransitions.get(idx);
tt = tzt.getTime();
if (tt < base || (inclusive && tt == base)) {
break;
}
idx--;
}
result = tzt;
}
}
// For now, this implementation ignore transitions with only zone name changes.
TimeZoneRule from = result.getFrom();
TimeZoneRule to = result.getTo();
if (from.getRawOffset() == to.getRawOffset()
&& from.getDSTSavings() == to.getDSTSavings()) {
// No offset changes. Try previous one
result = getPreviousTransition(result.getTime(), false /* always exclusive */);
}
return result;
}
/**
* {@inheritDoc}
* @stable ICU 3.8
*/
@Override
public Object clone() {
if (isFrozen()) {
return this;
}
return cloneAsThawed();
}
// private stuff
/*
* Resolve historic transition times and update fields used for offset
* calculation.
*/
private void complete() {
if (upToDate) {
// No rules were added since last time.
return;
}
// Make sure either no final rules or a pair of AnnualTimeZoneRules
// are available.
if (finalRules != null && finalRules[1] == null) {
throw new IllegalStateException("Incomplete final rules");
}
// Create a TimezoneTransition and add to the list
if (historicRules != null || finalRules != null) {
TimeZoneRule curRule = initialRule;
long lastTransitionTime = Grego.MIN_MILLIS;
// Build the transition array which represents historical time zone
// transitions.
if (historicRules != null) {
BitSet done = new BitSet(historicRules.size()); // for skipping rules already processed
while (true) {
int curStdOffset = curRule.getRawOffset();
int curDstSavings = curRule.getDSTSavings();
long nextTransitionTime = Grego.MAX_MILLIS;
TimeZoneRule nextRule = null;
Date d;
long tt;
for (int i = 0; i < historicRules.size(); i++) {
if (done.get(i)) {
continue;
}
TimeZoneRule r = historicRules.get(i);
d = r.getNextStart(lastTransitionTime, curStdOffset, curDstSavings, false);
if (d == null) {
// No more transitions from this rule - skip this rule next time
done.set(i);
} else {
if (r == curRule ||
(r.getName().equals(curRule.getName())
&& r.getRawOffset() == curRule.getRawOffset()
&& r.getDSTSavings() == curRule.getDSTSavings())) {
continue;
}
tt = d.getTime();
if (tt < nextTransitionTime) {
nextTransitionTime = tt;
nextRule = r;
}
}
}
if (nextRule == null) {
// Check if all historic rules are done
boolean bDoneAll = true;
for (int j = 0; j < historicRules.size(); j++) {
if (!done.get(j)) {
bDoneAll = false;
break;
}
}
if (bDoneAll) {
break;
}
}
if (finalRules != null) {
// Check if one of final rules has earlier transition date
for (int i = 0; i < 2 /* finalRules.length */; i++) {
if (finalRules[i] == curRule) {
continue;
}
d = finalRules[i].getNextStart(lastTransitionTime, curStdOffset, curDstSavings, false);
if (d != null) {
tt = d.getTime();
if (tt < nextTransitionTime) {
nextTransitionTime = tt;
nextRule = finalRules[i];
}
}
}
}
if (nextRule == null) {
// Nothing more
break;
}
if (historicTransitions == null) {
historicTransitions = new ArrayList<>();
}
historicTransitions.add(new TimeZoneTransition(nextTransitionTime, curRule, nextRule));
lastTransitionTime = nextTransitionTime;
curRule = nextRule;
}
}
if (finalRules != null) {
if (historicTransitions == null) {
historicTransitions = new ArrayList<>();
}
// Append the first transition for each
Date d0 = finalRules[0].getNextStart(lastTransitionTime, curRule.getRawOffset(), curRule.getDSTSavings(), false);
Date d1 = finalRules[1].getNextStart(lastTransitionTime, curRule.getRawOffset(), curRule.getDSTSavings(), false);
if (d1.after(d0)) {
historicTransitions.add(new TimeZoneTransition(d0.getTime(), curRule, finalRules[0]));
d1 = finalRules[1].getNextStart(d0.getTime(), finalRules[0].getRawOffset(), finalRules[0].getDSTSavings(), false);
historicTransitions.add(new TimeZoneTransition(d1.getTime(), finalRules[0], finalRules[1]));
} else {
historicTransitions.add(new TimeZoneTransition(d1.getTime(), curRule, finalRules[1]));
d0 = finalRules[0].getNextStart(d1.getTime(), finalRules[1].getRawOffset(), finalRules[1].getDSTSavings(), false);
historicTransitions.add(new TimeZoneTransition(d0.getTime(), finalRules[1], finalRules[0]));
}
}
}
upToDate = true;
}
/*
* getOffset internal implementation
*/
private void getOffset(long time, boolean local, int NonExistingTimeOpt, int DuplicatedTimeOpt, int[] offsets) {
complete();
TimeZoneRule rule = null;
if (historicTransitions == null) {
rule = initialRule;
} else {
long tstart = getTransitionTime(historicTransitions.get(0),
local, NonExistingTimeOpt, DuplicatedTimeOpt);
if (time < tstart) {
rule = initialRule;
} else {
int idx = historicTransitions.size() - 1;
long tend = getTransitionTime(historicTransitions.get(idx),
local, NonExistingTimeOpt, DuplicatedTimeOpt);
if (time > tend) {
if (finalRules != null) {
rule = findRuleInFinal(time, local, NonExistingTimeOpt, DuplicatedTimeOpt);
}
if (rule == null) {
// no final rules or the given time is before the first transition
// specified by the final rules -> use the last rule
rule = (historicTransitions.get(idx)).getTo();
}
} else {
// Find a historical transition
while (idx >= 0) {
if (time >= getTransitionTime(historicTransitions.get(idx),
local, NonExistingTimeOpt, DuplicatedTimeOpt)) {
break;
}
idx--;
}
rule = (historicTransitions.get(idx)).getTo();
}
}
}
offsets[0] = rule.getRawOffset();
offsets[1] = rule.getDSTSavings();
}
/*
* Find a time zone rule applicable to the specified time
*/
private TimeZoneRule findRuleInFinal(long time, boolean local, int NonExistingTimeOpt, int DuplicatedTimeOpt) {
if (finalRules == null || finalRules.length != 2 || finalRules[0] == null || finalRules[1] == null) {
return null;
}
Date start0, start1;
long base;
int localDelta;
base = time;
if (local) {
localDelta = getLocalDelta(finalRules[1].getRawOffset(), finalRules[1].getDSTSavings(),
finalRules[0].getRawOffset(), finalRules[0].getDSTSavings(),
NonExistingTimeOpt, DuplicatedTimeOpt);
base -= localDelta;
}
start0 = finalRules[0].getPreviousStart(base, finalRules[1].getRawOffset(), finalRules[1].getDSTSavings(), true);
base = time;
if (local) {
localDelta = getLocalDelta(finalRules[0].getRawOffset(), finalRules[0].getDSTSavings(),
finalRules[1].getRawOffset(), finalRules[1].getDSTSavings(),
NonExistingTimeOpt, DuplicatedTimeOpt);
base -= localDelta;
}
start1 = finalRules[1].getPreviousStart(base, finalRules[0].getRawOffset(), finalRules[0].getDSTSavings(), true);
if (start0 == null || start1 == null) {
if (start0 != null) {
return finalRules[0];
} else if (start1 != null) {
return finalRules[1];
}
// Both rules take effect after the given time
return null;
}
return start0.after(start1) ? finalRules[0] : finalRules[1];
}
/*
* Get the transition time in local wall clock
*/
private static long getTransitionTime(TimeZoneTransition tzt, boolean local,
int NonExistingTimeOpt, int DuplicatedTimeOpt) {
long time = tzt.getTime();
if (local) {
time += getLocalDelta(tzt.getFrom().getRawOffset(), tzt.getFrom().getDSTSavings(),
tzt.getTo().getRawOffset(), tzt.getTo().getDSTSavings(),
NonExistingTimeOpt, DuplicatedTimeOpt);
}
return time;
}
/*
* Returns amount of local time adjustment used for checking rule transitions
*/
private static int getLocalDelta(int rawBefore, int dstBefore, int rawAfter, int dstAfter,
int NonExistingTimeOpt, int DuplicatedTimeOpt) {
int delta = 0;
int offsetBefore = rawBefore + dstBefore;
int offsetAfter = rawAfter + dstAfter;
boolean dstToStd = (dstBefore != 0) && (dstAfter == 0);
boolean stdToDst = (dstBefore == 0) && (dstAfter != 0);
if (offsetAfter - offsetBefore >= 0) {
// Positive transition, which makes a non-existing local time range
if (((NonExistingTimeOpt & STD_DST_MASK) == LOCAL_STD && dstToStd)
|| ((NonExistingTimeOpt & STD_DST_MASK) == LOCAL_DST && stdToDst)) {
delta = offsetBefore;
} else if (((NonExistingTimeOpt & STD_DST_MASK) == LOCAL_STD && stdToDst)
|| ((NonExistingTimeOpt & STD_DST_MASK) == LOCAL_DST && dstToStd)) {
delta = offsetAfter;
} else if ((NonExistingTimeOpt & FORMER_LATTER_MASK) == LOCAL_LATTER) {
delta = offsetBefore;
} else {
// Interprets the time with rule before the transition,
// default for non-existing time range
delta = offsetAfter;
}
} else {
// Negative transition, which makes a duplicated local time range
if (((DuplicatedTimeOpt & STD_DST_MASK) == LOCAL_STD && dstToStd)
|| ((DuplicatedTimeOpt & STD_DST_MASK) == LOCAL_DST && stdToDst)) {
delta = offsetAfter;
} else if (((DuplicatedTimeOpt & STD_DST_MASK) == LOCAL_STD && stdToDst)
|| ((DuplicatedTimeOpt & STD_DST_MASK) == LOCAL_DST && dstToStd)) {
delta = offsetBefore;
} else if ((DuplicatedTimeOpt & FORMER_LATTER_MASK) == LOCAL_FORMER) {
delta = offsetBefore;
} else {
// Interprets the time with rule after the transition,
// default for duplicated local time range
delta = offsetAfter;
}
}
return delta;
}
// Freezable stuffs
private volatile transient boolean isFrozen = false;
/**
* {@inheritDoc}
* @stable ICU 49
*/
@Override
public boolean isFrozen() {
return isFrozen;
}
/**
* {@inheritDoc}
* @stable ICU 49
*/
@Override
public TimeZone freeze() {
complete();
isFrozen = true;
return this;
}
/**
* {@inheritDoc}
* @stable ICU 49
*/
@Override
public TimeZone cloneAsThawed() {
RuleBasedTimeZone tz = (RuleBasedTimeZone)super.cloneAsThawed();
if (historicRules != null) {
tz.historicRules = new ArrayList<>(historicRules); // rules are immutable
}
if (finalRules != null) {
tz.finalRules = finalRules.clone();
}
tz.isFrozen = false;
return tz;
}
}