From 9b20bed1a3cc1cf17fce80a143c91ffbaf3a4b4d Mon Sep 17 00:00:00 2001 From: Alan Liu Date: Fri, 9 May 2003 19:13:59 +0000 Subject: [PATCH] ICU-2792 fix anomalous behavior when adding across a DST boundary X-SVN-Rev: 11866 --- icu4c/source/i18n/gregocal.cpp | 41 ++++++++++----- icu4c/source/test/intltest/calregts.cpp | 67 +++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 12 deletions(-) diff --git a/icu4c/source/i18n/gregocal.cpp b/icu4c/source/i18n/gregocal.cpp index 2ab29d4c47a..249496be3cb 100644 --- a/icu4c/source/i18n/gregocal.cpp +++ b/icu4c/source/i18n/gregocal.cpp @@ -1365,7 +1365,7 @@ GregorianCalendar::aggregateStamp(int32_t stamp_a, int32_t stamp_b) // ------------------------------------- void GregorianCalendar::add(EDateFields field, int32_t amount, UErrorCode& status) { - add((UCalendarDateFields) field, amount, status); + add((UCalendarDateFields) field, amount, status); } void @@ -1482,19 +1482,35 @@ GregorianCalendar::add(UCalendarDateFields field, int32_t amount, UErrorCode& st return; } - // Save the current DST state. + // In order to keep the hour invariant (for fields where this is + // appropriate), record the DST_OFFSET before and after the add() + // operation. If it has changed, then adjust the millis to + // compensate. int32_t dst = 0; - if (adjustDST) - dst = internalGet(UCAL_DST_OFFSET); + int32_t hour = 0; + if (adjustDST) { + dst = get(UCAL_DST_OFFSET, status); + hour = internalGet(UCAL_HOUR_OF_DAY); + } - setTimeInMillis(internalGetTime() + delta, status); // Automatically computes fields if necessary + setTimeInMillis(internalGetTime() + delta, status); if (adjustDST) { - // Now do the DST adjustment alluded to above. - // Only call setTimeInMillis if necessary, because it's an expensive call. - dst -= internalGet(UCAL_DST_OFFSET); - if(dst!= 0) - setTimeInMillis(internalGetTime() + dst, status); + dst -= get(UCAL_DST_OFFSET, status); + if (dst != 0) { + // We have done an hour-invariant adjustment but the + // DST offset has altered. 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. + UDate t = internalGetTime(); + setTimeInMillis(t + dst, status); + if (get(UCAL_HOUR_OF_DAY, status) != hour) { + setTimeInMillis(t, status); + } + } } } } @@ -1508,7 +1524,7 @@ GregorianCalendar::add(UCalendarDateFields field, int32_t amount, UErrorCode& st void GregorianCalendar::roll(EDateFields field, int32_t amount, UErrorCode& status) { - roll((UCalendarDateFields) field, amount, status); + roll((UCalendarDateFields) field, amount, status); } void @@ -1856,7 +1872,7 @@ GregorianCalendar::roll(UCalendarDateFields field, int32_t amount, UErrorCode& s // ------------------------------------- int32_t GregorianCalendar::getMinimum(EDateFields field) const { - return getMinimum((UCalendarDateFields) field); + return getMinimum((UCalendarDateFields) field); } int32_t @@ -2048,3 +2064,4 @@ U_NAMESPACE_END #endif /* #if !UCONFIG_NO_FORMATTING */ //eof + diff --git a/icu4c/source/test/intltest/calregts.cpp b/icu4c/source/test/intltest/calregts.cpp index 6843cf3ea76..442a4d8df95 100644 --- a/icu4c/source/test/intltest/calregts.cpp +++ b/icu4c/source/test/intltest/calregts.cpp @@ -78,6 +78,7 @@ CalendarRegressionTest::runIndexedTest( int32_t index, UBool exec, const char* & CASE(40,test4059654); CASE(41,test4092362); CASE(42,TestWeekShift); + CASE(43,TestTimeZoneTransitionAdd); default: name = ""; break; } } @@ -2161,6 +2162,72 @@ void CalendarRegressionTest::TestWeekShift() { } } +/** + * Make sure that when adding a day, we actually wind up in a + * different day. The DST adjustments we use to keep the hour + * constant across DST changes can backfire and change the day. + */ +void CalendarRegressionTest::TestTimeZoneTransitionAdd() { + UErrorCode ec = U_ZERO_ERROR; + Locale locale(Locale::getUS()); // could also be CHINA + SimpleDateFormat dateFormat("MM/dd/yyyy HH:mm z", locale, ec); + + StringEnumeration *tz = TimeZone::createEnumeration(); + if (tz == NULL) { + errln("FAIL: TimeZone::createEnumeration"); + return; + } + + UnicodeString buf1, buf2; + + const UChar* id; + while ((id = tz->unext(NULL, ec)) != NULL && U_SUCCESS(ec)) { + if (U_FAILURE(ec)) { + errln("FAIL: StringEnumeration::unext"); + break; + } + + TimeZone *t = TimeZone::createTimeZone(id); + if (t == NULL) { + errln("FAIL: TimeZone::createTimeZone"); + break; + } + dateFormat.setTimeZone(*t); + + Calendar *cal = Calendar::createInstance(t, locale, ec); + if (cal == NULL || U_FAILURE(ec)) { + errln("FAIL: Calendar::createTimeZone"); + delete cal; + break; + } + + cal->clear(); + // Scan the year 2003, overlapping the edges of the year + cal->set(UCAL_YEAR, 2002); + cal->set(UCAL_MONTH, UCAL_DECEMBER); + cal->set(UCAL_DATE, 25); + + for (int32_t i=0; i<365+10 && U_SUCCESS(ec); ++i) { + UDate yesterday = cal->getTime(ec); + int32_t yesterday_day = cal->get(UCAL_DATE, ec); + cal->add(UCAL_DATE, 1, ec); + if (yesterday_day == cal->get(UCAL_DATE, ec)) { + errln(UnicodeString(id) + " " + + dateFormat.format(yesterday, buf1) + " +1d= " + + dateFormat.format(cal->getTime(ec), buf2)); + buf1.truncate(0); + buf2.truncate(0); + } + } + } + + if (U_FAILURE(ec)) { + errln("FAIL: %s", u_errorName(ec)); + } + + delete tz; +} + UDate CalendarRegressionTest::makeDate(int32_t y, int32_t m, int32_t d, int32_t hr, int32_t min, int32_t sec)