ICU-10544 Fixed some implementation problems in Calendar#add. When adding day or lager field results wall time falls into non-existing time slot created by DST transition, the new implementaion honors the current skipped wall time option.

X-SVN-Rev: 35232
This commit is contained in:
Yoshito Umaoka 2014-02-25 23:51:51 +00:00
parent f7f73fe88d
commit 25d898f0b4
2 changed files with 277 additions and 58 deletions

View file

@ -3064,10 +3064,10 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca
// 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
@ -3082,7 +3082,7 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca
// <April 30>, rather than <April 31> => <May 1>.
long delta = amount; // delta in ms
boolean keepHourInvariant = true;
boolean keepWallTimeInvariant = true;
switch (field) {
case ERA:
@ -3144,22 +3144,22 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca
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:
@ -3167,40 +3167,61 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca
") 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;
}
}
}
}
@ -5047,7 +5068,7 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca
* millisecond time value <code>time</code>.
* @stable ICU 2.0
*/
protected void computeTime() {
protected void computeTime() {
if (!isLenient()) {
validateFields();
}
@ -5126,26 +5147,11 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca
// 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;
}
@ -5155,16 +5161,40 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca
}
}
/**
* 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;
@ -5196,7 +5226,7 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca
* @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;

View file

@ -1097,23 +1097,30 @@ public class IBMCalendarTest extends CalendarTest {
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) {
@ -1124,11 +1131,22 @@ public class IBMCalendarTest extends CalendarTest {
&& 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;
@ -1681,4 +1699,175 @@ public class IBMCalendarTest extends CalendarTest {
}
}
}
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());
}
}
}
}