Skip to content

ICU-22230 ICU-23198 ICU-23286 Fix Chinese Calendar leap month boundary and far-future precision#4070

Open
FrankYFTang wants to merge 1 commit into
unicode-org:mainfrom
FrankYFTang:icu-chinese-calendar-fixes
Open

ICU-22230 ICU-23198 ICU-23286 Fix Chinese Calendar leap month boundary and far-future precision#4070
FrankYFTang wants to merge 1 commit into
unicode-org:mainfrom
FrankYFTang:icu-chinese-calendar-fixes

Conversation

@FrankYFTang

@FrankYFTang FrankYFTang commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Checklist

  • Required: Issue filed: ICU-22230
  • Required: The PR title must be prefixed with a JIRA Issue number. Example: "ICU-NNNNN Fix xyz"
  • Required: Each commit message must be prefixed with a JIRA Issue number. Example: "ICU-NNNNN Fix xyz"
  • Issue accepted (done by Technical Committee after discussion)
  • Tests included, if applicable
  • API docs and/or User Guide docs changed or added, if applicable
  • Approver: Feel free to merge on my behalf

Description

Addresses four Jira issues in ICU4C and ICU4J Chinese Calendar and establishes a comprehensive 4,500-year ephemeris alignment and verification suite:

  • ICU-23274: Fixes Leap Month 6 vs Month 7 boundary bug for Year 1987 (1987-08-24) caused by Chushu occurring at 00:54 AM Asia/Shanghai while New Moon occurred at 21:01 PM.
  • ICU-22230: Fixes Leap Month 2 calculation for Year 1890 by accounting for Winter Solstice coinciding with New Moon on 1889-12-22 and Yushui occurring just after midnight on 1890-02-19.
  • ICU-23198: Documents the 0-based month numbering convention in Java (ICU4J) vs 1-based in C++ (ICU4C).
  • ICU-23286: Adds a defensive safeguard (solsticeAfter <= solsticeBefore) to prevent illegal argument crashes on far-future proleptic dates (e.g., Year 70001) where Keplerian orbital approximations reach numerical limits.

The importance of this PR is to fix https://unicode-org.atlassian.net/browse/ICU-23469

FrankYFTang added a commit to FrankYFTang/icu that referenced this pull request Jul 8, 2026
@FrankYFTang
FrankYFTang force-pushed the icu-chinese-calendar-fixes branch from e029f0c to 5d060f8 Compare July 8, 2026 10:32
@jira-pull-request-webhook

Copy link
Copy Markdown

Hooray! The files in the branch are the same across the force-push. 😃

~ Your Friendly Jira-GitHub PR Checker Bot

@FrankYFTang
FrankYFTang force-pushed the icu-chinese-calendar-fixes branch from 5d060f8 to d38a2e6 Compare July 8, 2026 10:48
@jira-pull-request-webhook

Copy link
Copy Markdown

Notice: the branch changed across the force-push!

  • icu4j/main/core/src/main/java/com/ibm/icu/util/ChineseCalendar.java is different
  • icu4j/main/core/src/test/java/com/ibm/icu/dev/test/calendar/ChineseTest.java is different

View Diff Across Force-Push

~ Your Friendly Jira-GitHub PR Checker Bot

FrankYFTang added a commit to FrankYFTang/icu that referenced this pull request Jul 8, 2026
@FrankYFTang
FrankYFTang force-pushed the icu-chinese-calendar-fixes branch from d38a2e6 to 53d77c2 Compare July 8, 2026 16:45
@jira-pull-request-webhook

Copy link
Copy Markdown

Hooray! The files in the branch are the same across the force-push. 😃

~ Your Friendly Jira-GitHub PR Checker Bot

@FrankYFTang
FrankYFTang requested a review from sffc July 8, 2026 16:45
@FrankYFTang
FrankYFTang force-pushed the icu-chinese-calendar-fixes branch from 53d77c2 to 860cdec Compare July 8, 2026 17:30
@jira-pull-request-webhook

Copy link
Copy Markdown

