// a computed amount of millis to the current millis. The only
// wrinkle is with DST (and/or a change to the zone's UTC offset, which
// we'll include with DST) -- for some fields, like the DAY_OF_MONTH,
- // we don't want the HOUR to shift due to changes in DST. If the
+ // we don't want the wall time to shift due to changes in DST. If the
// result of the add operation is to move from DST to Standard, or
// vice versa, we need to adjust by an hour forward or back,
- // respectively. For such fields we set keepHourInvariant to true.
+ // respectively. For such fields we set keepWallTimeInvariant to true.
// We only adjust the DST for fields larger than an hour. For
// fields smaller than an hour, we cannot adjust for DST without
// <April 30>, rather than <April 31> => <May 1>.
long delta = amount; // delta in ms
- boolean keepHourInvariant = true;
+ boolean keepWallTimeInvariant = true;
switch (field) {
case ERA:
case HOUR_OF_DAY:
case HOUR:
delta *= ONE_HOUR;
- keepHourInvariant = false;
+ keepWallTimeInvariant = false;
break;
case MINUTE:
delta *= ONE_MINUTE;
- keepHourInvariant = false;
+ keepWallTimeInvariant = false;
break;
case SECOND:
delta *= ONE_SECOND;
- keepHourInvariant = false;
+ keepWallTimeInvariant = false;
break;
case MILLISECOND:
case MILLISECONDS_IN_DAY:
- keepHourInvariant = false;
+ keepWallTimeInvariant = false;
break;
default:
") not supported");
}
- // In order to keep the hour invariant (for fields where this is
+ // In order to keep the wall time invariant (for fields where this is
// appropriate), check the combined DST & ZONE offset before and
// after the add() operation. If it changes, then adjust the millis
// to compensate.
int prevOffset = 0;
- int hour = 0;
- if (keepHourInvariant) {
+ int prevWallTime = 0;
+ if (keepWallTimeInvariant) {
prevOffset = get(DST_OFFSET) + get(ZONE_OFFSET);
- hour = internalGet(HOUR_OF_DAY);
+ prevWallTime = get(MILLISECONDS_IN_DAY);
}
setTimeInMillis(getTimeInMillis() + delta);
- if (keepHourInvariant) {
- int newOffset = get(DST_OFFSET) + get(ZONE_OFFSET);
- if (newOffset != prevOffset) {
- // We have done an hour-invariant adjustment but the
- // combined offset has changed. We adjust millis to keep
- // the hour constant. In cases such as midnight after
- // a DST change which occurs at midnight, there is the
- // danger of adjusting into a different day. To avoid
- // this we make the adjustment only if it actually
- // maintains the hour.
-
- // When the difference of the previous UTC offset and
- // the new UTC offset exceeds 1 full day, we do not want
- // to roll over/back the date. For now, this only happens
- // in Samoa (Pacific/Apia) on Dec 30, 2011. See ticket:9452.
- long adjAmount = (prevOffset - newOffset) % ONE_DAY;
- if (adjAmount != 0) {
- long t = time;
- setTimeInMillis(time + adjAmount);
- if (get(HOUR_OF_DAY) != hour) {
- setTimeInMillis(t);
+ if (keepWallTimeInvariant) {
+ int newWallTime = get(MILLISECONDS_IN_DAY);
+ if (newWallTime != prevWallTime) {
+ // There is at least one zone transition between the base
+ // time and the result time. As the result, wall time has
+ // changed.
+ long t = internalGetTimeInMillis();
+ int newOffset = get(DST_OFFSET) + get(ZONE_OFFSET);
+ if (newOffset != prevOffset) {
+ // When the difference of the previous UTC offset and
+ // the new UTC offset exceeds 1 full day, we do not want
+ // to roll over/back the date. For now, this only happens
+ // in Samoa (Pacific/Apia) on Dec 30, 2011. See ticket:9452.
+ long adjAmount = (prevOffset - newOffset) % ONE_DAY;
+ if (adjAmount != 0) {
+ setTimeInMillis(t + adjAmount);
+ newWallTime = get(MILLISECONDS_IN_DAY);
+ }
+ if (newWallTime != prevWallTime) {
+ // The result wall time or adjusted wall time was shifted because
+ // the target wall time does not exist on the result date.
+ switch (skippedWallTime) {
+ case WALLTIME_FIRST:
+ if (adjAmount > 0) {
+ setTimeInMillis(t);
+ }
+ break;
+ case WALLTIME_LAST:
+ if (adjAmount < 0) {
+ setTimeInMillis(t);
+ }
+ break;
+ case WALLTIME_NEXT_VALID:
+ long tmpT = adjAmount > 0 ? internalGetTimeInMillis() : t;
+ Long immediatePrevTrans = getImmediatePreviousZoneTransition(tmpT);
+ if (immediatePrevTrans != null) {
+ setTimeInMillis(immediatePrevTrans);
+ } else {
+ throw new RuntimeException("Could not locate a time zone transition before " + tmpT);
+ }
+ break;
+ }
}
}
}
* millisecond time value <code>time</code>.
* @stable ICU 2.0
*/
- protected void computeTime() {
+ protected void computeTime() {
if (!isLenient()) {
validateFields();
}
// Adjust time to the next valid wall clock time.
// At this point, tmpTime is on or after the zone offset transition causing
// the skipped time range.
- if (zone instanceof BasicTimeZone) {
- TimeZoneTransition transition = ((BasicTimeZone)zone).getPreviousTransition(tmpTime, true);
- if (transition == null) {
- // Could not find any transitions
- throw new RuntimeException("Could not locate previous zone transition");
- }
- time = transition.getTime();
- } else {
- // Usually, it is enough to check past one hour because such transition is most
- // likely +1 hour shift. However, there is an example jumped +24 hour in the tz database.
- Long transitionT = getPreviousZoneTransitionTime(zone, tmpTime, 2*60*60*1000); // check last 2 hours
- if (transitionT == null) {
- transitionT = getPreviousZoneTransitionTime(zone, tmpTime, 30*60*60*1000); // try last 30 hours
- if (transitionT == null) {
- // Could not find any transitions in last 30 hours...
- throw new RuntimeException("Could not locate previous zone transition within 30 hours from " + tmpTime);
- }
- }
- time = transitionT.longValue();
+ Long immediatePrevTransition = getImmediatePreviousZoneTransition(tmpTime);
+ if (immediatePrevTransition == null) {
+ throw new RuntimeException("Could not locate a time zone transition before " + tmpTime);
}
+ time = immediatePrevTransition;
} else {
time = tmpTime;
}
}
}
+ /**
+ * Find the previous zone transtion near the given time.
+ *
+ * @param base The base time, inclusive.
+ * @return The time of the previous transition, or null if not found.
+ */
+ private Long getImmediatePreviousZoneTransition(long base) {
+ Long transitionTime = null;
+
+ if (zone instanceof BasicTimeZone) {
+ TimeZoneTransition transition = ((BasicTimeZone) zone).getPreviousTransition(base, true);
+ if (transition != null) {
+ transitionTime = transition.getTime();
+ }
+ } else {
+ // Usually, it is enough to check past one hour because such transition is most
+ // likely +1 hour shift. However, there is an example jumped +24 hour in the tz database.
+ transitionTime = getPreviousZoneTransitionTime(zone, base, 2 * 60 * 60 * 1000); // check last 2 hours
+ if (transitionTime == null) {
+ transitionTime = getPreviousZoneTransitionTime(zone, base, 30 * 60 * 60 * 1000); // try last 30 hours
+ }
+ }
+ return transitionTime;
+ }
+
/**
* Find the previous zone transition within the specified duration.
- * Note: This method should not be used when TimeZone is a BasicTimeZone.
- * {@link BasicTimeZone#getPreviousTransition(long, boolean)} is much more efficient.
+ * Note: This method is only used when TimeZone is NOT a BasicTimeZone.
* @param tz The time zone.
* @param base The base time, inclusive.
* @param duration The range of time evaluated.
* @return The time of the previous zone transition, or null if not available.
*/
- private Long getPreviousZoneTransitionTime(TimeZone tz, long base, long duration) {
+ private static Long getPreviousZoneTransitionTime(TimeZone tz, long base, long duration) {
assert duration > 0;
long upper = base;
* @param lower The lower bound, exclusive.
* @return The time of the previous zone transition, or null if not available.
*/
- private Long findPreviousZoneTransitionTime(TimeZone tz, int upperOffset, long upper, long lower) {
+ private static Long findPreviousZoneTransitionTime(TimeZone tz, int upperOffset, long upper, long lower) {
boolean onUnitTime = false;
long mid = 0;
private int hour;
private int min;
private int sec;
+ private int ms;
CalFields(int year, int month, int day, int hour, int min, int sec) {
+ this(year, month, day, hour, min, sec, 0);
+ }
+
+ CalFields(int year, int month, int day, int hour, int min, int sec, int ms) {
this.year = year;
this.month = month;
this.day = day;
this.hour = hour;
this.min = min;
this.sec = sec;
+ this.ms = ms;
}
void setTo(Calendar cal) {
cal.clear();
cal.set(year, month - 1, day, hour, min, sec);
+ cal.set(Calendar.MILLISECOND, ms);
}
public String toString() {
- return String.format("%04d-%02d-%02d %02d:%02d:%02d", year, month, day, hour, min, sec);
+ return String.format("%04d-%02d-%02d %02d:%02d:%02d.%03d", year, month, day, hour, min, sec, ms);
}
public boolean equals(Object other) {
&& day == otr.day
&& hour == otr.hour
&& min == otr.min
- && sec == otr.sec);
+ && sec == otr.sec
+ && ms == otr.ms);
}
return false;
}
+ boolean isEquivalentTo(Calendar cal) {
+ return year == cal.get(Calendar.YEAR)
+ && month == cal.get(Calendar.MONTH) + 1
+ && day == cal.get(Calendar.DAY_OF_MONTH)
+ && hour == cal.get(Calendar.HOUR_OF_DAY)
+ && min == cal.get(Calendar.MINUTE)
+ && sec == cal.get(Calendar.SECOND)
+ && ms == cal.get(Calendar.MILLISECOND);
+ }
+
static CalFields createFrom(Calendar cal) {
int year = cal.get(Calendar.YEAR);
int month = cal.get(Calendar.MONTH) + 1;
}
}
}
+
+ public void TestAddAcrossZoneTransition() {
+ class TestData {
+ String zone;
+ CalFields base;
+ int deltaDays;
+ int skippedWTOpt;
+ CalFields expected;
+
+ TestData(String zone, CalFields base, int deltaDays, int skippedWTOpt, CalFields expected) {
+ this.zone = zone;
+ this.base = base;
+ this.deltaDays = deltaDays;
+ this.skippedWTOpt = skippedWTOpt;
+ this.expected = expected;
+ }
+ }
+
+ TestData[] data = new TestData[] {
+ // Add 1 day, from the date before DST transition
+ new TestData("America/Los_Angeles", new CalFields(2014, 3, 8, 1, 59, 59, 999), 1, Calendar.WALLTIME_FIRST,
+ new CalFields(2014, 3, 9, 1, 59, 59, 999)),
+
+ new TestData("America/Los_Angeles", new CalFields(2014, 3, 8, 1, 59, 59, 999), 1, Calendar.WALLTIME_LAST,
+ new CalFields(2014, 3, 9, 1, 59, 59, 999)),
+
+ new TestData("America/Los_Angeles", new CalFields(2014, 3, 8, 1, 59, 59, 999), 1, Calendar.WALLTIME_NEXT_VALID,
+ new CalFields(2014, 3, 9, 1, 59, 59, 999)),
+
+
+ new TestData("America/Los_Angeles", new CalFields(2014, 3, 8, 2, 0, 0, 0), 1, Calendar.WALLTIME_FIRST,
+ new CalFields(2014, 3, 9, 1, 0, 0, 0)),
+
+ new TestData("America/Los_Angeles", new CalFields(2014, 3, 8, 2, 0, 0, 0), 1, Calendar.WALLTIME_LAST,
+ new CalFields(2014, 3, 9, 3, 0, 0, 0)),
+
+ new TestData("America/Los_Angeles", new CalFields(2014, 3, 8, 2, 0, 0, 0), 1, Calendar.WALLTIME_NEXT_VALID,
+ new CalFields(2014, 3, 9, 3, 0, 0, 0)),
+
+
+ new TestData("America/Los_Angeles", new CalFields(2014, 3, 8, 2, 30, 0, 0), 1, Calendar.WALLTIME_FIRST,
+ new CalFields(2014, 3, 9, 1, 30, 0, 0)),
+
+ new TestData("America/Los_Angeles", new CalFields(2014, 3, 8, 2, 30, 0, 0), 1, Calendar.WALLTIME_LAST,
+ new CalFields(2014, 3, 9, 3, 30, 0, 0)),
+
+ new TestData("America/Los_Angeles", new CalFields(2014, 3, 8, 2, 30, 0, 0), 1, Calendar.WALLTIME_NEXT_VALID,
+ new CalFields(2014, 3, 9, 3, 0, 0, 0)),
+
+
+ new TestData("America/Los_Angeles", new CalFields(2014, 3, 8, 3, 0, 0, 0), 1, Calendar.WALLTIME_FIRST,
+ new CalFields(2014, 3, 9, 3, 0, 0, 0)),
+
+ new TestData("America/Los_Angeles", new CalFields(2014, 3, 8, 3, 0, 0, 0), 1, Calendar.WALLTIME_LAST,
+ new CalFields(2014, 3, 9, 3, 0, 0, 0)),
+
+ new TestData("America/Los_Angeles", new CalFields(2014, 3, 8, 3, 0, 0, 0), 1, Calendar.WALLTIME_NEXT_VALID,
+ new CalFields(2014, 3, 9, 3, 0, 0, 0)),
+
+
+ // Subtract 1 day, from one day after DST transition
+ new TestData("America/Los_Angeles", new CalFields(2014, 3, 10, 1, 59, 59, 999), -1, Calendar.WALLTIME_FIRST,
+ new CalFields(2014, 3, 9, 1, 59, 59, 999)),
+
+ new TestData("America/Los_Angeles", new CalFields(2014, 3, 10, 1, 59, 59, 999), -1, Calendar.WALLTIME_LAST,
+ new CalFields(2014, 3, 9, 1, 59, 59, 999)),
+
+ new TestData("America/Los_Angeles", new CalFields(2014, 3, 10, 1, 59, 59, 999), -1, Calendar.WALLTIME_NEXT_VALID,
+ new CalFields(2014, 3, 9, 1, 59, 59, 999)),
+
+
+ new TestData("America/Los_Angeles", new CalFields(2014, 3, 10, 2, 0, 0, 0), -1, Calendar.WALLTIME_FIRST,
+ new CalFields(2014, 3, 9, 1, 0, 0, 0)),
+
+ new TestData("America/Los_Angeles", new CalFields(2014, 3, 10, 2, 0, 0, 0), -1, Calendar.WALLTIME_LAST,
+ new CalFields(2014, 3, 9, 3, 0, 0, 0)),
+
+ new TestData("America/Los_Angeles", new CalFields(2014, 3, 10, 2, 0, 0, 0), -1, Calendar.WALLTIME_NEXT_VALID,
+ new CalFields(2014, 3, 9, 3, 0, 0, 0)),
+
+
+ new TestData("America/Los_Angeles", new CalFields(2014, 3, 10, 2, 30, 0, 0), -1, Calendar.WALLTIME_FIRST,
+ new CalFields(2014, 3, 9, 1, 30, 0, 0)),
+
+ new TestData("America/Los_Angeles", new CalFields(2014, 3, 10, 2, 30, 0, 0), -1, Calendar.WALLTIME_LAST,
+ new CalFields(2014, 3, 9, 3, 30, 0, 0)),
+
+ new TestData("America/Los_Angeles", new CalFields(2014, 3, 10, 2, 30, 0, 0), -1, Calendar.WALLTIME_NEXT_VALID,
+ new CalFields(2014, 3, 9, 3, 0, 0, 0)),
+
+
+ new TestData("America/Los_Angeles", new CalFields(2014, 3, 10, 3, 0, 0, 0), -1, Calendar.WALLTIME_FIRST,
+ new CalFields(2014, 3, 9, 3, 0, 0, 0)),
+
+ new TestData("America/Los_Angeles", new CalFields(2014, 3, 10, 3, 0, 0, 0), -1, Calendar.WALLTIME_LAST,
+ new CalFields(2014, 3, 9, 3, 0, 0, 0)),
+
+ new TestData("America/Los_Angeles", new CalFields(2014, 3, 10, 3, 0, 0, 0), -1, Calendar.WALLTIME_NEXT_VALID,
+ new CalFields(2014, 3, 9, 3, 0, 0, 0)),
+
+
+ // Test case for ticket#10544
+ new TestData("America/Santiago", new CalFields(2013, 4, 27, 0, 0, 0, 0), 134, Calendar.WALLTIME_FIRST,
+ new CalFields(2013, 9, 7, 23, 0, 0, 0)),
+
+ new TestData("America/Santiago", new CalFields(2013, 4, 27, 0, 0, 0, 0), 134, Calendar.WALLTIME_LAST,
+ new CalFields(2013, 9, 8, 1, 0, 0, 0)),
+
+ new TestData("America/Santiago", new CalFields(2013, 4, 27, 0, 0, 0, 0), 134, Calendar.WALLTIME_NEXT_VALID,
+ new CalFields(2013, 9, 8, 1, 0, 0, 0)),
+
+
+ new TestData("America/Santiago", new CalFields(2013, 4, 27, 0, 30, 0, 0), 134, Calendar.WALLTIME_FIRST,
+ new CalFields(2013, 9, 7, 23, 30, 0, 0)),
+
+ new TestData("America/Santiago", new CalFields(2013, 4, 27, 0, 30, 0, 0), 134, Calendar.WALLTIME_LAST,
+ new CalFields(2013, 9, 8, 1, 30, 0, 0)),
+
+ new TestData("America/Santiago", new CalFields(2013, 4, 27, 0, 30, 0, 0), 134, Calendar.WALLTIME_NEXT_VALID,
+ new CalFields(2013, 9, 8, 1, 0, 0, 0)),
+
+
+ // Extreme transition - Pacific/Apia completely skips 2011-12-30
+ new TestData("Pacific/Apia", new CalFields(2011, 12, 29, 0, 0, 0, 0), 1, Calendar.WALLTIME_FIRST,
+ new CalFields(2011, 12, 31, 0, 0, 0, 0)),
+
+ new TestData("Pacific/Apia", new CalFields(2011, 12, 29, 0, 0, 0, 0), 1, Calendar.WALLTIME_LAST,
+ new CalFields(2011, 12, 31, 0, 0, 0, 0)),
+
+ new TestData("Pacific/Apia", new CalFields(2011, 12, 29, 0, 0, 0, 0), 1, Calendar.WALLTIME_NEXT_VALID,
+ new CalFields(2011, 12, 31, 0, 0, 0, 0)),
+
+
+ new TestData("Pacific/Apia", new CalFields(2011, 12, 31, 12, 0, 0, 0), -1, Calendar.WALLTIME_FIRST,
+ new CalFields(2011, 12, 29, 12, 0, 0, 0)),
+
+ new TestData("Pacific/Apia", new CalFields(2011, 12, 31, 12, 0, 0, 0), -1, Calendar.WALLTIME_LAST,
+ new CalFields(2011, 12, 29, 12, 0, 0, 0)),
+
+ new TestData("Pacific/Apia", new CalFields(2011, 12, 31, 12, 0, 0, 0), -1, Calendar.WALLTIME_NEXT_VALID,
+ new CalFields(2011, 12, 29, 12, 0, 0, 0)),
+
+
+ // 30 minutes DST - Australia/Lord_Howe
+ new TestData("Australia/Lord_Howe", new CalFields(2013, 10, 5, 2, 15, 0, 0), 1, Calendar.WALLTIME_FIRST,
+ new CalFields(2013, 10, 6, 1, 45, 0, 0)),
+
+ new TestData("Australia/Lord_Howe", new CalFields(2013, 10, 5, 2, 15, 0, 0), 1, Calendar.WALLTIME_LAST,
+ new CalFields(2013, 10, 6, 2, 45, 0, 0)),
+
+ new TestData("Australia/Lord_Howe", new CalFields(2013, 10, 5, 2, 15, 0, 0), 1, Calendar.WALLTIME_NEXT_VALID,
+ new CalFields(2013, 10, 6, 2, 30, 0, 0)),
+ };
+
+ Calendar cal = Calendar.getInstance();
+ for (TestData d : data) {
+ cal.setTimeZone(TimeZone.getTimeZone(d.zone));
+ cal.setSkippedWallTimeOption(d.skippedWTOpt);
+ d.base.setTo(cal);
+ cal.add(Calendar.DATE, d.deltaDays);
+
+ if (!d.expected.isEquivalentTo(cal)) {
+ CalFields res = CalFields.createFrom(cal);
+ String optDisp = d.skippedWTOpt == Calendar.WALLTIME_FIRST ? "FIRST" :
+ d.skippedWTOpt == Calendar.WALLTIME_LAST ? "LAST" : "NEXT_VALID";
+ errln("Error: base:" + d.base.toString() + ", tz:" + d.zone
+ + ", delta:" + d.deltaDays + " day(s), opt:" + optDisp
+ + ", result:" + res.toString() + " - expected:" + d.expected.toString());
+ }
+ }
+ }
}