com.ibm.icu.util.RuleBasedTimeZone Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of icu4j Show documentation
Show all versions of icu4j Show documentation
International Component for Unicode for Java (ICU4J) is a mature, widely used Java library
providing Unicode and Globalization support
// © 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;
}
}