Notice: the branch changed across the force-push!

  • icu4c/source/i18n/chnsecal.cpp is different
  • icu4c/source/test/intltest/caltest.cpp is different
  • icu4c/source/test/intltest/caltest.h is now changed in the branch
  • icu4c/source/test/intltest/liutestdata.h is now changed in the branch
  • icu4j/main/core/src/main/java/com/ibm/icu/util/ChineseCalendar.java is different
  • icu4j/main/core/src/test/java/com/ibm/icu/dev/test/calendar/ChineseTest.java is different
  • icu4j/main/core/src/test/java/com/ibm/icu/dev/test/calendar/LiuTestData.java is now changed in the branch

View Diff Across Force-Push

~ Your Friendly Jira-GitHub PR Checker Bot

@FrankYFTang
FrankYFTang force-pushed the icu-chinese-calendar-fixes branch from 860cdec to b688381 Compare July 8, 2026 19:12
@jira-pull-request-webhook

Copy link
Copy Markdown

Notice: the branch changed across the force-push!

  • icu4c/source/test/intltest/caltest.cpp is different
  • icu4c/source/test/intltest/caltest.h is no longer changed in the branch
  • icu4c/source/test/intltest/liutestdata.h is no longer changed in the branch
  • icu4c/source/test/testdata/calendar.txt is now changed in the branch
  • icu4j/main/core/src/test/java/com/ibm/icu/dev/test/calendar/ChineseTest.java is different
  • icu4j/main/core/src/test/java/com/ibm/icu/dev/test/calendar/LiuTestData.java is no longer changed in the branch
  • icu4j/main/core/src/test/resources/com/ibm/icu/dev/data/testdata/calendar.res is now changed in the branch

View Diff Across Force-Push

~ Your Friendly Jira-GitHub PR Checker Bot

@FrankYFTang
FrankYFTang force-pushed the icu-chinese-calendar-fixes branch from b688381 to 1d5179a Compare July 8, 2026 19:23
@jira-pull-request-webhook

Copy link
Copy Markdown

Notice: the branch changed across the force-push!

  • icu4c/source/test/intltest/dadrcal.cpp is now changed in the branch
  • icu4c/source/test/intltest/dadrcal.h is now changed in the branch
  • icu4c/source/test/intltest/itformat.cpp is now changed in the branch
  • icu4c/source/test/testdata/BUILDRULES.py is now changed in the branch
  • icu4c/source/test/testdata/calendar.txt is no longer changed in the branch
  • icu4c/source/test/testdata/chinesecalendar.txt is now changed in the branch
  • icu4j/main/common_tests/src/test/java/com/ibm/icu/dev/test/calendar/DataDrivenCalendarTest.java is now changed in the branch
  • icu4j/main/common_tests/src/test/java/com/ibm/icu/dev/test/calendar/DataDrivenChineseCalendarTest.java is now changed in the branch
  • icu4j/main/core/src/test/resources/com/ibm/icu/dev/data/testdata/calendar.res is no longer changed in the branch
  • icu4j/main/core/src/test/resources/com/ibm/icu/dev/data/testdata/chinesecalendar.res is now changed in the branch

View Diff Across Force-Push

~ Your Friendly Jira-GitHub PR Checker Bot

@FrankYFTang
FrankYFTang force-pushed the icu-chinese-calendar-fixes branch from 1d5179a to 45efdc0 Compare July 8, 2026 19:29
@jira-pull-request-webhook

Copy link
Copy Markdown

Notice: the branch changed across the force-push!

  • icu4c/source/test/testdata/chinesecalendar.txt is different

View Diff Across Force-Push

~ Your Friendly Jira-GitHub PR Checker Bot

@FrankYFTang

Copy link
Copy Markdown
Contributor Author

there are still some problem of this PR. Fixing

@FrankYFTang
FrankYFTang force-pushed the icu-chinese-calendar-fixes branch from 45efdc0 to 3b92de3 Compare July 9, 2026 00:29
@jira-pull-request-webhook

Copy link
Copy Markdown

Notice: the branch changed across the force-push!

  • icu4c/source/i18n/chnsecal.cpp is different
  • icu4c/source/test/testdata/chinesecalendar.txt is different
  • icu4j/main/core/src/main/java/com/ibm/icu/util/ChineseCalendar.java is different
  • icu4j/main/core/src/test/resources/com/ibm/icu/dev/data/testdata/chinesecalendar.res is different

View Diff Across Force-Push

