/*
*******************************************************************************
-* Copyright (C) 1997-2013, International Business Machines Corporation and *
+* Copyright (C) 1997-2014, International Business Machines Corporation and *
* others. All Rights Reserved. *
*******************************************************************************
*
// 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>.
double delta = amount; // delta in ms
- UBool keepHourInvariant = TRUE;
+ UBool keepWallTimeInvariant = TRUE;
switch (field) {
case UCAL_ERA:
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:
// ") 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;
+ }
}
}
}
// 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 {
}
}
+/**
+ * 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
TestWeekData();
}
break;
+ case 35:
+ name = "TestAddAcrossZoneTransition";
+ if(exec) {
+ logln("TestAddAcrossZoneTransition---"); logln("");
+ TestAddAcrossZoneTransition();
+ }
+ break;
default: name = ""; break;
}
}
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;
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) {
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;
&& day == rhs.day
&& hour == rhs.hour
&& min == rhs.min
- && sec == rhs.sec;
+ && sec == rhs.sec
+ && ms == rhs.ms;
}
UBool
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;
}
}
+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