diff --git a/icu4c/source/i18n/calendar.cpp b/icu4c/source/i18n/calendar.cpp index 27825c04a2c..9e7a3cc86d2 100644 --- a/icu4c/source/i18n/calendar.cpp +++ b/icu4c/source/i18n/calendar.cpp @@ -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 // , rather than => . 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 diff --git a/icu4c/source/i18n/unicode/calendar.h b/icu4c/source/i18n/unicode/calendar.h index b861e1b2052..83d9140bf4c 100644 --- a/icu4c/source/i18n/unicode/calendar.h +++ b/icu4c/source/i18n/unicode/calendar.h @@ -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; }; // ------------------------------------- diff --git a/icu4c/source/test/intltest/caltest.cpp b/icu4c/source/test/intltest/caltest.cpp index c6849acf7e3..c7647d57b0c 100644 --- a/icu4c/source/test/intltest/caltest.cpp +++ b/icu4c/source/test/intltest/caltest.cpp @@ -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 diff --git a/icu4c/source/test/intltest/caltest.h b/icu4c/source/test/intltest/caltest.h index 1c428ef3604..0c9811657bd 100644 --- a/icu4c/source/test/intltest/caltest.h +++ b/icu4c/source/test/intltest/caltest.h @@ -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 */