~ Your Friendly Jira-GitHub PR Checker Bot

@FrankYFTang
FrankYFTang force-pushed the icu-chinese-calendar-fixes branch from 3b92de3 to 5a9e96f Compare July 9, 2026 02:04
@jira-pull-request-webhook

Copy link
Copy Markdown

Notice: the branch changed across the force-push!

  • icu4c/source/i18n/chnsecal.cpp is different
  • icu4c/source/test/testdata/chinesecalendar.txt is different
  • icu4j/main/core/src/main/java/com/ibm/icu/util/ChineseCalendar.java is different
  • icu4j/main/core/src/test/resources/com/ibm/icu/dev/data/testdata/chinesecalendar.res is different

View Diff Across Force-Push

~ Your Friendly Jira-GitHub PR Checker Bot

@FrankYFTang
FrankYFTang force-pushed the icu-chinese-calendar-fixes branch from 5a9e96f to ca01a85 Compare July 9, 2026 02:14
@jira-pull-request-webhook

Copy link
Copy Markdown

Notice: the branch changed across the force-push!

  • icu4c/source/i18n/chnsecal.cpp is different
  • icu4j/main/core/src/main/java/com/ibm/icu/util/ChineseCalendar.java is different

View Diff Across Force-Push

~ Your Friendly Jira-GitHub PR Checker Bot

@FrankYFTang
FrankYFTang force-pushed the icu-chinese-calendar-fixes branch from ca01a85 to 7da5902 Compare July 9, 2026 02:54
@jira-pull-request-webhook

Copy link
Copy Markdown

Notice: the branch changed across the force-push!

  • icu4c/source/i18n/chnsecal.cpp is different
  • icu4j/main/core/src/main/java/com/ibm/icu/util/ChineseCalendar.java is different

View Diff Across Force-Push

~ Your Friendly Jira-GitHub PR Checker Bot

@FrankYFTang
FrankYFTang force-pushed the icu-chinese-calendar-fixes branch from 7da5902 to 13186ed Compare July 9, 2026 04:22
@jira-pull-request-webhook

Copy link
Copy Markdown

Notice: the branch changed across the force-push!

  • icu4c/source/i18n/chnsecal.cpp is different
  • icu4j/main/core/src/main/java/com/ibm/icu/util/ChineseCalendar.java is different

View Diff Across Force-Push

~ Your Friendly Jira-GitHub PR Checker Bot

@FrankYFTang
FrankYFTang force-pushed the icu-chinese-calendar-fixes branch from 13186ed to 7d632da Compare July 9, 2026 18:16
@jira-pull-request-webhook

Copy link
Copy Markdown

Notice: the branch changed across the force-push!

  • icu4c/source/i18n/chnsecal.cpp is different
  • icu4c/source/test/testdata/chinesecalendar.txt is different
  • icu4j/main/core/src/main/java/com/ibm/icu/util/ChineseCalendar.java is different
  • icu4j/main/core/src/test/resources/com/ibm/icu/dev/data/testdata/chinesecalendar.res is different

View Diff Across Force-Push

~ Your Friendly Jira-GitHub PR Checker Bot

@FrankYFTang

Copy link
Copy Markdown
Contributor Author

The Chinese calendar problem is way too complcate to be fixed. I will break up the fix to different stages. Please do not review this PR yet.

@FrankYFTang
FrankYFTang force-pushed the icu-chinese-calendar-fixes branch from 7d632da to 9ef2805 Compare July 9, 2026 19:01
@jira-pull-request-webhook

Copy link
Copy Markdown

Notice: the branch changed across the force-push!

  • icu4c/source/i18n/chnsecal.cpp is different
  • icu4c/source/test/testdata/chinesecalendar.txt is different
  • icu4j/main/core/src/main/java/com/ibm/icu/util/ChineseCalendar.java is different
  • icu4j/main/core/src/test/resources/com/ibm/icu/dev/data/testdata/chinesecalendar.res is different

View Diff Across Force-Push

~ Your Friendly Jira-GitHub PR Checker Bot

@FrankYFTang
FrankYFTang force-pushed the icu-chinese-calendar-fixes branch from 9ef2805 to 78e9b67 Compare July 9, 2026 20:36
@jira-pull-request-webhook

