Skip to content

Commit

Permalink
ICU-9972 Fix Chinese/Dangi Calendar getActualMaximum(UCAL_DAY_OF_YEAR)
Browse files Browse the repository at this point in the history
Also fix ICU-12620 which is mark duplicate of ICU-9972 just now.
and fix ICU-22258.

Separate the new year and winter solstice cache since the calculated
value for these two calendar are mostly but not always the same due
to slightly different observation timeZone.

Remove the epochYear and zoneAstroCalc from the member data
and instead return them from a getStting() method with the two caches
since all four of them are constant per subclass of ChineseCalendar
and do not need to be different per object.

The known issues in the TestLimit is caused by both Calendar get/put the
value from the same cache while the calculated result depends on the
timeZone zoneAstroCalc.
  • Loading branch information
FrankYFTang committed Mar 28, 2024
1 parent 9e6173f commit d83d26c
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 124 deletions.
116 changes: 68 additions & 48 deletions icu4c/source/i18n/chnsecal.cpp
Expand Up @@ -24,7 +24,7 @@
#include "umutex.h"
#include <float.h>
#include "gregoimp.h" // Math
#include "astro.h" // CalendarAstronomer
#include "astro.h" // CalendarAstronomer and CalendarCache
#include "unicode/simpletz.h"
#include "uhash.h"
#include "ucln_in.h"
Expand Down Expand Up @@ -117,7 +117,7 @@ namespace {

const TimeZone* getAstronomerTimeZone();
int32_t newMoonNear(const TimeZone*, double, UBool);
int32_t newYear(const TimeZone* timeZone, int32_t);
int32_t newYear(const icu::ChineseCalendar::Setting&, int32_t);
UBool isLeapMonthBetween(const TimeZone*, int32_t, int32_t);

} // namespace
Expand All @@ -128,27 +128,13 @@ ChineseCalendar* ChineseCalendar::clone() const {

ChineseCalendar::ChineseCalendar(const Locale& aLocale, UErrorCode& success)
: Calendar(TimeZone::forLocaleOrDefault(aLocale), aLocale, success),
hasLeapMonthBetweenWinterSolstices(false),
fEpochYear(CHINESE_EPOCH_YEAR),
fAstronomerTimeZone(getAstronomerTimeZone())
{
setTimeInMillis(getNow(), success); // Call this again now that the vtable is set up properly.
}

ChineseCalendar::ChineseCalendar(const Locale& aLocale, int32_t epochYear,
const TimeZone* zoneAstroCalc, UErrorCode &success)
: Calendar(TimeZone::forLocaleOrDefault(aLocale), aLocale, success),
hasLeapMonthBetweenWinterSolstices(false),
fEpochYear(epochYear),
fAstronomerTimeZone(zoneAstroCalc)
hasLeapMonthBetweenWinterSolstices(false)
{
setTimeInMillis(getNow(), success); // Call this again now that the vtable is set up properly.
}

ChineseCalendar::ChineseCalendar(const ChineseCalendar& other) : Calendar(other) {
hasLeapMonthBetweenWinterSolstices = other.hasLeapMonthBetweenWinterSolstices;
fEpochYear = other.fEpochYear;
fAstronomerTimeZone = other.fAstronomerTimeZone;
}

ChineseCalendar::~ChineseCalendar()
Expand Down Expand Up @@ -239,12 +225,16 @@ int32_t ChineseCalendar::handleGetExtendedYear(UErrorCode& status) {
// adjust to the instance specific epoch
int32_t cycle = internalGet(UCAL_ERA, 1);
year = internalGet(UCAL_YEAR, 1);
const Setting setting = getSetting(status);
if (U_FAILURE(status)) {
return 0;
}
// Handle int32 overflow calculation for
// year = year + (cycle-1) * 60 -(fEpochYear - CHINESE_EPOCH_YEAR)
if (uprv_add32_overflow(cycle, -1, &cycle) || // 0-based cycle
uprv_mul32_overflow(cycle, 60, &cycle) ||
uprv_add32_overflow(year, cycle, &year) ||
uprv_add32_overflow(year, -(fEpochYear-CHINESE_EPOCH_YEAR),
uprv_add32_overflow(year, -(setting.epochYear-CHINESE_EPOCH_YEAR),
&year)) {
status = U_ILLEGAL_ARGUMENT_ERROR;
return 0;
Expand All @@ -262,13 +252,14 @@ int32_t ChineseCalendar::handleGetExtendedYear(UErrorCode& status) {
* @stable ICU 2.8
*/
int32_t ChineseCalendar::handleGetMonthLength(int32_t extendedYear, int32_t month, UErrorCode& status) const {
const Setting setting = getSetting(status);
int32_t thisStart = handleComputeMonthStart(extendedYear, month, true, status);
if (U_FAILURE(status)) {
return 0;
}
thisStart = thisStart -
kEpochStartAsJulianDay + 1; // Julian day -> local days
int32_t nextStart = newMoonNear(fAstronomerTimeZone, thisStart + SYNODIC_GAP, true);
int32_t nextStart = newMoonNear(setting.zoneAstroCalc, thisStart + SYNODIC_GAP, true);
return nextStart - thisStart;
}

Expand Down Expand Up @@ -318,7 +309,9 @@ struct MonthInfo {
bool isLeapMonth;
bool hasLeapMonthBetweenWinterSolstices;
};
struct MonthInfo computeMonthInfo(const TimeZone* timeZone, int32_t gyear, int32_t days);
struct MonthInfo computeMonthInfo(
const icu::ChineseCalendar::Setting& setting,
int32_t gyear, int32_t days);

} // namespace

Expand Down Expand Up @@ -350,13 +343,18 @@ int64_t ChineseCalendar::handleComputeMonthStart(int32_t eyear, int32_t month, U
month = (int32_t)m;
}

const Setting setting = getSetting(status);
if (U_FAILURE(status)) {
return 0;
}
int32_t gyear;
if (uprv_add32_overflow(eyear, fEpochYear - 1, &gyear)) {
if (uprv_add32_overflow(eyear, setting.epochYear - 1, &gyear)) {
status = U_ILLEGAL_ARGUMENT_ERROR;
return 0;
}
int32_t theNewYear = newYear(fAstronomerTimeZone, gyear);
int32_t newMoon = newMoonNear(fAstronomerTimeZone, theNewYear + month * 29, true);

int32_t theNewYear = newYear(setting, gyear);
int32_t newMoon = newMoonNear(setting.zoneAstroCalc, theNewYear + month * 29, true);

// Ignore IS_LEAP_MONTH field if useMonth is false
bool isLeapMonth = false;
Expand All @@ -370,9 +368,9 @@ int64_t ChineseCalendar::handleComputeMonthStart(int32_t eyear, int32_t month, U
int32_t unusedDayOfYear;
Grego::dayToFields(newMoon, gyear, unusedMonth, unusedDayOfWeek, unusedDayOfMonth, unusedDayOfYear);

struct MonthInfo monthInfo = computeMonthInfo(fAstronomerTimeZone, gyear, newMoon);
struct MonthInfo monthInfo = computeMonthInfo(setting, gyear, newMoon);
if (month != monthInfo.month-1 || isLeapMonth != monthInfo.isLeapMonth) {
newMoon = newMoonNear(fAstronomerTimeZone, newMoon + SYNODIC_GAP, true);
newMoon = newMoonNear(setting.zoneAstroCalc, newMoon + SYNODIC_GAP, true);
}
int32_t julianDay;
if (uprv_add32_overflow(newMoon-1, kEpochStartAsJulianDay, &julianDay)) {
Expand Down Expand Up @@ -484,12 +482,13 @@ void ChineseCalendar::roll(UCalendarDateFields field, int32_t amount, UErrorCode
case UCAL_MONTH:
case UCAL_ORDINAL_MONTH:
if (amount != 0) {
const Setting setting = getSetting(status);
int32_t day = get(UCAL_JULIAN_DAY, status) - kEpochStartAsJulianDay; // Get local day
int32_t month = get(UCAL_MONTH, status); // 0-based month
int32_t dayOfMonth = get(UCAL_DAY_OF_MONTH, status);
bool isLeapMonth = get(UCAL_IS_LEAP_MONTH, status) == 1;
if (U_FAILURE(status)) break;
struct RollMonthInfo r = rollMonth(fAstronomerTimeZone, amount,
struct RollMonthInfo r = rollMonth(setting.zoneAstroCalc, amount,
day, month, dayOfMonth, isLeapMonth, hasLeapMonthBetweenWinterSolstices, status);
if (U_FAILURE(status)) break;
if (r.newMoon != r.month) {
Expand Down Expand Up @@ -566,15 +565,17 @@ double millisToDays(const TimeZone* timeZone, double millis) {
* Return the major solar term on or after December 15 of the given
* Gregorian year, that is, the winter solstice of the given year.
* Computations are relative to Asia/Shanghai time zone.
* @param timeZone time zone for the Astro calculation.
* @param setting setting (time zone and caches) for the Astro calculation.
* @param gyear a Gregorian year
* @return days after January 1, 1970 0:00 Asia/Shanghai of the
* winter solstice of the given year
*/
int32_t winterSolstice(const TimeZone* timeZone, int32_t gyear) {
int32_t winterSolstice(const icu::ChineseCalendar::Setting& setting,
int32_t gyear) {
const TimeZone* timeZone = setting.zoneAstroCalc;

UErrorCode status = U_ZERO_ERROR;
int32_t cacheValue = CalendarCache::get(&gWinterSolsticeCache, gyear, status);
int32_t cacheValue = CalendarCache::get(setting.winterSolsticeCache, gyear, status);

if (cacheValue == 0) {
// In books December 15 is used, but it fails for some years
Expand All @@ -592,7 +593,7 @@ int32_t winterSolstice(const TimeZone* timeZone, int32_t gyear) {
return 0;
}
cacheValue = (int32_t) days;
CalendarCache::put(&gWinterSolsticeCache, gyear, cacheValue, status);
CalendarCache::put(setting.winterSolsticeCache, gyear, cacheValue, status);
}
if(U_FAILURE(status)) {
cacheValue = 0;
Expand Down Expand Up @@ -695,27 +696,30 @@ UBool isLeapMonthBetween(const TimeZone* timeZone, int32_t newMoon1, int32_t new

/**
* Compute the information about the year.
* @param timeZone time zone for the Astro calculation.
* @param setting setting (time zone and caches) for the Astro calculation.
* @param gyear the Gregorian year of the given date
* @param days days after January 1, 1970 0:00 astronomical base zone
* of the date to compute fields for
* @return The MonthInfo result.
*/
struct MonthInfo computeMonthInfo(const TimeZone* timeZone, int32_t gyear, int32_t days) {
struct MonthInfo computeMonthInfo(
const icu::ChineseCalendar::Setting& setting,
int32_t gyear, int32_t days) {
struct MonthInfo output;
// Find the winter solstices before and after the target date.
// These define the boundaries of this Chinese year, specifically,
// the position of month 11, which always contains the solstice.
// We want solsticeBefore <= date < solsticeAfter.
int32_t solsticeBefore;
int32_t solsticeAfter = winterSolstice(timeZone, gyear);
int32_t solsticeAfter = winterSolstice(setting, gyear);
if (days < solsticeAfter) {
solsticeBefore = winterSolstice(timeZone, gyear - 1);
solsticeBefore = winterSolstice(setting, gyear - 1);
} else {
solsticeBefore = solsticeAfter;
solsticeAfter = winterSolstice(timeZone, gyear + 1);
solsticeAfter = winterSolstice(setting, gyear + 1);
}

const TimeZone* timeZone = setting.zoneAstroCalc;
// Find the start of the month after month 11. This will be either
// the prior month 12 or leap month 11 (very rare). Also find the
// start of the following month 11.
Expand All @@ -725,9 +729,9 @@ struct MonthInfo computeMonthInfo(const TimeZone* timeZone, int32_t gyear, int32
output.hasLeapMonthBetweenWinterSolstices = synodicMonthsBetween(firstMoon, lastMoon) == 12;

output.month = synodicMonthsBetween(firstMoon, output.thisMoon);
int32_t theNewYear = newYear(timeZone, gyear);
int32_t theNewYear = newYear(setting, gyear);
if (days < theNewYear) {
theNewYear = newYear(timeZone, gyear-1);
theNewYear = newYear(setting, gyear-1);
}
if (output.hasLeapMonthBetweenWinterSolstices &&
isLeapMonthBetween(timeZone, firstMoon, output.thisMoon)) {
Expand Down Expand Up @@ -779,11 +783,15 @@ void ChineseCalendar::handleComputeFields(int32_t julianDay, UErrorCode & status
int32_t gyear = getGregorianYear();
int32_t gmonth = getGregorianMonth();

struct MonthInfo monthInfo = computeMonthInfo(fAstronomerTimeZone, gyear, days);
const Setting setting = getSetting(status);
if (U_FAILURE(status)) {
return;
}
struct MonthInfo monthInfo = computeMonthInfo(setting, gyear, days);
hasLeapMonthBetweenWinterSolstices = monthInfo.hasLeapMonthBetweenWinterSolstices;

// Extended year and cycle year is based on the epoch year
int32_t eyear = gyear - fEpochYear;
int32_t eyear = gyear - setting.epochYear;
int32_t cycle_year = gyear - CHINESE_EPOCH_YEAR;
if (monthInfo.month < 11 ||
gmonth >= UCAL_JULY) {
Expand All @@ -800,9 +808,9 @@ void ChineseCalendar::handleComputeFields(int32_t julianDay, UErrorCode & status
// date is in month 11, leap 11, 12. There is never a leap 12.
// New year computations are cached so this should be cheap in
// the long run.
int32_t theNewYear = newYear(fAstronomerTimeZone, gyear);
int32_t theNewYear = newYear(setting, gyear);
if (days < theNewYear) {
theNewYear = newYear(fAstronomerTimeZone, gyear-1);
theNewYear = newYear(setting, gyear-1);
}
cycle++;
yearOfCycle++;
Expand Down Expand Up @@ -844,19 +852,21 @@ namespace {

/**
* Return the Chinese new year of the given Gregorian year.
* @param timeZone time zone for the Astro calculation.
* @param setting setting (time zone and caches) for the Astro calculation.
* @param gyear a Gregorian year
* @return days after January 1, 1970 0:00 astronomical base zone of the
* Chinese new year of the given year (this will be a new moon)
*/
int32_t newYear(const TimeZone* timeZone, int32_t gyear) {
int32_t newYear(const icu::ChineseCalendar::Setting& setting,
int32_t gyear) {
const TimeZone* timeZone = setting.zoneAstroCalc;
UErrorCode status = U_ZERO_ERROR;
int32_t cacheValue = CalendarCache::get(&gNewYearCache, gyear, status);
int32_t cacheValue = CalendarCache::get(setting.newYearCache, gyear, status);

if (cacheValue == 0) {

int32_t solsticeBefore= winterSolstice(timeZone, gyear - 1);
int32_t solsticeAfter = winterSolstice(timeZone, gyear);
int32_t solsticeBefore= winterSolstice(setting, gyear - 1);
int32_t solsticeAfter = winterSolstice(setting, gyear);
int32_t newMoon1 = newMoonNear(timeZone, solsticeBefore + 1, true);
int32_t newMoon2 = newMoonNear(timeZone, newMoon1 + SYNODIC_GAP, true);
int32_t newMoon11 = newMoonNear(timeZone, solsticeAfter + 1, false);
Expand All @@ -869,7 +879,7 @@ int32_t newYear(const TimeZone* timeZone, int32_t gyear) {
cacheValue = newMoon2;
}

CalendarCache::put(&gNewYearCache, gyear, cacheValue, status);
CalendarCache::put(setting.newYearCache, gyear, cacheValue, status);
}
if(U_FAILURE(status)) {
cacheValue = 0;
Expand All @@ -893,6 +903,7 @@ int32_t newYear(const TimeZone* timeZone, int32_t gyear) {
*/
void ChineseCalendar::offsetMonth(int32_t newMoon, int32_t dayOfMonth, int32_t delta,
UErrorCode& status) {
const Setting setting = getSetting(status);
if (U_FAILURE(status)) { return; }

// Move to the middle of the month before our target month.
Expand All @@ -906,7 +917,7 @@ void ChineseCalendar::offsetMonth(int32_t newMoon, int32_t dayOfMonth, int32_t d
newMoon = static_cast<int32_t>(value);

// Search forward to the target month's new moon
newMoon = newMoonNear(fAstronomerTimeZone, newMoon, true);
newMoon = newMoonNear(setting.zoneAstroCalc, newMoon, true);

// Find the target dayOfMonth
int32_t jd = newMoon + kEpochStartAsJulianDay - 1 + dayOfMonth;
Expand Down Expand Up @@ -1039,6 +1050,15 @@ int32_t ChineseCalendar::internalGetMonth(int32_t defaultValue, UErrorCode& stat
return internalGetMonth(status);
}

ChineseCalendar::Setting ChineseCalendar::getSetting(UErrorCode&) const {
return {
CHINESE_EPOCH_YEAR,
getAstronomerTimeZone(),
&gWinterSolsticeCache,
&gNewYearCache
};
}

U_NAMESPACE_END

#endif
Expand Down
28 changes: 8 additions & 20 deletions icu4c/source/i18n/chnsecal.h
Expand Up @@ -27,6 +27,7 @@

U_NAMESPACE_BEGIN

class CalendarCache;
/**
* <code>ChineseCalendar</code> is a concrete subclass of {@link Calendar}
* that implements a traditional Chinese calendar. The traditional Chinese
Expand Down Expand Up @@ -152,23 +153,6 @@ class U_I18N_API ChineseCalendar : public Calendar {
*/
virtual void setTemporalMonthCode(const char* code, UErrorCode& status) override;

protected:

/**
* Constructs a ChineseCalendar based on the current time in the default time zone
* with the given locale, using the specified epoch year and time zone for
* astronomical calculations.
*
* @param aLocale The given locale.
* @param epochYear The epoch year to use for calculation.
* @param zoneAstroCalc The TimeZone to use for astronomical calculations. If null,
* will be set appropriately for Chinese calendar (UTC + 8:00).
* @param success Indicates the status of ChineseCalendar object construction;
* if successful, will not be changed to an error value.
* @internal
*/
ChineseCalendar(const Locale& aLocale, int32_t epochYear, const TimeZone* zoneAstroCalc, UErrorCode &success);

public:
/**
* Copy Constructor
Expand Down Expand Up @@ -197,9 +181,6 @@ class U_I18N_API ChineseCalendar : public Calendar {
// this value could be false for a date prior to the Winter Solstice of that
// year but that year still has a leap month and therefor is a leap year.
UBool hasLeapMonthBetweenWinterSolstices;
int32_t fEpochYear; // Start year of this Chinese calendar instance.
const TimeZone* fAstronomerTimeZone; // Zone used for the astronomical calculation
// of this Chinese calendar instance.

//----------------------------------------------------------------------
// Calendar framework
Expand Down Expand Up @@ -273,7 +254,14 @@ class U_I18N_API ChineseCalendar : public Calendar {
*/
virtual const char * getType() const override;

struct Setting {
int32_t epochYear;
const TimeZone* zoneAstroCalc;
CalendarCache** winterSolsticeCache;
CalendarCache** newYearCache;
};
protected:
virtual Setting getSetting(UErrorCode& status) const;
virtual int32_t internalGetMonth(int32_t defaultValue, UErrorCode& status) const override;

virtual int32_t internalGetMonth(UErrorCode& status) const override;
Expand Down

0 comments on commit d83d26c

Please sign in to comment.