From c63be1f302d3862ebc968c4a7c873430445db153 Mon Sep 17 00:00:00 2001 From: Peter Edberg Date: Tue, 19 Jun 2012 05:42:52 +0000 Subject: [PATCH] ICU-9226 Calendar add +year should always move forward in time; roll year should wrap for eras with real bounds, pin otherwise X-SVN-Rev: 31966 --- icu4c/source/i18n/calendar.cpp | 58 ++++- icu4c/source/test/cintltst/ccaltst.c | 303 +++++++++++++++++++++++---- 2 files changed, 318 insertions(+), 43 deletions(-) diff --git a/icu4c/source/i18n/calendar.cpp b/icu4c/source/i18n/calendar.cpp index 225ca2a5b9..41671de565 100644 --- a/icu4c/source/i18n/calendar.cpp +++ b/icu4c/source/i18n/calendar.cpp @@ -1600,6 +1600,45 @@ void Calendar::roll(UCalendarDateFields field, int32_t amount, UErrorCode& statu case UCAL_YEAR: case UCAL_YEAR_WOY: + { + // * If era==0 and years go backwards in time, change sign of amount. + // * Until we have new API per #9393, we temporarily hardcode knowledge of + // which calendars have era 0 years that go backwards. + UBool era0WithYearsThatGoBackwards = FALSE; + int32_t era = get(UCAL_ERA, status); + if (era == 0) { + const char * calType = getType(); + if ( uprv_strcmp(calType,"gregorian")==0 || uprv_strcmp(calType,"roc")==0 || uprv_strcmp(calType,"coptic")==0 ) { + amount = -amount; + era0WithYearsThatGoBackwards = TRUE; + } + } + int32_t newYear = internalGet(field) + amount; + if (era > 0 || newYear >= 1) { + int32_t maxYear = getActualMaximum(field, status); + if (maxYear < 32768) { + // this era has real bounds, roll should wrap years + if (newYear < 1) { + newYear = maxYear - ((-newYear) % maxYear); + } else if (newYear > maxYear) { + newYear = ((newYear - 1) % maxYear) + 1; + } + // else era is unbounded, just pin low year instead of wrapping + } else if (newYear < 1) { + newYear = 1; + } + // else we are in era 0 with newYear < 1; + // calendars with years that go backwards must pin the year value at 0, + // other calendars can have years < 0 in era 0 + } else if (era0WithYearsThatGoBackwards) { + newYear = 1; + } + set(field, newYear); + pinField(UCAL_MONTH,status); + pinField(UCAL_DAY_OF_MONTH,status); + return; + } + case UCAL_EXTENDED_YEAR: // Rolling the year can involve pinning the DAY_OF_MONTH. set(field, internalGet(field) + amount); @@ -1861,8 +1900,25 @@ void Calendar::add(UCalendarDateFields field, int32_t amount, UErrorCode& status return; case UCAL_YEAR: - case UCAL_EXTENDED_YEAR: case UCAL_YEAR_WOY: + { + // * If era=0 and years go backwards in time, change sign of amount. + // * Until we have new API per #9393, we temporarily hardcode knowledge of + // which calendars have era 0 years that go backwards. + // * Note that for UCAL_YEAR (but not UCAL_YEAR_WOY) we could instead handle + // this by applying the amount to the UCAL_EXTENDED_YEAR field; but since + // we would still need to handle UCAL_YEAR_WOY as below, might as well + // also handle UCAL_YEAR the same way. + int32_t era = get(UCAL_ERA, status); + if (era == 0) { + const char * calType = getType(); + if ( uprv_strcmp(calType,"gregorian")==0 || uprv_strcmp(calType,"roc")==0 || uprv_strcmp(calType,"coptic")==0 ) { + amount = -amount; + } + } + } + // Fall through into normal handling + case UCAL_EXTENDED_YEAR: case UCAL_MONTH: { UBool oldLenient = isLenient(); diff --git a/icu4c/source/test/cintltst/ccaltst.c b/icu4c/source/test/cintltst/ccaltst.c index 7f7f0aa833..66e1e1f0bb 100644 --- a/icu4c/source/test/cintltst/ccaltst.c +++ b/icu4c/source/test/cintltst/ccaltst.c @@ -32,6 +32,7 @@ void TestGregorianChange(void); void TestFieldDifference(void); +void TestAddRollEra0AndEraBounds(void); void addCalTest(TestNode** root); @@ -50,6 +51,7 @@ void addCalTest(TestNode** root) addTest(root, &TestWeekend, "tsformat/ccaltst/TestWeekend"); addTest(root, &TestFieldDifference, "tsformat/ccaltst/TestFieldDifference"); addTest(root, &TestAmbiguousWallTime, "tsformat/ccaltst/TestAmbiguousWallTime"); + addTest(root, &TestAddRollEra0AndEraBounds, "tsformat/ccaltst/TestAddRollEra0AndEraBounds"); } /* "GMT" */ @@ -1655,46 +1657,46 @@ static void TestWeekend() { log_data_err("Unable to create UDateFormat - %s\n", u_errorName(fmtStatus)); return; } - for (count = sizeof(testDates)/sizeof(testDates[0]); count-- > 0; ++testDatesPtr) { + for (count = sizeof(testDates)/sizeof(testDates[0]); count-- > 0; ++testDatesPtr) { UErrorCode status = U_ZERO_ERROR; - UCalendar * cal = ucal_open(NULL, 0, testDatesPtr->locale, UCAL_GREGORIAN, &status); - log_verbose("locale: %s\n", testDatesPtr->locale); - if (U_SUCCESS(status)) { - const TestWeekendDates * weekendDatesPtr = testDatesPtr->dates; - for (subCount = testDatesPtr->numDates; subCount--; ++weekendDatesPtr) { - UDate dateToTest; - UBool isWeekend; - char fmtDateBytes[kFormattedDateMax] = ""; /* initialize for failure */ + UCalendar * cal = ucal_open(NULL, 0, testDatesPtr->locale, UCAL_GREGORIAN, &status); + log_verbose("locale: %s\n", testDatesPtr->locale); + if (U_SUCCESS(status)) { + const TestWeekendDates * weekendDatesPtr = testDatesPtr->dates; + for (subCount = testDatesPtr->numDates; subCount--; ++weekendDatesPtr) { + UDate dateToTest; + UBool isWeekend; + char fmtDateBytes[kFormattedDateMax] = ""; /* initialize for failure */ - ucal_clear(cal); - ucal_setDateTime(cal, weekendDatesPtr->year, weekendDatesPtr->month, weekendDatesPtr->day, - weekendDatesPtr->hour, 0, 0, &status); - dateToTest = ucal_getMillis(cal, &status) + weekendDatesPtr->millisecOffset; - isWeekend = ucal_isWeekend(cal, dateToTest, &status); - if (U_SUCCESS(fmtStatus)) { - UChar fmtDate[kFormattedDateMax]; - (void)udat_format(fmt, dateToTest, fmtDate, kFormattedDateMax, NULL, &fmtStatus); - if (U_SUCCESS(fmtStatus)) { - u_austrncpy(fmtDateBytes, fmtDate, kFormattedDateMax); - fmtDateBytes[kFormattedDateMax-1] = 0; - } else { - fmtStatus = U_ZERO_ERROR; - } - } - if ( U_FAILURE(status) ) { - log_err("FAIL: locale %s date %s isWeekend() status %s\n", testDatesPtr->locale, fmtDateBytes, u_errorName(status) ); - status = U_ZERO_ERROR; - } else if ( (isWeekend!=0) != (weekendDatesPtr->isWeekend!=0) ) { - log_err("FAIL: locale %s date %s isWeekend %d, expected the opposite\n", testDatesPtr->locale, fmtDateBytes, isWeekend ); - } else { - log_verbose("OK: locale %s date %s isWeekend %d\n", testDatesPtr->locale, fmtDateBytes, isWeekend ); - } - } - ucal_close(cal); - } else { - log_data_err("FAIL: ucal_open for locale %s failed: %s - (Are you missing data?)\n", testDatesPtr->locale, u_errorName(status) ); - } - } + ucal_clear(cal); + ucal_setDateTime(cal, weekendDatesPtr->year, weekendDatesPtr->month, weekendDatesPtr->day, + weekendDatesPtr->hour, 0, 0, &status); + dateToTest = ucal_getMillis(cal, &status) + weekendDatesPtr->millisecOffset; + isWeekend = ucal_isWeekend(cal, dateToTest, &status); + if (U_SUCCESS(fmtStatus)) { + UChar fmtDate[kFormattedDateMax]; + (void)udat_format(fmt, dateToTest, fmtDate, kFormattedDateMax, NULL, &fmtStatus); + if (U_SUCCESS(fmtStatus)) { + u_austrncpy(fmtDateBytes, fmtDate, kFormattedDateMax); + fmtDateBytes[kFormattedDateMax-1] = 0; + } else { + fmtStatus = U_ZERO_ERROR; + } + } + if ( U_FAILURE(status) ) { + log_err("FAIL: locale %s date %s isWeekend() status %s\n", testDatesPtr->locale, fmtDateBytes, u_errorName(status) ); + status = U_ZERO_ERROR; + } else if ( (isWeekend!=0) != (weekendDatesPtr->isWeekend!=0) ) { + log_err("FAIL: locale %s date %s isWeekend %d, expected the opposite\n", testDatesPtr->locale, fmtDateBytes, isWeekend ); + } else { + log_verbose("OK: locale %s date %s isWeekend %d\n", testDatesPtr->locale, fmtDateBytes, isWeekend ); + } + } + ucal_close(cal); + } else { + log_data_err("FAIL: ucal_open for locale %s failed: %s - (Are you missing data?)\n", testDatesPtr->locale, u_errorName(status) ); + } + } if (U_SUCCESS(fmtStatus)) { udat_close(fmt); } @@ -1712,12 +1714,12 @@ static void TestWeekend() { transition = ucal_getWeekendTransition(cal, daysOfWeekPtr->dayOfWeek, &status); } if ( U_FAILURE(status) ) { - log_err("FAIL: locale %s DOW %d getDayOfWeekType() status %s\n", testDaysPtr->locale, daysOfWeekPtr->dayOfWeek, u_errorName(status) ); - status = U_ZERO_ERROR; + log_err("FAIL: locale %s DOW %d getDayOfWeekType() status %s\n", testDaysPtr->locale, daysOfWeekPtr->dayOfWeek, u_errorName(status) ); + status = U_ZERO_ERROR; } else if ( dayType != daysOfWeekPtr->dayType || transition != daysOfWeekPtr->transition ) { - log_err("FAIL: locale %s DOW %d type %d, expected %d\n", testDaysPtr->locale, daysOfWeekPtr->dayOfWeek, dayType, daysOfWeekPtr->dayType ); + log_err("FAIL: locale %s DOW %d type %d, expected %d\n", testDaysPtr->locale, daysOfWeekPtr->dayOfWeek, dayType, daysOfWeekPtr->dayType ); } else { - log_verbose("OK: locale %s DOW %d type %d\n", testDaysPtr->locale, daysOfWeekPtr->dayOfWeek, dayType ); + log_verbose("OK: locale %s DOW %d type %d\n", testDaysPtr->locale, daysOfWeekPtr->dayOfWeek, dayType ); } } ucal_close(cal); @@ -1937,4 +1939,221 @@ void TestAmbiguousWallTime() { ucal_close(ucal); } +/** + * TestAddRollEra0AndEraBounds, for #9226 + */ + + typedef struct { + const char * locale; + UBool era0YearsGoBackwards; /* until we have API to get this, per #9393 */ + } EraTestItem; + +static const EraTestItem eraTestItems[] = { + /* calendars with non-modern era 0 that goes backwards, max era == 1 */ + { "en@calendar=gregorian", TRUE }, + { "en@calendar=roc", TRUE }, + { "en@calendar=coptic", TRUE }, + /* calendars with non-modern era 0 that goes forwards, max era > 1 */ + { "en@calendar=japanese", FALSE }, + { "en@calendar=chinese", FALSE }, + /* calendars with non-modern era 0 that goes forwards, max era == 1 */ + { "en@calendar=ethiopic", FALSE }, + /* calendars with only one era = 0, forwards */ + { "en@calendar=buddhist", FALSE }, + { "en@calendar=hebrew", FALSE }, + { "en@calendar=islamic", FALSE }, + { "en@calendar=indian", FALSE }, + { "en@calendar=persian", FALSE }, + { "en@calendar=ethiopic-amete-alem", FALSE }, + { NULL, FALSE } +}; + +static const UChar zoneGMT[] = { 0x47,0x4D,0x54,0 }; + +void TestAddRollEra0AndEraBounds() { + const EraTestItem * eraTestItemPtr; + for (eraTestItemPtr = eraTestItems; eraTestItemPtr->locale != NULL; eraTestItemPtr++) { + UErrorCode status = U_ZERO_ERROR; + UCalendar *ucalTest = ucal_open(zoneGMT, -1, eraTestItemPtr->locale, UCAL_DEFAULT, &status); + if ( U_SUCCESS(status) ) { + int32_t yrBefore, yrAfter, yrMax, eraAfter, eraMax, eraNow; + + status = U_ZERO_ERROR; + ucal_clear(ucalTest); + ucal_set(ucalTest, UCAL_YEAR, 2); + ucal_set(ucalTest, UCAL_ERA, 0); + yrBefore = ucal_get(ucalTest, UCAL_YEAR, &status); + ucal_add(ucalTest, UCAL_YEAR, 1, &status); + yrAfter = ucal_get(ucalTest, UCAL_YEAR, &status); + if (U_FAILURE(status)) { + log_err("FAIL: set era 0 year 2 then add 1 year and get year for %s, error %s\n", + eraTestItemPtr->locale, u_errorName(status)); + } else if ( (eraTestItemPtr->era0YearsGoBackwards && yrAfter>yrBefore) || + (!eraTestItemPtr->era0YearsGoBackwards && yrAfterlocale); + } + + status = U_ZERO_ERROR; + ucal_clear(ucalTest); + ucal_set(ucalTest, UCAL_YEAR, 2); + ucal_set(ucalTest, UCAL_ERA, 0); + yrBefore = ucal_get(ucalTest, UCAL_YEAR, &status); + ucal_roll(ucalTest, UCAL_YEAR, 1, &status); + yrAfter = ucal_get(ucalTest, UCAL_YEAR, &status); + if (U_FAILURE(status)) { + log_err("FAIL: set era 0 year 2 then roll 1 year and get year for %s, error %s\n", + eraTestItemPtr->locale, u_errorName(status)); + } else if ( (eraTestItemPtr->era0YearsGoBackwards && yrAfter>yrBefore) || + (!eraTestItemPtr->era0YearsGoBackwards && yrAfterlocale); + } + + status = U_ZERO_ERROR; + ucal_clear(ucalTest); + ucal_set(ucalTest, UCAL_YEAR, 1); + ucal_set(ucalTest, UCAL_ERA, 0); + if (eraTestItemPtr->era0YearsGoBackwards) { + ucal_roll(ucalTest, UCAL_YEAR, 1, &status); /* roll forward in time to era 0 boundary */ + yrAfter = ucal_get(ucalTest, UCAL_YEAR, &status); + eraAfter = ucal_get(ucalTest, UCAL_ERA, &status); + if (U_FAILURE(status)) { + log_err("FAIL: set era 0 year 1 then roll 1 year and get year,era for %s, error %s\n", + eraTestItemPtr->locale, u_errorName(status)); + /* all calendars with era0YearsGoBackwards have "unbounded" era0 year values, so we should pin at yr 1 */ + } else if (eraAfter != 0 || yrAfter != 1) { + log_err("FAIL: era 0 roll 1 year from year 1 does not stay within era or pin to year 1 for %s (get era %d year %d)\n", + eraTestItemPtr->locale, eraAfter, yrAfter); + } + } else { + /* roll backward in time to where era 0 years go negative, except for the Chinese + calendar, which uses negative eras instead of having years outside the range 1-60 */ + const char * calType = ucal_getType(ucalTest, &status); + ucal_roll(ucalTest, UCAL_YEAR, -2, &status); + yrAfter = ucal_get(ucalTest, UCAL_YEAR, &status); + eraAfter = ucal_get(ucalTest, UCAL_ERA, &status); + if (U_FAILURE(status)) { + log_err("FAIL: set era 0 year 1 then roll -2 years and get year,era for %s, error %s\n", + eraTestItemPtr->locale, u_errorName(status)); + } else if ( uprv_strcmp(calType,"chinese")!=0 && (eraAfter != 0 || yrAfter != -1) ) { + log_err("FAIL: era 0 roll -2 years from year 1 does not stay within era or produce year -1 for %s (get era %d year %d)\n", + eraTestItemPtr->locale, eraAfter, yrAfter); + } + } + + status = U_ZERO_ERROR; + ucal_clear(ucalTest); + ucal_set(ucalTest, UCAL_YEAR, 1); + ucal_set(ucalTest, UCAL_ERA, 0); + eraMax = ucal_getLimit(ucalTest, UCAL_ERA, UCAL_MAXIMUM, &status); + if ( U_SUCCESS(status) && eraMax > 0 ) { + /* try similar tests for era 1 (if calendar has it), in which years always go forward */ + status = U_ZERO_ERROR; + ucal_clear(ucalTest); + ucal_set(ucalTest, UCAL_YEAR, 2); + ucal_set(ucalTest, UCAL_ERA, 1); + yrBefore = ucal_get(ucalTest, UCAL_YEAR, &status); + ucal_add(ucalTest, UCAL_YEAR, 1, &status); + yrAfter = ucal_get(ucalTest, UCAL_YEAR, &status); + if (U_FAILURE(status)) { + log_err("FAIL: set era 1 year 2 then add 1 year and get year for %s, error %s\n", + eraTestItemPtr->locale, u_errorName(status)); + } else if ( yrAfterlocale); + } + + status = U_ZERO_ERROR; + ucal_clear(ucalTest); + ucal_set(ucalTest, UCAL_YEAR, 2); + ucal_set(ucalTest, UCAL_ERA, 1); + yrBefore = ucal_get(ucalTest, UCAL_YEAR, &status); + ucal_roll(ucalTest, UCAL_YEAR, 1, &status); + yrAfter = ucal_get(ucalTest, UCAL_YEAR, &status); + if (U_FAILURE(status)) { + log_err("FAIL: set era 1 year 2 then roll 1 year and get year for %s, error %s\n", + eraTestItemPtr->locale, u_errorName(status)); + } else if ( yrAfterlocale); + } + + status = U_ZERO_ERROR; + ucal_clear(ucalTest); + ucal_set(ucalTest, UCAL_YEAR, 1); + ucal_set(ucalTest, UCAL_ERA, 1); + yrMax = ucal_getLimit(ucalTest, UCAL_YEAR, UCAL_ACTUAL_MAXIMUM, &status); /* max year value for era 1 */ + ucal_roll(ucalTest, UCAL_YEAR, -1, &status); /* roll down which should pin or wrap to end */ + yrAfter = ucal_get(ucalTest, UCAL_YEAR, &status); + eraAfter = ucal_get(ucalTest, UCAL_ERA, &status); + if (U_FAILURE(status)) { + log_err("FAIL: set era 1 year 1 then roll -1 year and get year,era for %s, error %s\n", + eraTestItemPtr->locale, u_errorName(status)); + /* if yrMax is reasonable we should wrap to that, else we should pin at yr 1 */ + } else if (yrMax >= 32768) { + if (eraAfter != 1 || yrAfter != 1) { + log_err("FAIL: era 1 roll -1 year from year 1 does not stay within era or pin to year 1 for %s (get era %d year %d)\n", + eraTestItemPtr->locale, eraAfter, yrAfter); + } + } else if (eraAfter != 1 || yrAfter != yrMax) { + log_err("FAIL: era 1 roll -1 year from year 1 does not stay within era or wrap to year %d for %s (get era %d year %d)\n", + yrMax, eraTestItemPtr->locale, eraAfter, yrAfter); + } else { + /* now roll up which should wrap to beginning */ + ucal_roll(ucalTest, UCAL_YEAR, 1, &status); /* now roll up which should wrap to beginning */ + yrAfter = ucal_get(ucalTest, UCAL_YEAR, &status); + eraAfter = ucal_get(ucalTest, UCAL_ERA, &status); + if (U_FAILURE(status)) { + log_err("FAIL: era 1 roll 1 year from end and get year,era for %s, error %s\n", + eraTestItemPtr->locale, u_errorName(status)); + } else if (eraAfter != 1 || yrAfter != 1) { + log_err("FAIL: era 1 roll 1 year from year %d does not stay within era or wrap to year 1 for %s (get era %d year %d)\n", + yrMax, eraTestItemPtr->locale, eraAfter, yrAfter); + } + } + + /* if current era > 1, try the same roll tests for current era */ + ucal_setMillis(ucalTest, ucal_getNow(), &status); + eraNow = ucal_get(ucalTest, UCAL_ERA, &status); + if ( U_SUCCESS(status) && eraNow > 1 ) { + status = U_ZERO_ERROR; + ucal_clear(ucalTest); + ucal_set(ucalTest, UCAL_YEAR, 1); + ucal_set(ucalTest, UCAL_ERA, eraNow); + yrMax = ucal_getLimit(ucalTest, UCAL_YEAR, UCAL_ACTUAL_MAXIMUM, &status); /* max year value for this era */ + ucal_roll(ucalTest, UCAL_YEAR, -1, &status); + yrAfter = ucal_get(ucalTest, UCAL_YEAR, &status); + eraAfter = ucal_get(ucalTest, UCAL_ERA, &status); + if (U_FAILURE(status)) { + log_err("FAIL: set era %d year 1 then roll -1 year and get year,era for %s, error %s\n", + eraNow, eraTestItemPtr->locale, u_errorName(status)); + /* if yrMax is reasonable we should wrap to that, else we should pin at yr 1 */ + } else if (yrMax >= 32768) { + if (eraAfter != eraNow || yrAfter != 1) { + log_err("FAIL: era %d roll -1 year from year 1 does not stay within era or pin to year 1 for %s (get era %d year %d)\n", + eraNow, eraTestItemPtr->locale, eraAfter, yrAfter); + } + } else if (eraAfter != eraNow || yrAfter != yrMax) { + log_err("FAIL: era %d roll -1 year from year 1 does not stay within era or wrap to year %d for %s (get era %d year %d)\n", + eraNow, yrMax, eraTestItemPtr->locale, eraAfter, yrAfter); + } else { + /* now roll up which should wrap to beginning */ + ucal_roll(ucalTest, UCAL_YEAR, 1, &status); /* now roll up which should wrap to beginning */ + yrAfter = ucal_get(ucalTest, UCAL_YEAR, &status); + eraAfter = ucal_get(ucalTest, UCAL_ERA, &status); + if (U_FAILURE(status)) { + log_err("FAIL: era %d roll 1 year from end and get year,era for %s, error %s\n", + eraNow, eraTestItemPtr->locale, u_errorName(status)); + } else if (eraAfter != eraNow || yrAfter != 1) { + log_err("FAIL: era %d roll 1 year from year %d does not stay within era or wrap to year 1 for %s (get era %d year %d)\n", + eraNow, yrMax, eraTestItemPtr->locale, eraAfter, yrAfter); + } + } + } + } + + ucal_close(ucalTest); + } else { + log_data_err("FAIL: ucal_open fails for zone GMT, locale %s, UCAL_DEFAULT\n", eraTestItemPtr->locale); + } + } +} + #endif /* #if !UCONFIG_NO_FORMATTING */