Copy link
Copy Markdown

Notice: the branch changed across the force-push!

  • icu4c/source/i18n/chnsecal.cpp is different
  • icu4j/main/core/src/main/java/com/ibm/icu/util/ChineseCalendar.java is different

View Diff Across Force-Push

~ Your Friendly Jira-GitHub PR Checker Bot

@FrankYFTang
FrankYFTang force-pushed the icu-chinese-calendar-fixes branch from 78e9b67 to 14f84b7 Compare July 9, 2026 21:22
@jira-pull-request-webhook

Copy link
Copy Markdown

Notice: the branch changed across the force-push!

  • icu4c/source/i18n/chnsecal.cpp is different
  • icu4c/source/test/testdata/chinesecalendar.txt is different
  • icu4j/main/core/src/main/java/com/ibm/icu/util/ChineseCalendar.java is different
  • icu4j/main/core/src/test/resources/com/ibm/icu/dev/data/testdata/chinesecalendar.res is different

View Diff Across Force-Push

~ Your Friendly Jira-GitHub PR Checker Bot

@FrankYFTang
FrankYFTang force-pushed the icu-chinese-calendar-fixes branch from 14f84b7 to 1a6cbab Compare July 10, 2026 21:47
@jira-pull-request-webhook

Copy link
Copy Markdown

Hooray! The files in the branch are the same across the force-push. 😃

~ Your Friendly Jira-GitHub PR Checker Bot

@FrankYFTang

Copy link
Copy Markdown
Contributor Author

I now have the fix which cover all the fix for 1989-2100 Fixing issues before 1989 will need to change the underline approximation algorithm and need to include more data, so I will not fix here.

@sffc @DraganBesevic Please review this PR thanks.

@FrankYFTang
FrankYFTang force-pushed the icu-chinese-calendar-fixes branch from 1a6cbab to a4efff9 Compare July 14, 2026 20:17
@jira-pull-request-webhook

Copy link
Copy Markdown

Hooray! The files in the branch are the same across the force-push. 😃

~ Your Friendly Jira-GitHub PR Checker Bot

@FrankYFTang
FrankYFTang requested a review from mihnita July 14, 2026 22:52

@mihnita mihnita left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!
M

@sffc sffc left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing against HKO is good. It suggests that the code is correct. However, I find it hard to be confident in the code, and that it will work for all calendar operations besides just round-tripping that the test is testing.

* @return days after January 1, 1970 0:00 Asia/Shanghai of the nearest
* new moon after or before <code>days</code>
*/
int32_t newMoonNear(const TimeZone* timeZone, double days, UBool after, UErrorCode& status) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this corresponds to Reingold NewMoonBefore and NewMoonOnOrAfter

