Skip to content

Conversation

@catamorphism
Copy link
Contributor

@catamorphism catamorphism commented Dec 12, 2025

Add years/months first, then skip to the end of the month if the duration sign is negative, then add the other duration fields. This is to address cases with overflow: reject where previously, an exception was thrown due to constraining the internally-constructed end-of-the-month date.

In the spec, new abstract operations CalendarDateLastDayOfMonth and NonISODateLastDayOfMonth are added to make the algorithm simpler to specify.

Closes #3197

Add years/months first, then skip to the end of the month if the
duration sign is negative, then add the other duration fields.
This is to address cases with overflow: reject where previously,
an exception was thrown due to constraining the internally-constructed
end-of-the-month date.

In the spec, new abstract operations CalendarDateLastDayOfMonth
and NonISODateLastDayOfMonth are added to make the algorithm simpler to
specify.
@catamorphism
Copy link
Contributor Author

The test262 failures are expected; see tc39/test262#4783 .

@catamorphism
Copy link
Contributor Author

The NonISODateLastDayOfMonth AO also needs to be defined in proposal-intl-era-monthcode, but I'll wait for feedback on this PR before I do that.

Copy link
Collaborator

@ptomato ptomato left a comment

Choose a reason for hiding this comment

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

This is much nicer than the previous approach with two overflow parameters!

(Comments on the polyfill code also apply to spec text.)

Comment on lines 1723 to 1736
let previousIsoDate = isoDate;
let nextIsoDate = isoDate;
let calendarDate = calendarImplForID(calendar).isoToDate(nextIsoDate, { daysInMonth: true });
while (calendarDate.day !== calendarDate.daysInMonth) {
previousIsoDate = nextIsoDate;
calendarDate = calendarImplForID(calendar).isoToDate(nextIsoDate, { daysInMonth: true });
try {
nextIsoDate = CalendarDateAdd(calendar, nextIsoDate, { days: 1 }, 'constrain');
} catch (e) {
// Date overflowed maximum range => consider this the last day
break;
}
}
return previousIsoDate;
Copy link
Collaborator

Choose a reason for hiding this comment

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

The previous approach for jumping to the last day of the month (jump to day 1, add 1 month, subtract 1 day) was chosen mainly because it made the fewest calls into user code. Since we dropped user-defined calendars, we will always have zero calls into user code, so I'm not opposed to rewriting this to be more efficient.

But, daysInMonth is the count of days in the month, not the day number of the last day of the month so although this rewrite is correct for all currently supported calendars, it'll break when ICU adds a calendar that skips days (which has been a plan for some time now.)

You could add days one at a time until the month number changes? or just add the number of days in the month minus 1, I think?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm also not (yet) convinced about stopping the loop early when we get to the end of the range. Although it would allow Temporal.PlainYearMonth.from('+275760-09').subtract({ months: 1 }) which previously wouldn't work, it would also allow oddities like Temporal.PlainYearMonth.from('+275760-09').subtract({ weeks: 2 }) resulting in +275760-08.

Copy link
Collaborator

Choose a reason for hiding this comment

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

One thing we could do to not error out on the end of the range, is just skip the second addition if the weeks and days are zero.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed in cf79f59 to not assume there are no skipped days in the month.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Although it would allow Temporal.PlainYearMonth.from('+275760-09').subtract({ months: 1 }) which previously wouldn't work, it would also allow oddities like Temporal.PlainYearMonth.from('+275760-09').subtract({ weeks: 2 }) resulting in +275760-08.

If we don't stop the loop early, we get Temporal.PlainYearMonth.from('+275760-09').subtract({ weeks: 2 }) throwing because 275760-09-16 is out of range. I find that just as surprising as the result being 275760-08. I'm keeping it as it is for now, but could be convinced otherwise.

Comment on lines +598 to +612
<emu-clause id="sec-temporal-nonisodatelastdayofmonth" type="implementation-defined abstract operation">
<h1>
NonISODateLastDayOfMonth (
_calendar_: a calendar type that is not *"iso8601"*,
_isoYear_: an integer,
_isoMonth_: an integer between 1 and 12 (inclusive),
): an ISO Date Record
</h1>
<dl class="header">
<dt>description</dt>
<dd>
The operation performs implementation-defined processing to determine the last day of the month denoted by _isoDate_. It returns an ISO Date Record with the same year and month as _isoYear_ and _isoMonth_, and the day set to the last day of _isoMonth_ in _isoYear_ according to the years, months and weeks reckoning of _calendar_.
</dd>
</dl>
</emu-clause>
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it'd be better to specify CalendarDateLastDayOfMonth in terms of existing implementation-defined operations if possible, then we wouldn't have to have another implementation-defined operation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The problem is that if we specify it in terms of NonISODateAdd, there's no way to "catch" the exception if the result of the addition is outside the representable range and we want to back off to the last representable day. (Or is there a way to catch exceptions in spec language?)

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.

Surprising behavior with PlainYearMonth subtraction and constraining

2 participants