/* | |
******************************************************************************* | |
* Copyright (C) 2007, 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.Iterator; | |
import java.util.List; | |
import com.ibm.icu.impl.Grego; | |
/** | |
* <code>RuleBasedTimeZone</code> is a concrete subclass of <code>TimeZone</code> that allows users to define | |
* custom historic time transition rules. | |
* | |
* @see com.ibm.icu.util.TimeZoneRule | |
* | |
* @draft ICU 3.8 | |
* @provisional This API might change or be removed in a future release. | |
*/ | |
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 <code>RuleBasedTimeZone</code> object with the ID and the | |
* <code>InitialTimeZoneRule</code> | |
* | |
* @param id The time zone ID. | |
* @param initialRule The initial time zone rule. | |
* | |
* @draft ICU 3.8 | |
* @provisional This API might change or be removed in a future release. | |
*/ | |
public RuleBasedTimeZone(String id, InitialTimeZoneRule initialRule) { | |
super.setID(id); | |
this.initialRule = initialRule; | |
} | |
/** | |
* Adds the <code>TimeZoneRule</code> which represents time transitions. | |
* The <code>TimeZoneRule</code> must have start times, that is, the result | |
* of {@link com.ibm.icu.util.TimeZoneRule#isTransitionRule()} must be true. | |
* Otherwise, <code>IllegalArgumentException</code> is thrown. | |
* | |
* @param rule The <code>TimeZoneRule</code>. | |
* | |
* @draft ICU 3.8 | |
* @provisional This API might change or be removed in a future release. | |
*/ | |
public void addTransitionRule(TimeZoneRule rule) { | |
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} | |
* | |
* @draft ICU 3.8 | |
* @provisional This API might change or be removed in a future release. | |
*/ | |
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, offsets); | |
return (offsets[0] + offsets[1]); | |
} | |
/** | |
* {@inheritDoc} | |
* | |
* @draft ICU 3.8 | |
* @provisional This API might change or be removed in a future release. | |
*/ | |
public void getOffset(long time, boolean local, int[] offsets) { | |
complete(); | |
TimeZoneRule rule; | |
if (historicTransitions == null) { | |
rule = initialRule; | |
} else { | |
long tstart = getTransitionTime((TimeZoneTransition)historicTransitions.get(0), local); | |
if (time < tstart) { | |
rule = initialRule; | |
} else { | |
int idx = historicTransitions.size() - 1; | |
long tend = getTransitionTime((TimeZoneTransition)historicTransitions.get(idx), local); | |
if (time > tend) { | |
if (finalRules != null) { | |
rule = findRuleInFinal(time, local); | |
} else { | |
// no final rule, use the last rule | |
rule = ((TimeZoneTransition)historicTransitions.get(idx)).getTo(); | |
} | |
} else { | |
// Find a historical transition | |
while (idx >= 0) { | |
if (time >= getTransitionTime((TimeZoneTransition)historicTransitions.get(idx), local)) { | |
break; | |
} | |
idx--; | |
} | |
rule = ((TimeZoneTransition)historicTransitions.get(idx)).getTo(); | |
} | |
} | |
} | |
offsets[0] = rule.getRawOffset(); | |
offsets[1] = rule.getDSTSavings(); | |
} | |
/** | |
* {@inheritDoc} | |
* | |
* @draft ICU 3.8 | |
* @provisional This API might change or be removed in a future release. | |
*/ | |
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} | |
* | |
* @draft ICU 3.8 | |
* @provisional This API might change or be removed in a future release. | |
*/ | |
public boolean inDaylightTime(Date date) { | |
int[] offsets = new int[2]; | |
getOffset(date.getTime(), false, offsets); | |
return (offsets[1] != 0); | |
} | |
/** | |
* {@inheritDoc} | |
* | |
* @draft ICU 3.8 | |
* @provisional This API might change or be removed in a future release. | |
*/ | |
///CLOVER:OFF | |
public void setRawOffset(int offsetMillis) { | |
// TODO: Do nothing for now.. | |
throw new UnsupportedOperationException("setRawOffset in RuleBasedTimeZone is not supported."); | |
} | |
///CLOVER:ON | |
/** | |
* {@inheritDoc} | |
* | |
* @draft ICU 3.8 | |
* @provisional This API might change or be removed in a future release. | |
*/ | |
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} | |
* | |
* @draft ICU 3.8 | |
* @provisional This API might change or be removed in a future release. | |
*/ | |
public boolean hasSameRules(TimeZone other) { | |
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; | |
} | |
Iterator it = historicRules.iterator(); | |
while (it.hasNext()) { | |
TimeZoneRule rule = (TimeZoneRule)it.next(); | |
Iterator oit = otherRBTZ.historicRules.iterator(); | |
boolean foundSameRule = false; | |
while (oit.hasNext()) { | |
TimeZoneRule orule = (TimeZoneRule)oit.next(); | |
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} | |
* | |
* @draft ICU 3.8 | |
* @provisional This API might change or be removed in a future release. | |
*/ | |
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] = (TimeZoneRule)historicRules.get(idx - 1); | |
} | |
} | |
if (finalRules != null) { | |
rules[idx++] = finalRules[0]; | |
if (finalRules[1] != null) { | |
rules[idx] = finalRules[1]; | |
} | |
} | |
return rules; | |
} | |
/** | |
* {@inheritDoc} | |
* | |
* @draft ICU 3.8 | |
* @provisional This API might change or be removed in a future release. | |
*/ | |
public TimeZoneTransition getNextTransition(long base, boolean inclusive) { | |
complete(); | |
if (historicTransitions == null) { | |
return null; | |
} | |
boolean isFinal = false; | |
TimeZoneTransition result = null; | |
TimeZoneTransition tzt = (TimeZoneTransition)historicTransitions.get(0); | |
long tt = getTransitionTime(tzt, false); | |
if (tt > base || (inclusive && tt == base)) { | |
result = tzt; | |
} else { | |
int idx = historicTransitions.size() - 1; | |
tzt = (TimeZoneTransition)historicTransitions.get(idx); | |
tt = getTransitionTime(tzt, false); | |
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 = (TimeZoneTransition)historicTransitions.get(idx); | |
tt = getTransitionTime(tzt, false); | |
if (tt < base || (!inclusive && tt == base)) { | |
break; | |
} | |
idx--; | |
prev = tzt; | |
} | |
result = prev; | |
} | |
} | |
if (result != null) { | |
// 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} | |
* | |
* @draft ICU 3.8 | |
* @provisional This API might change or be removed in a future release. | |
*/ | |
public TimeZoneTransition getPreviousTransition(long base, boolean inclusive) { | |
complete(); | |
if (historicTransitions == null) { | |
return null; | |
} | |
TimeZoneTransition result = null; | |
TimeZoneTransition tzt = (TimeZoneTransition)historicTransitions.get(0); | |
long tt = getTransitionTime(tzt, false); | |
if (inclusive && tt == base) { | |
result = tzt; | |
} else if (tt >= base) { | |
return null; | |
} else { | |
int idx = historicTransitions.size() - 1; | |
tzt = (TimeZoneTransition)historicTransitions.get(idx); | |
tt = getTransitionTime(tzt, false); | |
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 = (TimeZoneTransition)historicTransitions.get(idx); | |
tt = getTransitionTime(tzt, false); | |
if (tt < base || (inclusive && tt == base)) { | |
break; | |
} | |
idx--; | |
} | |
result = tzt; | |
} | |
} | |
if (result != null) { | |
// 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} | |
* @draft ICU 3.8 | |
* @provisional This API might change or be removed in a future release. | |
*/ | |
public Object clone() { | |
RuleBasedTimeZone other = (RuleBasedTimeZone)super.clone(); | |
if (historicRules != null) { | |
other.historicRules = (List)((ArrayList)historicRules).clone(); // rules are immutable | |
} | |
if (finalRules != null) { | |
other.finalRules = (AnnualTimeZoneRule[])finalRules.clone(); | |
} | |
return other; | |
} | |
// 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 = (TimeZoneRule)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; | |
} | |
/* | |
* Find a time zone rule applicable to the specified time | |
*/ | |
private TimeZoneRule findRuleInFinal(long time, boolean local) { | |
if (finalRules == null || finalRules.length != 2 || finalRules[0] == null || finalRules[1] == null) { | |
return null; | |
} | |
Date start0, start1; | |
long base; | |
base = local ? time - finalRules[1].getRawOffset() - finalRules[1].getDSTSavings() : time; | |
start0 = finalRules[0].getPreviousStart(base, finalRules[1].getRawOffset(), finalRules[1].getDSTSavings(), true); | |
base = local ? time - finalRules[0].getRawOffset() - finalRules[0].getDSTSavings() : time; | |
start1 = finalRules[1].getPreviousStart(base, finalRules[0].getRawOffset(), finalRules[0].getDSTSavings(), true); | |
return start0.after(start1) ? finalRules[0] : finalRules[1]; | |
} | |
/* | |
* Get the transition time in local wall clock | |
*/ | |
private static long getTransitionTime(TimeZoneTransition tzt, boolean local) { | |
long time = tzt.getTime(); | |
if (local) { | |
time += tzt.getFrom().getRawOffset() + tzt.getFrom().getDSTSavings(); | |
} | |
return time; | |
} | |
} | |