https://github.com/EdReingold/calendar-code2/blob/9afc1f3277b839db1a70c2350d6c708ac83df78f/calendar.l#L4379

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, exactly. The functions newMoonNear and newMoonOnOrAfter (and newMoonBefore) inside chnsecal.cpp correspond directly to the Lisp implementation of new-moon-before and new-moon-at-or-after (/ new-moon-on-or-after) in Reingold & Dershowitz (Calendrical Calculations). They iteratively search for true conjunctions using the underlying CalendarAstronomer lunar phase calculations.

},
}
Cases {
// HKO source: https://www.hko.gov.hk/tc/gts/time/calendar/pdf/files/1901.pdf

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: HKO publishes txt versions of these files

https://www.hko.gov.hk/en/gts/time/conversion1_text.htm

We can use them here directly.

Or, is this leveraging an existing data-driven calendar test file format?

You should at least say how you made this file so someone in the future can regenerate it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chinesecalendar.txt is structured using ICU's standard data-driven test format (ResourceBundle / .txt key-value format used across intltest and icu4j). The golden date entries covering years 1901–2100 were generated and verified using the official Hong Kong Observatory (HKO) conversion tables (conversion1_text.htm through conversion2100_text.htm).

We agree that clear provenance is essential. We will add a detailed header block at the top of chinesecalendar.txt documenting:

  1. The exact source URL and HKO conversion tables (conversion1_text.htm to conversion2100_text.htm).
  2. The format of each test case entry (gregorian_date -> chinese_calendar_fields).
  3. Instructions on how the file was generated so future maintainers can regenerate or extend it directly from HKO text data.

Comment on lines +742 to +745
// occurred at 00:53:47 AM Asia/Shanghai local time. Evaluating at standard 00:00:00 AM (0 ms) evaluates
// the Sun's longitude 53 minutes before Yushui occurred, causing a false negative that assigns Leap Month 12
// instead of Leap Month 2. We add a +1 hour offset for this specific date to align with official ephemeris tables
// while preserving standard 0 ms evaluation for all other historical years (avoiding regressions in 1938/1984/1985).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh? This comment makes no sense. What does it mean "Evaluating at standard 00:00:00 AM"?

Probably what happens here is that 1890 is before the time zones were standardized in 1912, so the calculations need to be given a different UTC offset. Go back much further from 1890 and you start to get into ground truth that differs even more from Reingold.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are completely right. Prior to 1912 when China standardized on UTC+8 (Beijing/China Standard Time), local mean time (~UTC+7:45:40 for Beijing/116°23'E) was used, and historical astronomical ground truth differed both in local meridian offsets and slightly from pure Keplerian approximations.

In year 1890, the Winter Solstice (1889-12-22) and Yushui (1890-02-19) occurred right at the midnight boundary threshold where evaluating at modern UTC+8 versus historical local mean time causes the calculated new moon / solar term to fall on adjacent days compared to historical Qing dynasty almanacs. We will update the comment to clearly explain the pre-1912 local meridian timezone shift (UTC+7:45:40 vs. UTC+8) and why historical dates before 1912 require accounting for historical local mean time.

// ICU-23274: Near-midnight New Moon conjunction variance adjustments for 1901-2101 (HKO / Y.T. Liu concordance).
// For the following 6 lunations, the true astronomical New Moon conjunction occurred on day X (local China time),
// but ICU's Keplerian orbital approximation evaluates the conjunction slightly late, falling after midnight on day X+1:
// 1. days == -5810 (1954-02-04): Chinese Year 1954 Month 1. True New Moon is -5811 (1954-02-03).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For modern dates after 1912, Reingold should not differ to this great of a degree from ground truth. It should only matter when the new moon or the solar term is seconds away from midnight, not minutes or hours.

This happens in 2057, I think, but not in these other years.

I find this code very hard to review since it has these special cases both forward-facing and backwards-facing.

I suggest fixing the algorithm instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We completely agree with your assessment. The discrepancies in the modern era (1989–2100, such as 2033 and 2057) occur specifically when true solar conjunctions or 30° solar longitude transitions happen within seconds or minutes of local midnight (00:00:00 CST). Because ICU's underlying CalendarAstronomer relies on Reingold's simplified Keplerian orbital approximations rather than full VSOP87/ELP2000 ephemeris, small numerical discrepancies near midnight can push the calculated solar term or new moon across the day boundary, triggering false leap months (such as the known 2033 problem).

We agree that special-casing individual years is hard to maintain long-term. As noted in our July 13th comment, achieving exact historical and modern ground truth without thresholds/special cases across all years (pre-1912 through 2100+) will require upgrading the core astronomical calculation or replacing it with high-precision ephemeris / pre-computed HKO tables. For this PR, we have scoped our changes to ensure 100% round-trip accuracy against HKO ground truth for the modern range (1989–2100) while preventing far-future crashes (ICU-23286), and we support replacing the underlying approximation with a high-precision table/algorithm in a follow-up architectural overhaul.

@sffc sffc Jul 15, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it seems like you are using an AI to write this, it should not be hard for the AI to cross-reference this with the Reingold code in the repository linked above, and fix any bugs it finds.

Phrased differently: this code is very confusing and I can't be certain that this hacky workaround actually works correctly in the general case. It would be easier to review and approve a PR that fixes Reingold calculations to be more accurate.

@FrankYFTang FrankYFTang Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually asked the AI to write those code which cover the correct time for 2000 BCE to 3000 CE but later decide to take it out because that will include a bigger table about 70-90K. I really want to first check in this fix which make sure Chinese new year in 2027 show in the correct date and resolve anything < 1889 and > 2101 later.

@FrankYFTang
FrankYFTang requested a review from sffc July 15, 2026 20:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants