Skip to content

ICU-23489 WEEK_OF_MONTH in the Gregorian cutover year - #4132

Open
cayde-6 wants to merge 1 commit into
unicode-org:mainfrom
cayde-6:ICU-23489-wom-cutover
Open

ICU-23489 WEEK_OF_MONTH in the Gregorian cutover year#4132
cayde-6 wants to merge 1 commit into
unicode-org:mainfrom
cayde-6:ICU-23489-wom-cutover

Conversation

@cayde-6

@cayde-6 cayde-6 commented Aug 22, 2026

Copy link
Copy Markdown

Two related fixes, squashed into one commit as the single-commit check asks.
The first is a prerequisite: without it the second one changes behaviour for
calendars whose cutover was set explicitly.

1. Store a Julian Day in fCutoverJulianDay when the cutover is set explicitly

The constructors store kCutoverJulianDay, 2299161, an actual Julian Day, and
that is what handleComputeJulianDay() and handleComputeFields() compare the
field against. setGregorianChange() stored floorDivide(date, kOneDay)
instead, a day count relative to the 1970 epoch, 2440588 days off.

The visible effect: a calendar given the standard papal cutover date explicitly
resolves dates of the cutover year differently from the default calendar, which
uses that very same date implicitly. With WEEK_OF_MONTH in 1582, GMT, first
day of week Sunday, minimal days in first week 1, the two disagree in 25 of the
60 month and week combinations, February, March, June, August and September
each by seven days.

TestExplicitCutoverMatchesDefault23489 pins the two against each other across
the whole cutover year.

ICU4J has the same defect at GregorianCalendar.java:514, with the same
2299161 default at line 323. I have not touched it here, since I have no way to
build and run ICU4J; happy to file it separately.

2. Apply the cutover-year WEEK_OF_MONTH shift only to the cutover month

handleComputeJulianDay adds 14 days when the calendar is Gregorian, the
extended year equals the cutover year, and the best field is WEEK_OF_MONTH.
The condition tests the year, so it fired for every month of that year rather
than for the month that loses days.

With the default cutover only October 1582 is short: 1582-10-01 to 1582-10-04,
then 1582-10-15 to 1582-10-31. November and December 1582 are ordinary months.
1582-11-01 is a Monday and 1582-12-01 is a Wednesday, so with weeks starting on
Sunday the first week of November begins on 1582-10-31 and the first week of
December begins on 1582-11-28. ICU4C resolved those to 1582-11-14 and
1582-12-12, two weeks late. ICU4J, which has no such shift, returns the earlier
dates. An out of range month was shifted too: YEAR 1582 with MONTH 14 landed two
weeks after YEAR 1583 with MONTH 2.

The shift is now applied only when the requested month is the month the cutover
falls in. The month comes from internalGetMonth(), the same accessor
Calendar::handleComputeJulianDay uses to compute the day, so a month resolved
from ORDINAL_MONTH is treated identically to one set through MONTH. The
cutover month is derived from fGregorianCutover.

input before after
October 1582, weeks 1-5 five consecutive Sundays unchanged
November 1582, week 1 1582-11-14 1582-10-31
December 1582, week 1 1582-12-12 1582-11-28
YEAR 1582, MONTH 14, week 1 1583-03-13 1583-02-27
January 1583, January 2024 unchanged unchanged

Deleting the branch outright is not correct, in case that looks tempting:
without the shift, weeks 4 and 5 of October 1582 collapse onto the same instants
as weeks 2 and 3, verified to the millisecond.

The 14 day constant is left as is. It is tied to the ten day gap of the 1582
cutover, and a cutover set through setGregorianChange needs a different value.
That case is ICU-3350, which is open for exactly this.

TestWeekOfMonthInCutoverYear23489 covers October, November and December 1582,
two control months outside the cutover year, the ORDINAL_MONTH path, an out of
range month, and pins the five weeks of October 1582 as five distinct instants
exactly seven days apart.

Verification

intltest and cintltst both pass in full, from a clean build.

Reverting the whole change: TestExplicitCutoverMatchesDefault23489 fails on 25
of its 60 rows, and TestWeekOfMonthInCutoverYear23489 on 11, November,
December and the out of range month.

The two parts have to travel together. Reverting only the setGregorianChange
part while keeping the narrowed condition makes
TestExplicitCutoverMatchesDefault23489 fail on 45 rows rather than 25: the
narrowed condition removes a shift that used to mask the unit mismatch for
explicitly set cutovers. Reverting only the narrowed condition while keeping the
unit fix leaves TestWeekOfMonthInCutoverYear23489 failing on its 11 rows.

ICU4J parity

For the ordinary months of the cutover year ICU4C now agrees with ICU4J, which
is what this ticket is about.

For October 1582 the two still differ, and this does not paper over it: ICU4J
resolves weeks 4 and 5 to the same instants as weeks 2 and 3, while ICU4C keeps
five distinct weeks. ICU4C looks right here, so a mirrored ICU4J test would fail
today rather than pass. Worth a separate ticket if you agree.

@CLAassistant

CLAassistant commented Aug 22, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Two related fixes; the first is a prerequisite for the second.

1. Store a Julian Day in fCutoverJulianDay when the cutover is set
explicitly. The constructors store kCutoverJulianDay, an actual Julian
Day, and that is what handleComputeJulianDay() and handleComputeFields()
compare the field against, but setGregorianChange() stored a day count
relative to the 1970 epoch. A calendar given the standard cutover date
explicitly therefore resolved dates of the cutover year differently from
the default calendar, which uses that same date implicitly.

2. Apply the cutover-year WEEK_OF_MONTH shift only to the cutover month.
The shift compensates for the days the cutover removes from a month, but
it was applied to every month of the cutover year, so November and
December 1582 resolved two weeks late, as did an out of range month such
as MONTH 14 with YEAR 1582. ICU4J, which has no such shift, resolves the
ordinary months the way this change now does. The month comes from
internalGetMonth(), so a month resolved from ORDINAL_MONTH is treated the
same, and the cutover month is derived from fGregorianCutover.

October 1582 is unchanged: its five weeks stay five distinct instants
seven days apart.

Add TestExplicitCutoverMatchesDefault23489 and
TestWeekOfMonthInCutoverYear23489.
@cayde-6
cayde-6 force-pushed the ICU-23489-wom-cutover branch from 1e2f6be to 49bfa25 Compare August 22, 2026 21:06
@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

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.

2 participants