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

X-SVN-Rev: 35231
This commit is contained in:
Yoshito Umaoka 2014-02-25 23:50:35 +00:00
parent c2ba26e8ba
commit f7f73fe88d
4 changed files with 299 additions and 56 deletions

View file

@ -1,6 +1,6 @@
/*
*******************************************************************************
* Copyright (C) 1997-2013, International Business Machines Corporation and *
* Copyright (C) 1997-2014, International Business Machines Corporation and *
* others. All Rights Reserved. *
*******************************************************************************
*
@ -1894,10 +1894,10 @@ void Calendar::add(UCalendarDateFields field, int32_t amount, UErrorCode& status
// 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
@ -1912,7 +1912,7 @@ void Calendar::add(UCalendarDateFields field, int32_t amount, UErrorCode& status
// <April 30>, rather than <April 31> => <May 1>.
double delta = amount; // delta in ms
UBool keepHourInvariant = TRUE;
UBool keepWallTimeInvariant = TRUE;
switch (field) {
case UCAL_ERA:
@ -1974,22 +1974,22 @@ void Calendar::add(UCalendarDateFields field, int32_t amount, UErrorCode& status
case UCAL_HOUR_OF_DAY:
case UCAL_HOUR:
delta *= kOneHour;
keepHourInvariant = FALSE;
keepWallTimeInvariant = FALSE;
break;
case UCAL_MINUTE:
delta *= kOneMinute;
keepHourInvariant = FALSE;
keepWallTimeInvariant = FALSE;
break;
case UCAL_SECOND:
delta *= kOneSecond;
keepHourInvariant = FALSE;
keepWallTimeInvariant = FALSE;
break;
case UCAL_MILLISECOND:
case UCAL_MILLISECONDS_IN_DAY:
keepHourInvariant = FALSE;
keepWallTimeInvariant = FALSE;
break;
default:
@ -2003,41 +2003,61 @@ void Calendar::add(UCalendarDateFields field, int32_t amount, UErrorCode& status
// ") 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.
int32_t prevOffset = 0;
int32_t hour = 0;
if (keepHourInvariant) {
int32_t prevWallTime = 0;
if (keepWallTimeInvariant) {
prevOffset = get(UCAL_DST_OFFSET, status) + get(UCAL_ZONE_OFFSET, status);
hour = internalGet(UCAL_HOUR_OF_DAY);
prevWallTime = get(UCAL_MILLISECONDS_IN_DAY, status);
}
setTimeInMillis(getTimeInMillis(status) + delta, status);
if (keepHourInvariant) {
int32_t newOffset = get(UCAL_DST_OFFSET, status) + get(UCAL_ZONE_OFFSET, status);
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.
int32_t adjAmount = prevOffset - newOffset;
adjAmount = adjAmount >= 0 ? adjAmount % (int32_t)kOneDay : -(-adjAmount % (int32_t)kOneDay);
if (adjAmount != 0) {
double t = internalGetTime();
setTimeInMillis(t + adjAmount, status);
if (get(UCAL_HOUR_OF_DAY, status) != hour) {
setTimeInMillis(t, status);
if (keepWallTimeInvariant) {
int32_t newWallTime = get(UCAL_MILLISECONDS_IN_DAY, status);
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.
UDate t = internalGetTime();
int32_t newOffset = get(UCAL_DST_OFFSET, status) + get(UCAL_ZONE_OFFSET, status);
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.
int32_t adjAmount = prevOffset - newOffset;
adjAmount = adjAmount >= 0 ? adjAmount % (int32_t)kOneDay : -(-adjAmount % (int32_t)kOneDay);
if (adjAmount != 0) {
setTimeInMillis(t + adjAmount, status);
newWallTime = get(UCAL_MILLISECONDS_IN_DAY, status);
}
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 (fSkippedWallTime) {
case UCAL_WALLTIME_FIRST:
if (adjAmount > 0) {
setTimeInMillis(t, status);
}
break;
case UCAL_WALLTIME_LAST:
if (adjAmount < 0) {
setTimeInMillis(t, status);
}
break;
case UCAL_WALLTIME_NEXT_VALID:
UDate tmpT = adjAmount > 0 ? internalGetTime() : t;
UDate immediatePrevTrans;
UBool hasTransition = getImmediatePreviousZoneTransition(tmpT, &immediatePrevTrans, status);
if (U_SUCCESS(status) && hasTransition) {
setTimeInMillis(immediatePrevTrans, status);
}
break;
}
}
}
}
@ -2818,22 +2838,10 @@ void Calendar::computeTime(UErrorCode& status) {
// 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.
BasicTimeZone *btz = getBasicTimeZone();
if (btz) {
TimeZoneTransition transition;
UBool hasTransition = btz->getPreviousTransition(tmpTime, TRUE, transition);
if (hasTransition) {
t = transition.getTime();
} else {
// Could not find any transitions.
// Note: This should never happen.
status = U_INTERNAL_PROGRAM_ERROR;
}
} else {
// If not BasicTimeZone, return unsupported error for now.
// TODO: We may support non-BasicTimeZone in future.
status = U_UNSUPPORTED_ERROR;
UDate immediatePrevTransition;
UBool hasTransition = getImmediatePreviousZoneTransition(tmpTime, &immediatePrevTransition, status);
if (U_SUCCESS(status) && hasTransition) {
t = immediatePrevTransition;
}
}
} else {
@ -2849,6 +2857,30 @@ void Calendar::computeTime(UErrorCode& status) {
}
}
/**
* Find the previous zone transtion near the given time.
*/
UBool Calendar::getImmediatePreviousZoneTransition(UDate base, UDate *transitionTime, UErrorCode& status) const {
BasicTimeZone *btz = getBasicTimeZone();
if (btz) {
TimeZoneTransition trans;
UBool hasTransition = btz->getPreviousTransition(base, TRUE, trans);
if (hasTransition) {
*transitionTime = trans.getTime();
return TRUE;
} else {
// Could not find any transitions.
// Note: This should never happen.
status = U_INTERNAL_PROGRAM_ERROR;
}
} else {
// If not BasicTimeZone, return unsupported error for now.
// TODO: We may support non-BasicTimeZone in future.
status = U_UNSUPPORTED_ERROR;
}
return FALSE;
}
/**
* Compute the milliseconds in the day from the fields. This is a
* value from 0 to 23:59:59.999 inclusive, unless fields are out of

View file

@ -1,6 +1,6 @@
/*
********************************************************************************
* Copyright (C) 1997-2013, International Business Machines
* Copyright (C) 1997-2014, International Business Machines
* Corporation and others. All Rights Reserved.
********************************************************************************
*
@ -2428,6 +2428,15 @@ private:
* is not an instance of BasicTimeZone.
*/
BasicTimeZone* getBasicTimeZone() const;
/**
* Find the previous zone transtion near the given time.
* @param base The base time, inclusive
* @param transitionTime Receives the result time
* @param status The error status
* @return TRUE if a transition is found.
*/
UBool getImmediatePreviousZoneTransition(UDate base, UDate *transitionTime, UErrorCode& status) const;
};
// -------------------------------------

