2 *******************************************************************************
\r
3 * Copyright (C) 2007-2009, 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.List;
\r
13 import com.ibm.icu.impl.Grego;
\r
16 * <code>RuleBasedTimeZone</code> is a concrete subclass of <code>TimeZone</code> that allows users to define
\r
17 * custom historic time transition rules.
\r
19 * @see com.ibm.icu.util.TimeZoneRule
\r
23 public class RuleBasedTimeZone extends BasicTimeZone {
\r
25 private static final long serialVersionUID = 7580833058949327935L;
\r
27 private final InitialTimeZoneRule initialRule;
\r
28 private List<TimeZoneRule> historicRules;
\r
29 private AnnualTimeZoneRule[] finalRules;
\r
31 private transient List<TimeZoneTransition> historicTransitions;
\r
32 private transient boolean upToDate;
\r
35 * Constructs a <code>RuleBasedTimeZone</code> object with the ID and the
\r
36 * <code>InitialTimeZoneRule</code>
\r
38 * @param id The time zone ID.
\r
39 * @param initialRule The initial time zone rule.
\r
43 public RuleBasedTimeZone(String id, InitialTimeZoneRule initialRule) {
\r
45 this.initialRule = initialRule;
\r
49 * Adds the <code>TimeZoneRule</code> which represents time transitions.
\r
50 * The <code>TimeZoneRule</code> must have start times, that is, the result
\r
51 * of {@link com.ibm.icu.util.TimeZoneRule#isTransitionRule()} must be true.
\r
52 * Otherwise, <code>IllegalArgumentException</code> is thrown.
\r
54 * @param rule The <code>TimeZoneRule</code>.
\r
58 public void addTransitionRule(TimeZoneRule rule) {
\r
59 if (!rule.isTransitionRule()) {
\r
60 throw new IllegalArgumentException("Rule must be a transition rule");
\r
62 if (rule instanceof AnnualTimeZoneRule
\r
63 && ((AnnualTimeZoneRule)rule).getEndYear() == AnnualTimeZoneRule.MAX_YEAR) {
\r
64 // One of the final rules applicable in future forever
\r
65 if (finalRules == null) {
\r
66 finalRules = new AnnualTimeZoneRule[2];
\r
67 finalRules[0] = (AnnualTimeZoneRule)rule;
\r
68 } else if (finalRules[1] == null) {
\r
69 finalRules[1] = (AnnualTimeZoneRule)rule;
\r
71 // Only a pair of AnnualTimeZoneRule is allowed.
\r
72 throw new IllegalStateException("Too many final rules");
\r
75 // If this is not a final rule, add it to the historic rule list
\r
76 if (historicRules == null) {
\r
77 historicRules = new ArrayList<TimeZoneRule>();
\r
79 historicRules.add(rule);
\r
81 // Mark dirty, so transitions are recalculated when offset information is
\r
82 // accessed next time.
\r
91 public int getOffset(int era, int year, int month, int day, int dayOfWeek,
\r
93 if (era == GregorianCalendar.BC) {
\r
94 // Convert to extended year
\r
97 long time = Grego.fieldsToDay(year, month, day) * Grego.MILLIS_PER_DAY + milliseconds;
\r
98 int[] offsets = new int[2];
\r
99 getOffset(time, true, LOCAL_DST, LOCAL_STD, offsets);
\r
100 return (offsets[0] + offsets[1]);
\r
108 public void getOffset(long time, boolean local, int[] offsets) {
\r
109 getOffset(time, local, LOCAL_FORMER, LOCAL_LATTER, offsets);
\r
115 * @deprecated This API is ICU internal only.
\r
117 public void getOffsetFromLocal(long date,
\r
118 int nonExistingTimeOpt, int duplicatedTimeOpt, int[] offsets) {
\r
119 getOffset(date, true, nonExistingTimeOpt, duplicatedTimeOpt, offsets);
\r
127 public int getRawOffset() {
\r
128 // Note: This implementation returns standard GMT offset
\r
129 // as of current time.
\r
130 long now = System.currentTimeMillis();
\r
131 int[] offsets = new int[2];
\r
132 getOffset(now, false, offsets);
\r
141 public boolean inDaylightTime(Date date) {
\r
142 int[] offsets = new int[2];
\r
143 getOffset(date.getTime(), false, offsets);
\r
144 return (offsets[1] != 0);
\r
153 public void setRawOffset(int offsetMillis) {
\r
154 // TODO: Do nothing for now..
\r
155 throw new UnsupportedOperationException("setRawOffset in RuleBasedTimeZone is not supported.");
\r
164 public boolean useDaylightTime() {
\r
165 // Note: This implementation returns true when
\r
166 // daylight saving time is used as of now or
\r
167 // after the next transition.
\r
168 long now = System.currentTimeMillis();
\r
169 int[] offsets = new int[2];
\r
170 getOffset(now, false, offsets);
\r
171 if (offsets[1] != 0) {
\r
174 // If DST is not used now, check if DST is used after the next transition
\r
175 TimeZoneTransition tt = getNextTransition(now, false);
\r
176 if (tt != null && tt.getTo().getDSTSavings() != 0) {
\r
187 public boolean hasSameRules(TimeZone other) {
\r
188 if (!(other instanceof RuleBasedTimeZone)) {
\r
189 // We cannot reasonably compare rules in different types
\r
192 RuleBasedTimeZone otherRBTZ = (RuleBasedTimeZone)other;
\r
195 if (!initialRule.isEquivalentTo(otherRBTZ.initialRule)) {
\r
200 if (finalRules != null && otherRBTZ.finalRules != null) {
\r
201 for (int i = 0; i < finalRules.length; i++) {
\r
202 if (finalRules[i] == null && otherRBTZ.finalRules[i] == null) {
\r
205 if (finalRules[i] != null && otherRBTZ.finalRules[i] != null
\r
206 && finalRules[i].isEquivalentTo(otherRBTZ.finalRules[i])) {
\r
212 } else if (finalRules != null || otherRBTZ.finalRules != null) {
\r
217 if (historicRules != null && otherRBTZ.historicRules != null) {
\r
218 if (historicRules.size() != otherRBTZ.historicRules.size()) {
\r
221 for (TimeZoneRule rule : historicRules) {
\r
222 boolean foundSameRule = false;
\r
223 for (TimeZoneRule orule : otherRBTZ.historicRules) {
\r
224 if (rule.isEquivalentTo(orule)) {
\r
225 foundSameRule = true;
\r
229 if (!foundSameRule) {
\r
233 } else if (historicRules != null || otherRBTZ.historicRules != null) {
\r
239 // BasicTimeZone methods
\r
246 public TimeZoneRule[] getTimeZoneRules() {
\r
248 if (historicRules != null) {
\r
249 size += historicRules.size();
\r
252 if (finalRules != null) {
\r
253 if (finalRules[1] != null) {
\r
259 TimeZoneRule[] rules = new TimeZoneRule[size];
\r
260 rules[0] = initialRule;
\r
263 if (historicRules != null) {
\r
264 for (; idx < historicRules.size() + 1; idx++) {
\r
265 rules[idx] = historicRules.get(idx - 1);
\r
268 if (finalRules != null) {
\r
269 rules[idx++] = finalRules[0];
\r
270 if (finalRules[1] != null) {
\r
271 rules[idx] = finalRules[1];
\r
282 public TimeZoneTransition getNextTransition(long base, boolean inclusive) {
\r
284 if (historicTransitions == null) {
\r
287 boolean isFinal = false;
\r
288 TimeZoneTransition result = null;
\r
289 TimeZoneTransition tzt = historicTransitions.get(0);
\r
290 long tt = tzt.getTime();
\r
291 if (tt > base || (inclusive && tt == base)) {
\r
294 int idx = historicTransitions.size() - 1;
\r
295 tzt = historicTransitions.get(idx);
\r
296 tt = tzt.getTime();
\r
297 if (inclusive && tt == base) {
\r
299 } else if (tt <= base) {
\r
300 if (finalRules != null) {
\r
301 // Find a transion time with finalRules
\r
302 Date start0 = finalRules[0].getNextStart(base,
\r
303 finalRules[1].getRawOffset(), finalRules[1].getDSTSavings(), inclusive);
\r
304 Date start1 = finalRules[1].getNextStart(base,
\r
305 finalRules[0].getRawOffset(), finalRules[0].getDSTSavings(), inclusive);
\r
307 if (start1.after(start0)) {
\r
308 tzt = new TimeZoneTransition(start0.getTime(), finalRules[1], finalRules[0]);
\r
310 tzt = new TimeZoneTransition(start1.getTime(), finalRules[0], finalRules[1]);
\r
318 // Find a transition within the historic transitions
\r
320 TimeZoneTransition prev = tzt;
\r
322 tzt = historicTransitions.get(idx);
\r
323 tt = tzt.getTime();
\r
324 if (tt < base || (!inclusive && tt == base)) {
\r
333 if (result != null) {
\r
334 // For now, this implementation ignore transitions with only zone name changes.
\r
335 TimeZoneRule from = result.getFrom();
\r
336 TimeZoneRule to = result.getTo();
\r
337 if (from.getRawOffset() == to.getRawOffset()
\r
338 && from.getDSTSavings() == to.getDSTSavings()) {
\r
339 // No offset changes. Try next one if not final
\r
343 result = getNextTransition(result.getTime(), false /* always exclusive */);
\r
355 public TimeZoneTransition getPreviousTransition(long base, boolean inclusive) {
\r
357 if (historicTransitions == null) {
\r
360 TimeZoneTransition result = null;
\r
361 TimeZoneTransition tzt = historicTransitions.get(0);
\r
362 long tt = tzt.getTime();
\r
363 if (inclusive && tt == base) {
\r
365 } else if (tt >= base) {
\r
368 int idx = historicTransitions.size() - 1;
\r
369 tzt = historicTransitions.get(idx);
\r
370 tt = tzt.getTime();
\r
371 if (inclusive && tt == base) {
\r
373 } else if (tt < base) {
\r
374 if (finalRules != null) {
\r
375 // Find a transion time with finalRules
\r
376 Date start0 = finalRules[0].getPreviousStart(base,
\r
377 finalRules[1].getRawOffset(), finalRules[1].getDSTSavings(), inclusive);
\r
378 Date start1 = finalRules[1].getPreviousStart(base,
\r
379 finalRules[0].getRawOffset(), finalRules[0].getDSTSavings(), inclusive);
\r
381 if (start1.before(start0)) {
\r
382 tzt = new TimeZoneTransition(start0.getTime(), finalRules[1], finalRules[0]);
\r
384 tzt = new TimeZoneTransition(start1.getTime(), finalRules[0], finalRules[1]);
\r
389 // Find a transition within the historic transitions
\r
392 tzt = historicTransitions.get(idx);
\r
393 tt = tzt.getTime();
\r
394 if (tt < base || (inclusive && tt == base)) {
\r
402 if (result != null) {
\r
403 // For now, this implementation ignore transitions with only zone name changes.
\r
404 TimeZoneRule from = result.getFrom();
\r
405 TimeZoneRule to = result.getTo();
\r
406 if (from.getRawOffset() == to.getRawOffset()
\r
407 && from.getDSTSavings() == to.getDSTSavings()) {
\r
408 // No offset changes. Try previous one
\r
409 result = getPreviousTransition(result.getTime(), false /* always exclusive */);
\r
419 public Object clone() {
\r
420 RuleBasedTimeZone other = (RuleBasedTimeZone)super.clone();
\r
421 if (historicRules != null) {
\r
422 other.historicRules = new ArrayList<TimeZoneRule>(historicRules); // rules are immutable
\r
424 if (finalRules != null) {
\r
425 other.finalRules = finalRules.clone();
\r
433 * Resolve historic transition times and update fields used for offset
\r
436 private void complete() {
\r
438 // No rules were added since last time.
\r
442 // Make sure either no final rules or a pair of AnnualTimeZoneRules
\r
444 if (finalRules != null && finalRules[1] == null) {
\r
445 throw new IllegalStateException("Incomplete final rules");
\r
448 // Create a TimezoneTransition and add to the list
\r
449 if (historicRules != null || finalRules != null) {
\r
450 TimeZoneRule curRule = initialRule;
\r
451 long lastTransitionTime = Grego.MIN_MILLIS;
\r
453 // Build the transition array which represents historical time zone
\r
455 if (historicRules != null) {
\r
456 BitSet done = new BitSet(historicRules.size()); // for skipping rules already processed
\r
459 int curStdOffset = curRule.getRawOffset();
\r
460 int curDstSavings = curRule.getDSTSavings();
\r
461 long nextTransitionTime = Grego.MAX_MILLIS;
\r
462 TimeZoneRule nextRule = null;
\r
466 for (int i = 0; i < historicRules.size(); i++) {
\r
470 TimeZoneRule r = historicRules.get(i);
\r
471 d = r.getNextStart(lastTransitionTime, curStdOffset, curDstSavings, false);
\r
473 // No more transitions from this rule - skip this rule next time
\r
476 if (r == curRule ||
\r
477 (r.getName().equals(curRule.getName())
\r
478 && r.getRawOffset() == curRule.getRawOffset()
\r
479 && r.getDSTSavings() == curRule.getDSTSavings())) {
\r
483 if (tt < nextTransitionTime) {
\r
484 nextTransitionTime = tt;
\r
490 if (nextRule == null) {
\r
491 // Check if all historic rules are done
\r
492 boolean bDoneAll = true;
\r
493 for (int j = 0; j < historicRules.size(); j++) {
\r
494 if (!done.get(j)) {
\r
504 if (finalRules != null) {
\r
505 // Check if one of final rules has earlier transition date
\r
506 for (int i = 0; i < 2 /* finalRules.length */; i++) {
\r
507 if (finalRules[i] == curRule) {
\r
510 d = finalRules[i].getNextStart(lastTransitionTime, curStdOffset, curDstSavings, false);
\r
513 if (tt < nextTransitionTime) {
\r
514 nextTransitionTime = tt;
\r
515 nextRule = finalRules[i];
\r
521 if (nextRule == null) {
\r
526 if (historicTransitions == null) {
\r
527 historicTransitions = new ArrayList<TimeZoneTransition>();
\r
529 historicTransitions.add(new TimeZoneTransition(nextTransitionTime, curRule, nextRule));
\r
530 lastTransitionTime = nextTransitionTime;
\r
531 curRule = nextRule;
\r
534 if (finalRules != null) {
\r
535 if (historicTransitions == null) {
\r
536 historicTransitions = new ArrayList<TimeZoneTransition>();
\r
538 // Append the first transition for each
\r
539 Date d0 = finalRules[0].getNextStart(lastTransitionTime, curRule.getRawOffset(), curRule.getDSTSavings(), false);
\r
540 Date d1 = finalRules[1].getNextStart(lastTransitionTime, curRule.getRawOffset(), curRule.getDSTSavings(), false);
\r
541 if (d1.after(d0)) {
\r
542 historicTransitions.add(new TimeZoneTransition(d0.getTime(), curRule, finalRules[0]));
\r
543 d1 = finalRules[1].getNextStart(d0.getTime(), finalRules[0].getRawOffset(), finalRules[0].getDSTSavings(), false);
\r
544 historicTransitions.add(new TimeZoneTransition(d1.getTime(), finalRules[0], finalRules[1]));
\r
546 historicTransitions.add(new TimeZoneTransition(d1.getTime(), curRule, finalRules[1]));
\r
547 d0 = finalRules[0].getNextStart(d1.getTime(), finalRules[1].getRawOffset(), finalRules[1].getDSTSavings(), false);
\r
548 historicTransitions.add(new TimeZoneTransition(d0.getTime(), finalRules[1], finalRules[0]));
\r
556 * getOffset internal implementation
\r
558 private void getOffset(long time, boolean local, int NonExistingTimeOpt, int DuplicatedTimeOpt, int[] offsets) {
\r
561 if (historicTransitions == null) {
\r
562 rule = initialRule;
\r
564 long tstart = getTransitionTime(historicTransitions.get(0),
\r
565 local, NonExistingTimeOpt, DuplicatedTimeOpt);
\r
566 if (time < tstart) {
\r
567 rule = initialRule;
\r
569 int idx = historicTransitions.size() - 1;
\r
570 long tend = getTransitionTime(historicTransitions.get(idx),
\r
571 local, NonExistingTimeOpt, DuplicatedTimeOpt);
\r
573 if (finalRules != null) {
\r
574 rule = findRuleInFinal(time, local, NonExistingTimeOpt, DuplicatedTimeOpt);
\r
576 // no final rule, use the last rule
\r
577 rule = (historicTransitions.get(idx)).getTo();
\r
580 // Find a historical transition
\r
582 if (time >= getTransitionTime(historicTransitions.get(idx),
\r
583 local, NonExistingTimeOpt, DuplicatedTimeOpt)) {
\r
588 rule = (historicTransitions.get(idx)).getTo();
\r
592 offsets[0] = rule.getRawOffset();
\r
593 offsets[1] = rule.getDSTSavings();
\r
597 * Find a time zone rule applicable to the specified time
\r
599 private TimeZoneRule findRuleInFinal(long time, boolean local, int NonExistingTimeOpt, int DuplicatedTimeOpt) {
\r
600 if (finalRules == null || finalRules.length != 2 || finalRules[0] == null || finalRules[1] == null) {
\r
604 Date start0, start1;
\r
610 localDelta = getLocalDelta(finalRules[1].getRawOffset(), finalRules[1].getDSTSavings(),
\r
611 finalRules[0].getRawOffset(), finalRules[0].getDSTSavings(),
\r
612 NonExistingTimeOpt, DuplicatedTimeOpt);
\r
613 base -= localDelta;
\r
615 start0 = finalRules[0].getPreviousStart(base, finalRules[1].getRawOffset(), finalRules[1].getDSTSavings(), true);
\r
619 localDelta = getLocalDelta(finalRules[0].getRawOffset(), finalRules[0].getDSTSavings(),
\r
620 finalRules[1].getRawOffset(), finalRules[1].getDSTSavings(),
\r
621 NonExistingTimeOpt, DuplicatedTimeOpt);
\r
622 base -= localDelta;
\r
624 start1 = finalRules[1].getPreviousStart(base, finalRules[0].getRawOffset(), finalRules[0].getDSTSavings(), true);
\r
626 return start0.after(start1) ? finalRules[0] : finalRules[1];
\r
630 * Get the transition time in local wall clock
\r
632 private static long getTransitionTime(TimeZoneTransition tzt, boolean local,
\r
633 int NonExistingTimeOpt, int DuplicatedTimeOpt) {
\r
634 long time = tzt.getTime();
\r
636 time += getLocalDelta(tzt.getFrom().getRawOffset(), tzt.getFrom().getDSTSavings(),
\r
637 tzt.getTo().getRawOffset(), tzt.getTo().getDSTSavings(),
\r
638 NonExistingTimeOpt, DuplicatedTimeOpt);
\r
644 * Returns amount of local time adjustment used for checking rule transitions
\r
646 private static int getLocalDelta(int rawBefore, int dstBefore, int rawAfter, int dstAfter,
\r
647 int NonExistingTimeOpt, int DuplicatedTimeOpt) {
\r
650 int offsetBefore = rawBefore + dstBefore;
\r
651 int offsetAfter = rawAfter + dstAfter;
\r
653 boolean dstToStd = (dstBefore != 0) && (dstAfter == 0);
\r
654 boolean stdToDst = (dstBefore == 0) && (dstAfter != 0);
\r
656 if (offsetAfter - offsetBefore >= 0) {
\r
657 // Positive transition, which makes a non-existing local time range
\r
658 if (((NonExistingTimeOpt & STD_DST_MASK) == LOCAL_STD && dstToStd)
\r
659 || ((NonExistingTimeOpt & STD_DST_MASK) == LOCAL_DST && stdToDst)) {
\r
660 delta = offsetBefore;
\r
661 } else if (((NonExistingTimeOpt & STD_DST_MASK) == LOCAL_STD && stdToDst)
\r
662 || ((NonExistingTimeOpt & STD_DST_MASK) == LOCAL_DST && dstToStd)) {
\r
663 delta = offsetAfter;
\r
664 } else if ((NonExistingTimeOpt & FORMER_LATTER_MASK) == LOCAL_LATTER) {
\r
665 delta = offsetBefore;
\r
667 // Interprets the time with rule before the transition,
\r
668 // default for non-existing time range
\r
669 delta = offsetAfter;
\r
672 // Negative transition, which makes a duplicated local time range
\r
673 if (((DuplicatedTimeOpt & STD_DST_MASK) == LOCAL_STD && dstToStd)
\r
674 || ((DuplicatedTimeOpt & STD_DST_MASK) == LOCAL_DST && stdToDst)) {
\r
675 delta = offsetAfter;
\r
676 } else if (((DuplicatedTimeOpt & STD_DST_MASK) == LOCAL_STD && stdToDst)
\r
677 || ((DuplicatedTimeOpt & STD_DST_MASK) == LOCAL_DST && dstToStd)) {
\r
678 delta = offsetBefore;
\r
679 } else if ((DuplicatedTimeOpt & FORMER_LATTER_MASK) == LOCAL_FORMER) {
\r
680 delta = offsetBefore;
\r
682 // Interprets the time with rule after the transition,
\r
683 // default for duplicated local time range
\r
684 delta = offsetAfter;
\r