2 *******************************************************************************
\r
3 * Copyright (C) 2007-2008, International Business Machines Corporation and *
\r
4 * others. All Rights Reserved. *
\r
5 *******************************************************************************
\r
7 package com.ibm.icu.util;
\r
8 import java.util.ArrayList;
\r
9 import java.util.BitSet;
\r
10 import java.util.Date;
\r
11 import java.util.Iterator;
\r
12 import java.util.List;
\r
14 import com.ibm.icu.impl.Grego;
\r
17 * <code>RuleBasedTimeZone</code> is a concrete subclass of <code>TimeZone</code> that allows users to define
\r
18 * custom historic time transition rules.
\r
20 * @see com.ibm.icu.util.TimeZoneRule
\r
24 public class RuleBasedTimeZone extends BasicTimeZone {
\r
26 private static final long serialVersionUID = 7580833058949327935L;
\r
28 private final InitialTimeZoneRule initialRule;
\r
29 private List historicRules;
\r
30 private AnnualTimeZoneRule[] finalRules;
\r
32 private transient List historicTransitions;
\r
33 private transient boolean upToDate;
\r
36 * Constructs a <code>RuleBasedTimeZone</code> object with the ID and the
\r
37 * <code>InitialTimeZoneRule</code>
\r
39 * @param id The time zone ID.
\r
40 * @param initialRule The initial time zone rule.
\r
44 public RuleBasedTimeZone(String id, InitialTimeZoneRule initialRule) {
\r
46 this.initialRule = initialRule;
\r
50 * Adds the <code>TimeZoneRule</code> which represents time transitions.
\r
51 * The <code>TimeZoneRule</code> must have start times, that is, the result
\r
52 * of {@link com.ibm.icu.util.TimeZoneRule#isTransitionRule()} must be true.
\r
53 * Otherwise, <code>IllegalArgumentException</code> is thrown.
\r
55 * @param rule The <code>TimeZoneRule</code>.
\r
59 public void addTransitionRule(TimeZoneRule rule) {
\r
60 if (!rule.isTransitionRule()) {
\r
61 throw new IllegalArgumentException("Rule must be a transition rule");
\r
63 if (rule instanceof AnnualTimeZoneRule
\r
64 && ((AnnualTimeZoneRule)rule).getEndYear() == AnnualTimeZoneRule.MAX_YEAR) {
\r
65 // One of the final rules applicable in future forever
\r
66 if (finalRules == null) {
\r
67 finalRules = new AnnualTimeZoneRule[2];
\r
68 finalRules[0] = (AnnualTimeZoneRule)rule;
\r
69 } else if (finalRules[1] == null) {
\r
70 finalRules[1] = (AnnualTimeZoneRule)rule;
\r
72 // Only a pair of AnnualTimeZoneRule is allowed.
\r
73 throw new IllegalStateException("Too many final rules");
\r
76 // If this is not a final rule, add it to the historic rule list
\r
77 if (historicRules == null) {
\r
78 historicRules = new ArrayList();
\r
80 historicRules.add(rule);
\r
82 // Mark dirty, so transitions are recalculated when offset information is
\r
83 // accessed next time.
\r
92 public int getOffset(int era, int year, int month, int day, int dayOfWeek,
\r
94 if (era == GregorianCalendar.BC) {
\r
95 // Convert to extended year
\r
98 long time = Grego.fieldsToDay(year, month, day) * Grego.MILLIS_PER_DAY + milliseconds;
\r
99 int[] offsets = new int[2];
\r
100 getOffset(time, true, LOCAL_DST, LOCAL_STD, offsets);
\r
101 return (offsets[0] + offsets[1]);
\r
109 public void getOffset(long time, boolean local, int[] offsets) {
\r
110 getOffset(time, local, LOCAL_FORMER, LOCAL_LATTER, offsets);
\r
116 * @deprecated This API is ICU internal only.
\r
118 public void getOffsetFromLocal(long date,
\r
119 int nonExistingTimeOpt, int duplicatedTimeOpt, int[] offsets) {
\r
120 getOffset(date, true, nonExistingTimeOpt, duplicatedTimeOpt, offsets);
\r
128 public int getRawOffset() {
\r
129 // Note: This implementation returns standard GMT offset
\r
130 // as of current time.
\r
131 long now = System.currentTimeMillis();
\r
132 int[] offsets = new int[2];
\r
133 getOffset(now, false, offsets);
\r
142 public boolean inDaylightTime(Date date) {
\r
143 int[] offsets = new int[2];
\r
144 getOffset(date.getTime(), false, offsets);
\r
145 return (offsets[1] != 0);
\r
154 public void setRawOffset(int offsetMillis) {
\r
155 // TODO: Do nothing for now..
\r
156 throw new UnsupportedOperationException("setRawOffset in RuleBasedTimeZone is not supported.");
\r
165 public boolean useDaylightTime() {
\r
166 // Note: This implementation returns true when
\r
167 // daylight saving time is used as of now or
\r
168 // after the next transition.
\r
169 long now = System.currentTimeMillis();
\r
170 int[] offsets = new int[2];
\r
171 getOffset(now, false, offsets);
\r
172 if (offsets[1] != 0) {
\r
175 // If DST is not used now, check if DST is used after the next transition
\r
176 TimeZoneTransition tt = getNextTransition(now, false);
\r
177 if (tt != null && tt.getTo().getDSTSavings() != 0) {
\r
188 public boolean hasSameRules(TimeZone other) {
\r
189 if (!(other instanceof RuleBasedTimeZone)) {
\r
190 // We cannot reasonably compare rules in different types
\r
193 RuleBasedTimeZone otherRBTZ = (RuleBasedTimeZone)other;
\r
196 if (!initialRule.isEquivalentTo(otherRBTZ.initialRule)) {
\r
201 if (finalRules != null && otherRBTZ.finalRules != null) {
\r
202 for (int i = 0; i < finalRules.length; i++) {
\r
203 if (finalRules[i] == null && otherRBTZ.finalRules[i] == null) {
\r
206 if (finalRules[i] != null && otherRBTZ.finalRules[i] != null
\r
207 && finalRules[i].isEquivalentTo(otherRBTZ.finalRules[i])) {
\r
213 } else if (finalRules != null || otherRBTZ.finalRules != null) {
\r
218 if (historicRules != null && otherRBTZ.historicRules != null) {
\r
219 if (historicRules.size() != otherRBTZ.historicRules.size()) {
\r
222 Iterator it = historicRules.iterator();
\r
223 while (it.hasNext()) {
\r
224 TimeZoneRule rule = (TimeZoneRule)it.next();
\r
225 Iterator oit = otherRBTZ.historicRules.iterator();
\r
226 boolean foundSameRule = false;
\r
227 while (oit.hasNext()) {
\r
228 TimeZoneRule orule = (TimeZoneRule)oit.next();
\r
229 if (rule.isEquivalentTo(orule)) {
\r
230 foundSameRule = true;
\r
234 if (!foundSameRule) {
\r
238 } else if (historicRules != null || otherRBTZ.historicRules != null) {
\r
244 // BasicTimeZone methods
\r
251 public TimeZoneRule[] getTimeZoneRules() {
\r
253 if (historicRules != null) {
\r
254 size += historicRules.size();
\r
257 if (finalRules != null) {
\r
258 if (finalRules[1] != null) {
\r
264 TimeZoneRule[] rules = new TimeZoneRule[size];
\r
265 rules[0] = initialRule;
\r
268 if (historicRules != null) {
\r
269 for (; idx < historicRules.size() + 1; idx++) {
\r
270 rules[idx] = (TimeZoneRule)historicRules.get(idx - 1);
\r
273 if (finalRules != null) {
\r
274 rules[idx++] = finalRules[0];
\r
275 if (finalRules[1] != null) {
\r
276 rules[idx] = finalRules[1];
\r
287 public TimeZoneTransition getNextTransition(long base, boolean inclusive) {
\r
289 if (historicTransitions == null) {
\r
292 boolean isFinal = false;
\r
293 TimeZoneTransition result = null;
\r
294 TimeZoneTransition tzt = (TimeZoneTransition)historicTransitions.get(0);
\r
295 long tt = tzt.getTime();
\r
296 if (tt > base || (inclusive && tt == base)) {
\r
299 int idx = historicTransitions.size() - 1;
\r
300 tzt = (TimeZoneTransition)historicTransitions.get(idx);
\r
301 tt = tzt.getTime();
\r
302 if (inclusive && tt == base) {
\r
304 } else if (tt <= base) {
\r
305 if (finalRules != null) {
\r
306 // Find a transion time with finalRules
\r
307 Date start0 = finalRules[0].getNextStart(base,
\r
308 finalRules[1].getRawOffset(), finalRules[1].getDSTSavings(), inclusive);
\r
309 Date start1 = finalRules[1].getNextStart(base,
\r
310 finalRules[0].getRawOffset(), finalRules[0].getDSTSavings(), inclusive);
\r
312 if (start1.after(start0)) {
\r
313 tzt = new TimeZoneTransition(start0.getTime(), finalRules[1], finalRules[0]);
\r
315 tzt = new TimeZoneTransition(start1.getTime(), finalRules[0], finalRules[1]);
\r
323 // Find a transition within the historic transitions
\r
325 TimeZoneTransition prev = tzt;
\r
327 tzt = (TimeZoneTransition)historicTransitions.get(idx);
\r
328 tt = tzt.getTime();
\r
329 if (tt < base || (!inclusive && tt == base)) {
\r
338 if (result != null) {
\r
339 // For now, this implementation ignore transitions with only zone name changes.
\r
340 TimeZoneRule from = result.getFrom();
\r
341 TimeZoneRule to = result.getTo();
\r
342 if (from.getRawOffset() == to.getRawOffset()
\r
343 && from.getDSTSavings() == to.getDSTSavings()) {
\r
344 // No offset changes. Try next one if not final
\r
348 result = getNextTransition(result.getTime(), false /* always exclusive */);
\r
360 public TimeZoneTransition getPreviousTransition(long base, boolean inclusive) {
\r
362 if (historicTransitions == null) {
\r
365 TimeZoneTransition result = null;
\r
366 TimeZoneTransition tzt = (TimeZoneTransition)historicTransitions.get(0);
\r
367 long tt = tzt.getTime();
\r
368 if (inclusive && tt == base) {
\r
370 } else if (tt >= base) {
\r
373 int idx = historicTransitions.size() - 1;
\r
374 tzt = (TimeZoneTransition)historicTransitions.get(idx);
\r
375 tt = tzt.getTime();
\r
376 if (inclusive && tt == base) {
\r
378 } else if (tt < base) {
\r
379 if (finalRules != null) {
\r
380 // Find a transion time with finalRules
\r
381 Date start0 = finalRules[0].getPreviousStart(base,
\r
382 finalRules[1].getRawOffset(), finalRules[1].getDSTSavings(), inclusive);
\r
383 Date start1 = finalRules[1].getPreviousStart(base,
\r
384 finalRules[0].getRawOffset(), finalRules[0].getDSTSavings(), inclusive);
\r
386 if (start1.before(start0)) {
\r
387 tzt = new TimeZoneTransition(start0.getTime(), finalRules[1], finalRules[0]);
\r
389 tzt = new TimeZoneTransition(start1.getTime(), finalRules[0], finalRules[1]);
\r
394 // Find a transition within the historic transitions
\r
397 tzt = (TimeZoneTransition)historicTransitions.get(idx);
\r
398 tt = tzt.getTime();
\r
399 if (tt < base || (inclusive && tt == base)) {
\r
407 if (result != null) {
\r
408 // For now, this implementation ignore transitions with only zone name changes.
\r
409 TimeZoneRule from = result.getFrom();
\r
410 TimeZoneRule to = result.getTo();
\r
411 if (from.getRawOffset() == to.getRawOffset()
\r
412 && from.getDSTSavings() == to.getDSTSavings()) {
\r
413 // No offset changes. Try previous one
\r
414 result = getPreviousTransition(result.getTime(), false /* always exclusive */);
\r
424 public Object clone() {
\r
425 RuleBasedTimeZone other = (RuleBasedTimeZone)super.clone();
\r
426 if (historicRules != null) {
\r
427 other.historicRules = (List)((ArrayList)historicRules).clone(); // rules are immutable
\r
429 if (finalRules != null) {
\r
430 other.finalRules = (AnnualTimeZoneRule[])finalRules.clone();
\r
438 * Resolve historic transition times and update fields used for offset
\r
441 private void complete() {
\r
443 // No rules were added since last time.
\r
447 // Make sure either no final rules or a pair of AnnualTimeZoneRules
\r
449 if (finalRules != null && finalRules[1] == null) {
\r
450 throw new IllegalStateException("Incomplete final rules");
\r
453 // Create a TimezoneTransition and add to the list
\r
454 if (historicRules != null || finalRules != null) {
\r
455 TimeZoneRule curRule = initialRule;
\r
456 long lastTransitionTime = Grego.MIN_MILLIS;
\r
458 // Build the transition array which represents historical time zone
\r
460 if (historicRules != null) {
\r
461 BitSet done = new BitSet(historicRules.size()); // for skipping rules already processed
\r
464 int curStdOffset = curRule.getRawOffset();
\r
465 int curDstSavings = curRule.getDSTSavings();
\r
466 long nextTransitionTime = Grego.MAX_MILLIS;
\r
467 TimeZoneRule nextRule = null;
\r
471 for (int i = 0; i < historicRules.size(); i++) {
\r
475 TimeZoneRule r = (TimeZoneRule)historicRules.get(i);
\r
476 d = r.getNextStart(lastTransitionTime, curStdOffset, curDstSavings, false);
\r
478 // No more transitions from this rule - skip this rule next time
\r
481 if (r == curRule ||
\r
482 (r.getName().equals(curRule.getName())
\r
483 && r.getRawOffset() == curRule.getRawOffset()
\r
484 && r.getDSTSavings() == curRule.getDSTSavings())) {
\r
488 if (tt < nextTransitionTime) {
\r
489 nextTransitionTime = tt;
\r
495 if (nextRule == null) {
\r
496 // Check if all historic rules are done
\r
497 boolean bDoneAll = true;
\r
498 for (int j = 0; j < historicRules.size(); j++) {
\r
499 if (!done.get(j)) {
\r
509 if (finalRules != null) {
\r
510 // Check if one of final rules has earlier transition date
\r
511 for (int i = 0; i < 2 /* finalRules.length */; i++) {
\r
512 if (finalRules[i] == curRule) {
\r
515 d = finalRules[i].getNextStart(lastTransitionTime, curStdOffset, curDstSavings, false);
\r
518 if (tt < nextTransitionTime) {
\r
519 nextTransitionTime = tt;
\r
520 nextRule = finalRules[i];
\r
526 if (nextRule == null) {
\r
531 if (historicTransitions == null) {
\r
532 historicTransitions = new ArrayList();
\r
534 historicTransitions.add(new TimeZoneTransition(nextTransitionTime, curRule, nextRule));
\r
535 lastTransitionTime = nextTransitionTime;
\r
536 curRule = nextRule;
\r
539 if (finalRules != null) {
\r
540 if (historicTransitions == null) {
\r
541 historicTransitions = new ArrayList();
\r
543 // Append the first transition for each
\r
544 Date d0 = finalRules[0].getNextStart(lastTransitionTime, curRule.getRawOffset(), curRule.getDSTSavings(), false);
\r
545 Date d1 = finalRules[1].getNextStart(lastTransitionTime, curRule.getRawOffset(), curRule.getDSTSavings(), false);
\r
546 if (d1.after(d0)) {
\r
547 historicTransitions.add(new TimeZoneTransition(d0.getTime(), curRule, finalRules[0]));
\r
548 d1 = finalRules[1].getNextStart(d0.getTime(), finalRules[0].getRawOffset(), finalRules[0].getDSTSavings(), false);
\r
549 historicTransitions.add(new TimeZoneTransition(d1.getTime(), finalRules[0], finalRules[1]));
\r
551 historicTransitions.add(new TimeZoneTransition(d1.getTime(), curRule, finalRules[1]));
\r
552 d0 = finalRules[0].getNextStart(d1.getTime(), finalRules[1].getRawOffset(), finalRules[1].getDSTSavings(), false);
\r
553 historicTransitions.add(new TimeZoneTransition(d0.getTime(), finalRules[1], finalRules[0]));
\r
561 * getOffset internal implementation
\r
563 private void getOffset(long time, boolean local, int NonExistingTimeOpt, int DuplicatedTimeOpt, int[] offsets) {
\r
566 if (historicTransitions == null) {
\r
567 rule = initialRule;
\r
569 long tstart = getTransitionTime((TimeZoneTransition)historicTransitions.get(0),
\r
570 local, NonExistingTimeOpt, DuplicatedTimeOpt);
\r
571 if (time < tstart) {
\r
572 rule = initialRule;
\r
574 int idx = historicTransitions.size() - 1;
\r
575 long tend = getTransitionTime((TimeZoneTransition)historicTransitions.get(idx),
\r
576 local, NonExistingTimeOpt, DuplicatedTimeOpt);
\r
578 if (finalRules != null) {
\r
579 rule = findRuleInFinal(time, local, NonExistingTimeOpt, DuplicatedTimeOpt);
\r
581 // no final rule, use the last rule
\r
582 rule = ((TimeZoneTransition)historicTransitions.get(idx)).getTo();
\r
585 // Find a historical transition
\r
587 if (time >= getTransitionTime((TimeZoneTransition)historicTransitions.get(idx),
\r
588 local, NonExistingTimeOpt, DuplicatedTimeOpt)) {
\r
593 rule = ((TimeZoneTransition)historicTransitions.get(idx)).getTo();
\r
597 offsets[0] = rule.getRawOffset();
\r
598 offsets[1] = rule.getDSTSavings();
\r
602 * Find a time zone rule applicable to the specified time
\r
604 private TimeZoneRule findRuleInFinal(long time, boolean local, int NonExistingTimeOpt, int DuplicatedTimeOpt) {
\r
605 if (finalRules == null || finalRules.length != 2 || finalRules[0] == null || finalRules[1] == null) {
\r
609 Date start0, start1;
\r
615 localDelta = getLocalDelta(finalRules[1].getRawOffset(), finalRules[1].getDSTSavings(),
\r
616 finalRules[0].getRawOffset(), finalRules[0].getDSTSavings(),
\r
617 NonExistingTimeOpt, DuplicatedTimeOpt);
\r
618 base -= localDelta;
\r
620 start0 = finalRules[0].getPreviousStart(base, finalRules[1].getRawOffset(), finalRules[1].getDSTSavings(), true);
\r
624 localDelta = getLocalDelta(finalRules[0].getRawOffset(), finalRules[0].getDSTSavings(),
\r
625 finalRules[1].getRawOffset(), finalRules[1].getDSTSavings(),
\r
626 NonExistingTimeOpt, DuplicatedTimeOpt);
\r
627 base -= localDelta;
\r
629 start1 = finalRules[1].getPreviousStart(base, finalRules[0].getRawOffset(), finalRules[0].getDSTSavings(), true);
\r
631 return start0.after(start1) ? finalRules[0] : finalRules[1];
\r
635 * Get the transition time in local wall clock
\r
637 private static long getTransitionTime(TimeZoneTransition tzt, boolean local,
\r
638 int NonExistingTimeOpt, int DuplicatedTimeOpt) {
\r
639 long time = tzt.getTime();
\r
641 time += getLocalDelta(tzt.getFrom().getRawOffset(), tzt.getFrom().getDSTSavings(),
\r
642 tzt.getTo().getRawOffset(), tzt.getTo().getDSTSavings(),
\r
643 NonExistingTimeOpt, DuplicatedTimeOpt);
\r
649 * Returns amount of local time adjustment used for checking rule transitions
\r
651 private static int getLocalDelta(int rawBefore, int dstBefore, int rawAfter, int dstAfter,
\r
652 int NonExistingTimeOpt, int DuplicatedTimeOpt) {
\r
655 int offsetBefore = rawBefore + dstBefore;
\r
656 int offsetAfter = rawAfter + dstAfter;
\r
658 boolean dstToStd = (dstBefore != 0) && (dstAfter == 0);
\r
659 boolean stdToDst = (dstBefore == 0) && (dstAfter != 0);
\r
661 if (offsetAfter - offsetBefore >= 0) {
\r
662 // Positive transition, which makes a non-existing local time range
\r
663 if (((NonExistingTimeOpt & STD_DST_MASK) == LOCAL_STD && dstToStd)
\r
664 || ((NonExistingTimeOpt & STD_DST_MASK) == LOCAL_DST && stdToDst)) {
\r
665 delta = offsetBefore;
\r
666 } else if (((NonExistingTimeOpt & STD_DST_MASK) == LOCAL_STD && stdToDst)
\r
667 || ((NonExistingTimeOpt & STD_DST_MASK) == LOCAL_DST && dstToStd)) {
\r
668 delta = offsetAfter;
\r
669 } else if ((NonExistingTimeOpt & FORMER_LATTER_MASK) == LOCAL_LATTER) {
\r
670 delta = offsetBefore;
\r
672 // Interprets the time with rule before the transition,
\r
673 // default for non-existing time range
\r
674 delta = offsetAfter;
\r
677 // Negative transition, which makes a duplicated local time range
\r
678 if (((DuplicatedTimeOpt & STD_DST_MASK) == LOCAL_STD && dstToStd)
\r
679 || ((DuplicatedTimeOpt & STD_DST_MASK) == LOCAL_DST && stdToDst)) {
\r
680 delta = offsetAfter;
\r
681 } else if (((DuplicatedTimeOpt & STD_DST_MASK) == LOCAL_STD && stdToDst)
\r
682 || ((DuplicatedTimeOpt & STD_DST_MASK) == LOCAL_DST && dstToStd)) {
\r
683 delta = offsetBefore;
\r
684 } else if ((DuplicatedTimeOpt & FORMER_LATTER_MASK) == LOCAL_FORMER) {
\r
685 delta = offsetBefore;
\r
687 // Interprets the time with rule after the transition,
\r
688 // default for duplicated local time range
\r
689 delta = offsetAfter;
\r