View file

@ -312,6 +312,13 @@ void CalendarTest::runIndexedTest( int32_t index, UBool exec, const char* &name,
TestWeekData();
}
break;
case 35:
name = "TestAddAcrossZoneTransition";
if(exec) {
logln("TestAddAcrossZoneTransition---"); logln("");
TestAddAcrossZoneTransition();
}
break;
default: name = ""; break;
}
}
@ -2422,12 +2429,13 @@ CalendarTest::TestAmbiguousWallTimeAPIs(void) {
class CalFields {
public:
CalFields(int32_t year, int32_t month, int32_t day, int32_t hour, int32_t min, int32_t sec);
CalFields(int32_t year, int32_t month, int32_t day, int32_t hour, int32_t min, int32_t sec, int32_t ms = 0);
CalFields(const Calendar& cal, UErrorCode& status);
void setTo(Calendar& cal) const;
char* toString(char* buf, int32_t len) const;
UBool operator==(const CalFields& rhs) const;
UBool operator!=(const CalFields& rhs) const;
UBool isEquivalentTo(const Calendar& cal, UErrorCode& status) const;
private:
int32_t year;
@ -2436,10 +2444,11 @@ private:
int32_t hour;
int32_t min;
int32_t sec;
int32_t ms;
};
CalFields::CalFields(int32_t year, int32_t month, int32_t day, int32_t hour, int32_t min, int32_t sec)
: year(year), month(month), day(day), hour(hour), min(min), sec(sec) {
CalFields::CalFields(int32_t year, int32_t month, int32_t day, int32_t hour, int32_t min, int32_t sec, int32_t ms)
: year(year), month(month), day(day), hour(hour), min(min), sec(sec), ms(ms) {
}
CalFields::CalFields(const Calendar& cal, UErrorCode& status) {
@ -2449,18 +2458,20 @@ CalFields::CalFields(const Calendar& cal, UErrorCode& status) {
hour = cal.get(UCAL_HOUR_OF_DAY, status);
min = cal.get(UCAL_MINUTE, status);
sec = cal.get(UCAL_SECOND, status);
ms = cal.get(UCAL_MILLISECOND, status);
}
void
CalFields::setTo(Calendar& cal) const {
cal.clear();
cal.set(year, month - 1, day, hour, min, sec);
cal.set(UCAL_MILLISECOND, ms);
}
char*
CalFields::toString(char* buf, int32_t len) const {
char local[32];
sprintf(local, "%04d-%02d-%02d %02d:%02d:%02d", year, month, day, hour, min, sec);
sprintf(local, "%04d-%02d-%02d %02d:%02d:%02d.%03d", year, month, day, hour, min, sec, ms);
uprv_strncpy(buf, local, len - 1);
buf[len - 1] = 0;
return buf;
@ -2473,7 +2484,8 @@ CalFields::operator==(const CalFields& rhs) const {
&& day == rhs.day
&& hour == rhs.hour
&& min == rhs.min
&& sec == rhs.sec;
&& sec == rhs.sec
&& ms == rhs.ms;
}
UBool
@ -2481,6 +2493,17 @@ CalFields::operator!=(const CalFields& rhs) const {
return !(*this == rhs);
}
UBool
CalFields::isEquivalentTo(const Calendar& cal, UErrorCode& status) const {
return year == cal.get(UCAL_YEAR, status)
&& month == cal.get(UCAL_MONTH, status) + 1
&& day == cal.get(UCAL_DAY_OF_MONTH, status)
&& hour == cal.get(UCAL_HOUR_OF_DAY, status)
&& min == cal.get(UCAL_MINUTE, status)
&& sec == cal.get(UCAL_SECOND, status)
&& ms == cal.get(UCAL_MILLISECOND, status);
}
typedef struct {
const char* tzid;
const CalFields in;
@ -2969,6 +2992,183 @@ void CalendarTest::TestWeekData() {
}
}
typedef struct {
const char* zone;
const CalFields base;
int32_t deltaDays;
UCalendarWallTimeOption skippedWTOpt;
const CalFields expected;
} TestAddAcrossZoneTransitionData;
static const TestAddAcrossZoneTransitionData AAZTDATA[] =
{
// Time zone Base wall time day(s) Skipped time options
// Expected wall time
// Add 1 day, from the date before DST transition
{"America/Los_Angeles", CalFields(2014,3,8,1,59,59,999), 1, UCAL_WALLTIME_FIRST,
CalFields(2014,3,9,1,59,59,999)},
{"America/Los_Angeles", CalFields(2014,3,8,1,59,59,999), 1, UCAL_WALLTIME_LAST,
CalFields(2014,3,9,1,59,59,999)},
{"America/Los_Angeles", CalFields(2014,3,8,1,59,59,999), 1, UCAL_WALLTIME_NEXT_VALID,
CalFields(2014,3,9,1,59,59,999)},
{"America/Los_Angeles", CalFields(2014,3,8,2,0,0,0), 1, UCAL_WALLTIME_FIRST,
CalFields(2014,3,9,1,0,0,0)},
{"America/Los_Angeles", CalFields(2014,3,8,2,0,0,0), 1, UCAL_WALLTIME_LAST,
CalFields(2014,3,9,3,0,0,0)},
{"America/Los_Angeles", CalFields(2014,3,8,2,0,0,0), 1, UCAL_WALLTIME_NEXT_VALID,
CalFields(2014,3,9,3,0,0,0)},
{"America/Los_Angeles", CalFields(2014,3,8,2,30,0,0), 1, UCAL_WALLTIME_FIRST,
CalFields(2014,3,9,1,30,0,0)},
{"America/Los_Angeles", CalFields(2014,3,8,2,30,0,0), 1, UCAL_WALLTIME_LAST,
CalFields(2014,3,9,3,30,0,0)},
{"America/Los_Angeles", CalFields(2014,3,8,2,30,0,0), 1, UCAL_WALLTIME_NEXT_VALID,
CalFields(2014,3,9,3,0,0,0)},
{"America/Los_Angeles", CalFields(2014,3,8,3,0,0,0), 1, UCAL_WALLTIME_FIRST,
CalFields(2014,3,9,3,0,0,0)},
{"America/Los_Angeles", CalFields(2014,3,8,3,0,0,0), 1, UCAL_WALLTIME_LAST,
CalFields(2014,3,9,3,0,0,0)},
{"America/Los_Angeles", CalFields(2014,3,8,3,0,0,0), 1, UCAL_WALLTIME_NEXT_VALID,
CalFields(2014,3,9,3,0,0,0)},
// Subtract 1 day, from one day after DST transition
{"America/Los_Angeles", CalFields(2014,3,10,1,59,59,999), -1, UCAL_WALLTIME_FIRST,
CalFields(2014,3,9,1,59,59,999)},
{"America/Los_Angeles", CalFields(2014,3,10,1,59,59,999), -1, UCAL_WALLTIME_LAST,
CalFields(2014,3,9,1,59,59,999)},
{"America/Los_Angeles", CalFields(2014,3,10,1,59,59,999), -1, UCAL_WALLTIME_NEXT_VALID,
CalFields(2014,3,9,1,59,59,999)},
{"America/Los_Angeles", CalFields(2014,3,10,2,0,0,0), -1, UCAL_WALLTIME_FIRST,
CalFields(2014,3,9,1,0,0,0)},
{"America/Los_Angeles", CalFields(2014,3,10,2,0,0,0), -1, UCAL_WALLTIME_LAST,
CalFields(2014,3,9,3,0,0,0)},
{"America/Los_Angeles", CalFields(2014,3,10,2,0,0,0), -1, UCAL_WALLTIME_NEXT_VALID,
CalFields(2014,3,9,3,0,0,0)},
{"America/Los_Angeles", CalFields(2014,3,10,2,30,0,0), -1, UCAL_WALLTIME_FIRST,
CalFields(2014,3,9,1,30,0,0)},
{"America/Los_Angeles", CalFields(2014,3,10,2,30,0,0), -1, UCAL_WALLTIME_LAST,
CalFields(2014,3,9,3,30,0,0)},
{"America/Los_Angeles", CalFields(2014,3,10,2,30,0,0), -1, UCAL_WALLTIME_NEXT_VALID,
CalFields(2014,3,9,3,0,0,0)},
{"America/Los_Angeles", CalFields(2014,3,10,3,0,0,0), -1, UCAL_WALLTIME_FIRST,
CalFields(2014,3,9,3,0,0,0)},
{"America/Los_Angeles", CalFields(2014,3,10,3,0,0,0), -1, UCAL_WALLTIME_LAST,
CalFields(2014,3,9,3,0,0,0)},
{"America/Los_Angeles", CalFields(2014,3,10,3,0,0,0), -1, UCAL_WALLTIME_NEXT_VALID,
CalFields(2014,3,9,3,0,0,0)},
// Test case for ticket#10544
{"America/Santiago", CalFields(2013,4,27,0,0,0,0), 134, UCAL_WALLTIME_FIRST,
CalFields(2013,9,7,23,0,0,0)},
{"America/Santiago", CalFields(2013,4,27,0,0,0,0), 134, UCAL_WALLTIME_LAST,
CalFields(2013,9,8,1,0,0,0)},
{"America/Santiago", CalFields(2013,4,27,0,0,0,0), 134, UCAL_WALLTIME_NEXT_VALID,
CalFields(2013,9,8,1,0,0,0)},
{"America/Santiago", CalFields(2013,4,27,0,30,0,0), 134, UCAL_WALLTIME_FIRST,
CalFields(2013,9,7,23,30,0,0)},
{"America/Santiago", CalFields(2013,4,27,0,30,0,0), 134, UCAL_WALLTIME_LAST,
CalFields(2013,9,8,1,30,0,0)},
{"America/Santiago", CalFields(2013,4,27,0,30,0,0), 134, UCAL_WALLTIME_NEXT_VALID,
CalFields(2013,9,8,1,0,0,0)},
// Extreme transition - Pacific/Apia completely skips 2011-12-30
{"Pacific/Apia", CalFields(2011,12,29,0,0,0,0), 1, UCAL_WALLTIME_FIRST,
CalFields(2011,12,31,0,0,0,0)},
{"Pacific/Apia", CalFields(2011,12,29,0,0,0,0), 1, UCAL_WALLTIME_LAST,
CalFields(2011,12,31,0,0,0,0)},
{"Pacific/Apia", CalFields(2011,12,29,0,0,0,0), 1, UCAL_WALLTIME_NEXT_VALID,
CalFields(2011,12,31,0,0,0,0)},
{"Pacific/Apia", CalFields(2011,12,31,12,0,0,0), -1, UCAL_WALLTIME_FIRST,
CalFields(2011,12,29,12,0,0,0)},
{"Pacific/Apia", CalFields(2011,12,31,12,0,0,0), -1, UCAL_WALLTIME_LAST,
CalFields(2011,12,29,12,0,0,0)},
{"Pacific/Apia", CalFields(2011,12,31,12,0,0,0), -1, UCAL_WALLTIME_NEXT_VALID,
CalFields(2011,12,29,12,0,0,0)},
// 30 minutes DST - Australia/Lord_Howe
{"Australia/Lord_Howe", CalFields(2013,10,5,2,15,0,0), 1, UCAL_WALLTIME_FIRST,
CalFields(2013,10,6,1,45,0,0)},
{"Australia/Lord_Howe", CalFields(2013,10,5,2,15,0,0), 1, UCAL_WALLTIME_LAST,
CalFields(2013,10,6,2,45,0,0)},
{"Australia/Lord_Howe", CalFields(2013,10,5,2,15,0,0), 1, UCAL_WALLTIME_NEXT_VALID,
CalFields(2013,10,6,2,30,0,0)},
{NULL, CalFields(0,0,0,0,0,0,0), 0, UCAL_WALLTIME_LAST, CalFields(0,0,0,0,0,0,0)}
};
void CalendarTest::TestAddAcrossZoneTransition() {
UErrorCode status = U_ZERO_ERROR;
GregorianCalendar cal(status);
TEST_CHECK_STATUS;
for (int32_t i = 0; AAZTDATA[i].zone; i++) {
status = U_ZERO_ERROR;
TimeZone *tz = TimeZone::createTimeZone(AAZTDATA[i].zone);
cal.adoptTimeZone(tz);
cal.setSkippedWallTimeOption(AAZTDATA[i].skippedWTOpt);
AAZTDATA[i].base.setTo(cal);
cal.add(UCAL_DATE, AAZTDATA[i].deltaDays, status);
TEST_CHECK_STATUS;
if (!AAZTDATA[i].expected.isEquivalentTo(cal, status)) {
CalFields res(cal, status);
TEST_CHECK_STATUS;
char buf[32];
const char *optDisp = AAZTDATA[i].skippedWTOpt == UCAL_WALLTIME_FIRST ? "FIRST" :
AAZTDATA[i].skippedWTOpt == UCAL_WALLTIME_LAST ? "LAST" : "NEXT_VALID";
errln(UnicodeString("Error: base:") + AAZTDATA[i].base.toString(buf, sizeof(buf)) + ", tz:" + AAZTDATA[i].zone
+ ", delta:" + AAZTDATA[i].deltaDays + " day(s), opt:" + optDisp
+ ", result:" + res.toString(buf, sizeof(buf))
+ " - expected:" + AAZTDATA[i].expected.toString(buf, sizeof(buf)));
}
}
}
#endif /* #if !UCONFIG_NO_FORMATTING */
//eof

View file

@ -249,6 +249,8 @@ public: // package
void setAndTestWholeYear(Calendar* cal, int32_t startYear, UErrorCode& status);
void TestWeekData(void);
void TestAddAcrossZoneTransition(void);
};
#endif /* #if !UCONFIG_NO_FORMATTING */