commit 37099145c6fbabf605808a45ce1cec792d7b25c9
parent b2239a2304b0cb1ce21718cd19095b5a5971fa6f
Author: André Bargull <andre.bargull@gmail.com>
Date: Fri, 12 Dec 2025 10:54:20 +0000
Bug 2005482: Constrain day before calling CreateDateFromCodes with overflow=reject for Hebrew calendar. r=spidermonkey-reviewers,dminor
Constrain the input `day` before calling `CreateDateFromCodes` when
`overflow=reject` to avoid incorrectly rejecting dates. When the day was
constrained, handle `overflow=reject` through an explicit check using
`CalendarDaysInMonth`.
This case is specific to the Hebrew calendar and can't happen for Chinese/Dangi
calendars, because all Chinese/Dangi months have the same number of minimum resp.
maximum days per month.
Differential Revision: https://phabricator.services.mozilla.com/D275957
Diffstat:
2 files changed, 97 insertions(+), 1 deletion(-)
diff --git a/js/src/builtin/temporal/Calendar.cpp b/js/src/builtin/temporal/Calendar.cpp
@@ -1338,10 +1338,25 @@ static UniqueICU4XDate CreateDateFrom(JSContext* cx, CalendarId calendarId,
MOZ_ASSERT(1 <= month && month <= 13);
+ // Constrain |day| when overflow is "reject" to avoid rejecting too large
+ // day values in CreateDateFromCodes.
+ //
+ // For example when month = 10 and day = 30 and the input year is a leap
+ // year. We first try month code "M10", but since "M10" can have at most
+ // 29 days, we need to constrain the days value before calling
+ // CreateDateFromCodes.
+ int32_t constrainedDay = day;
+ if (overflow == TemporalOverflow::Reject) {
+ constexpr auto daysInMonth = CalendarDaysInMonth(CalendarId::Hebrew);
+ if (day > daysInMonth.first && day <= daysInMonth.second) {
+ constrainedDay = daysInMonth.first;
+ }
+ }
+
// Create date with month number replaced by month-code.
auto monthCode = MonthCode{std::min(month, 12)};
auto date = CreateDateFromCodes(cx, calendarId, calendar, eraYear,
- monthCode, day, overflow);
+ monthCode, constrainedDay, overflow);
if (!date) {
return nullptr;
}
@@ -1350,6 +1365,18 @@ static UniqueICU4XDate CreateDateFrom(JSContext* cx, CalendarId calendarId,
// changes are necessary and we can directly return |date|.
int32_t ordinal = OrdinalMonth(date.get());
if (ordinal == month) {
+ // If |day| was constrained, check if the actual input days value
+ // exceeds the number of days in the resolved month.
+ if (constrainedDay < day) {
+ MOZ_ASSERT(overflow == TemporalOverflow::Reject);
+
+ if (day > CalendarDaysInMonth(calendarId, monthCode).second) {
+ ReportCalendarFieldOverflow(cx, "day", day);
+ return nullptr;
+ }
+ return CreateDateFromCodes(cx, calendarId, calendar, eraYear,
+ monthCode, day, overflow);
+ }
return date;
}
diff --git a/js/src/tests/non262/Temporal/PlainDate/hebrew-from-ordinal-month.js b/js/src/tests/non262/Temporal/PlainDate/hebrew-from-ordinal-month.js
@@ -0,0 +1,69 @@
+// |reftest| skip-if(!this.hasOwnProperty("Temporal"))
+
+const calendar = "hebrew";
+
+function assertDate(actual, {year, month, monthCode, day}) {
+ assertEq(actual.year, year);
+ assertEq(actual.month, month);
+ assertEq(actual.monthCode, monthCode);
+ assertEq(actual.day, day);
+}
+
+// 5783 is a common year.
+const commonYear = 5783;
+
+// 5784 is a leap year.
+const leapYear = 5784;
+
+// Test common and leap years.
+for (let year of [commonYear, leapYear]) {
+ let firstDayOfYear = Temporal.PlainDate.from({calendar, year, monthCode: "M01", day: 1});
+ assertEq(firstDayOfYear.inLeapYear, (year === leapYear));
+
+ let monthsInYear = firstDayOfYear.monthsInYear;
+ assertEq(monthsInYear, 12 + firstDayOfYear.inLeapYear);
+
+ // Test for each month in the year.
+ for (let month = 1; month <= monthsInYear; ++month) {
+ let firstDayOfMonthFromOrdinalMonth = Temporal.PlainDate.from({calendar, year, month, day: 1});
+ let monthCode = firstDayOfMonthFromOrdinalMonth.monthCode;
+ assertDate(firstDayOfMonthFromOrdinalMonth, {year, month, monthCode, day: 1});
+
+ let firstDayOfMonthFromMonthCode = Temporal.PlainDate.from({calendar, year, monthCode, day: 1});
+ assertDate(firstDayOfMonthFromMonthCode, {year, month, monthCode, day: 1});
+
+ // 29-30 days for each month.
+ let daysInMonth = firstDayOfMonthFromOrdinalMonth.daysInMonth;
+ assertEq(29 <= daysInMonth && daysInMonth <= 30 , true);
+
+ // Test for each day of the month.
+ for (let day = 2; day <= daysInMonth; ++day) {
+ for (let overflow of ["constrain", "reject"]) {
+ let fromOrdinalMonth = Temporal.PlainDate.from({calendar, year, month, day}, {overflow});
+ assertDate(fromOrdinalMonth, {year, month, monthCode, day});
+
+ let fromMonthCode = Temporal.PlainDate.from({calendar, year, monthCode, day}, {overflow});
+ assertDate(fromMonthCode, {year, month, monthCode, day});
+ }
+ }
+
+ // Test too large day values.
+ for (let day = daysInMonth + 1; day <= daysInMonth + 4; ++day) {
+ let fromOrdinalMonth = Temporal.PlainDate.from({calendar, year, month, day}, {overflow: "constrain"});
+ assertDate(fromOrdinalMonth, {year, month, monthCode, day: daysInMonth});
+
+ let fromMonthCode = Temporal.PlainDate.from({calendar, year, monthCode, day}, {overflow: "constrain"});
+ assertDate(fromMonthCode, {year, month, monthCode, day: daysInMonth});
+
+ assertThrowsInstanceOf(() => {
+ Temporal.PlainDate.from({calendar, year, month, day}, {overflow: "reject"});
+ }, RangeError);
+ assertThrowsInstanceOf(() => {
+ Temporal.PlainDate.from({calendar, year, monthCode, day}, {overflow: "reject"});
+ }, RangeError);
+ }
+ }
+}
+
+if (typeof reportCompare === "function")
+ reportCompare(true, true);