[JSC][Temporal] Implement ISODateToFields and CalendarMergeFields as the spec's own operations https://bugs.webkit.org/show_bug.cgi?id=321189 rdar://184247766 Reviewed by Yusuke Suzuki. Add isoDateToFields and calendarMergeFields, plus the CalendarFieldKeysPresent, CalendarFieldKeysToIgnore and NonISOFieldKeysToIgnore operations they rest on. Every .with() entry point previously hand-fused those steps with its own field-priority rules, PlainMonthDay's in 44 lines, and the MergeMode enum they needed is gone. differenceYearMonth's two hand-rolled copies of ISODateToFields(~year-month~) are converted too. CalendarFieldKeysToIgnore is what makes a year-only change correct on a lunisolar calendar: month and monthCode encode one field, so setting either must discard the other. Carrying the ordinal across a year change picks the wrong month once a leap month precedes it — chinese M05 is ordinal 5 in 2018, 6 in 2020. This also fixes PlainYearMonth.prototype.add/subtract shifting the month by a constant -2 for "buddhist", "roc" and "japanese" when the receiver's ISO year was in roughly 1..1582, confirmed pre-existing by A/B against a pre-conversion build. Root cause never established. Add calendarDayOfWeek, calendarWeekOfYear and calendarYearOfWeek so all fifteen CalendarISOToDate fields reach the getters through one accessor family; the spec's "undefined for calendars with no well-defined week calendar system" rule was re-derived at six getter sites and is now one std::optional. Remove the eight forwarders this leaves unreferenced. Inline TemporalPlainTime::with into its prototype, and restore ZonedDateTime.prototype.with to spec step order: steps 9, 10-16 and 18 ran after 19-22, so ISODateToFields could throw only after the options getters had run. Tests: JSTests/stress/temporal-calendar-merge-fields-with.js JSTests/stress/temporal-isodatetofields-call-sites.js Source/JavaScriptCore/API/tests/TemporalCoreTest.cpp Canonical link: https://commits.webkit.org/318785@main
diff --git a/JSTests/stress/temporal-calendar-merge-fields-with.js b/JSTests/stress/temporal-calendar-merge-fields-with.js new file mode 100644 index 0000000..9938c64 --- /dev/null +++ b/JSTests/stress/temporal-calendar-merge-fields-with.js
@@ -0,0 +1,50 @@ +//@ requireOptions("--useTemporal=1") + +function shouldThrow(fn, type, message) { + let err; + try { fn(); } catch (e) { err = e; } + if (!(err instanceof type)) + throw new Error(`Expected ${type.name} but got ${err}`); + if (message !== undefined && err.message !== message) + throw new Error(`Expected message "${message}" but got "${err.message}"`); +} + +function shouldBe(actual, expected) { + if (actual !== expected) + throw new Error(`expected ${JSON.stringify(expected)} but got ${JSON.stringify(actual)}`); +} + +// --- PlainDate.prototype.with --- + +shouldBe(Temporal.PlainDate.from("2020-06-15").with({ day: 20 }).toString(), "2020-06-20"); +shouldBe(Temporal.PlainDate.from("2020-06-15").with({ month: 3 }).toString(), "2020-03-15"); +shouldBe(Temporal.PlainDate.from("2020-06-15").with({ year: 2021 }).toString(), "2021-06-15"); + +shouldBe(Temporal.PlainDate.from({ year: 2020, month: 5, day: 1, calendar: "hebrew" }).with({ day: 10 }).toString(), "-001741-12-28[u-ca=hebrew]"); +shouldBe(Temporal.PlainDate.from({ year: 2020, month: 5, day: 1, calendar: "hebrew" }).with({ year: 2021 }).toString(), "-001739-01-07[u-ca=hebrew]"); + +// Chinese (lunisolar): year changes with month/monthCode absent from the partial — the +// case the removed lunisolarYearChange special case targeted (month must fall back via +// monthCode, not a raw ordinal, since leap-month insertion can shift the mapping). +shouldBe(Temporal.PlainDate.from({ year: 2020, month: 5, day: 1, calendar: "chinese" }).with({ year: 2023 }).toString(), "2023-05-19[u-ca=chinese]"); +shouldBe(Temporal.PlainDate.from({ year: 2020, month: 5, day: 1, calendar: "chinese" }).with({ day: 10 }).toString(), "2020-06-01[u-ca=chinese]"); + +shouldBe(Temporal.PlainDate.from({ era: "reiwa", eraYear: 3, month: 5, day: 1, calendar: "japanese" }).with({ month: 8 }).toString(), "2021-08-01[u-ca=japanese]"); + +// --- PlainYearMonth.prototype.with --- + +shouldBe(Temporal.PlainYearMonth.from("2020-06").with({ month: 3 }).toString(), "2020-03"); +shouldBe(Temporal.PlainYearMonth.from({ year: 2020, month: 5, calendar: "hebrew" }).with({ month: 2 }).toString(), "-001741-09-21[u-ca=hebrew]"); +shouldBe(Temporal.PlainYearMonth.from({ year: 2020, month: 5, calendar: "chinese" }).with({ year: 2023 }).toString(), "2023-05-19[u-ca=chinese]"); + +// --- PlainMonthDay.prototype.with --- + +shouldBe(Temporal.PlainMonthDay.from({ month: 6, day: 15 }).with({ day: 20 }).toString(), "06-20"); +shouldBe(Temporal.PlainMonthDay.from({ month: 6, day: 15 }).with({ month: 3 }).toString(), "03-15"); +shouldBe(Temporal.PlainMonthDay.from({ monthCode: "M05", day: 1, calendar: "hebrew" }).with({ day: 10 }).toString(), "1972-01-26[u-ca=hebrew]"); +shouldBe(Temporal.PlainMonthDay.from({ monthCode: "M05", day: 1, calendar: "hebrew" }).with({ month: 2, year: 2020 }).toString(), "1972-10-09[u-ca=hebrew]"); + +// Non-ISO month given without year (or era+eraYear): now deferred to +// nonISOResolveFields's own "year property must be present" check instead of a +// PlainMonthDay.prototype.with-specific message — same TypeError kind either way. +shouldThrow(() => Temporal.PlainMonthDay.from({ monthCode: "M05", day: 1, calendar: "hebrew" }).with({ month: 2 }), TypeError, "year property must be present");
diff --git a/JSTests/stress/temporal-isodatetofields-call-sites.js b/JSTests/stress/temporal-isodatetofields-call-sites.js new file mode 100644 index 0000000..b1e85f2 --- /dev/null +++ b/JSTests/stress/temporal-isodatetofields-call-sites.js
@@ -0,0 +1,110 @@ +//@ requireOptions("--useTemporal=1") + +function shouldBe(actual, expected, msg) { + if (String(actual) !== String(expected)) + throw new Error(`${msg}: expected ${JSON.stringify(String(expected))} but got ${JSON.stringify(String(actual))}`); +} + +function shouldThrow(fn, type, msg) { + let err; + try { fn(); } catch (e) { err = e; } + if (!(err instanceof type)) + throw new Error(`${msg}: expected ${type.name} but got ${err}`); +} + +const ym = (cal) => Temporal.PlainYearMonth.from({ year: 2021, month: 5, calendar: cal }); + +// --- AddDurationToYearMonth: ISODateToFields(~year-month~) at steps 9 and 13 --- +// The receiver's era/eraYear are NOT part of ISODateToFields' output; only monthCode and year are, +// so an era-bearing calendar must round-trip through the arithmetic year alone. +shouldBe(ym("iso8601").add({ months: 1 }), "2021-06", "iso add P1M"); +shouldBe(ym("iso8601").add({ months: 13 }), "2022-06", "iso add P13M"); +shouldBe(ym("iso8601").subtract({ months: 1 }), "2021-04", "iso sub P1M"); + +shouldBe(ym("japanese").add({ months: 1 }).toString(), "2021-06-01[u-ca=japanese]", "japanese add P1M"); +shouldBe(ym("japanese").add({ months: 13 }).toString(), "2022-06-01[u-ca=japanese]", "japanese add P13M"); +shouldBe(ym("japanese").add({ years: 1 }).toString(), "2022-05-01[u-ca=japanese]", "japanese add P1Y"); +shouldBe(ym("japanese").subtract({ months: 1 }).toString(), "2021-04-01[u-ca=japanese]", "japanese sub P1M"); +shouldBe(ym("gregory").add({ months: 1 }).toString(), "2021-06-01[u-ca=gregory]", "gregory add P1M"); +shouldBe(ym("roc").add({ months: 1 }).toString(), "3932-06-01[u-ca=roc]", "roc add P1M"); +shouldBe(ym("roc").add({ years: 1 }).toString(), "3933-05-01[u-ca=roc]", "roc add P1Y"); + +// Lunisolar: leap-month insertion means the ordinal month cannot be carried across years, so +// ISODateToFields must hand CalendarYearMonthFromFields a monthCode. +shouldBe(ym("hebrew").add({ months: 1 }).toString(), "-001739-02-06[u-ca=hebrew]", "hebrew add P1M"); +shouldBe(ym("hebrew").add({ months: 13 }).toString(), "-001738-01-25[u-ca=hebrew]", "hebrew add P13M"); +shouldBe(ym("hebrew").subtract({ months: 1 }).toString(), "-001740-12-09[u-ca=hebrew]", "hebrew sub P1M"); +shouldBe(ym("chinese").add({ months: 1 }).toString(), "2021-07-10[u-ca=chinese]", "chinese add P1M"); +shouldBe(ym("chinese").add({ months: 13 }).toString(), "2022-06-29[u-ca=chinese]", "chinese add P13M"); +shouldBe(ym("chinese").subtract({ months: 1 }).toString(), "2021-05-12[u-ca=chinese]", "chinese sub P1M"); + +// An era-bearing receiver built FROM era+eraYear must add identically to one built from year. +shouldBe(Temporal.PlainYearMonth.from({ era: "reiwa", eraYear: 3, month: 5, calendar: "japanese" }) + .add({ months: 1 }).toString(), "2021-06-01[u-ca=japanese]", "japanese add P1M from era"); + +// ISO takes the pure isoDateAdd overload so the extreme boundary years do not get ICU-clamped. +shouldBe(Temporal.PlainYearMonth.from("+275760-08").add({ months: 1 }), "+275760-09", "add to max boundary"); +shouldThrow(() => Temporal.PlainYearMonth.from("+275760-09").add({ months: 1 }), RangeError, "add past max"); +shouldThrow(() => Temporal.PlainYearMonth.from("-271821-05").subtract({ months: 1 }), RangeError, "sub past min"); + +for (const [cal, year, isoPrefix] of [["buddhist", 2021, "1478"], ["roc", -433, "1478"], ["japanese", 1478, "1478"]]) { + const base = Temporal.PlainYearMonth.from({ year, month: 5, calendar: cal }); + shouldBe(base.toString(), `${isoPrefix}-05-01[u-ca=${cal}]`, `${cal} base at ISO ${isoPrefix}`); + shouldBe(base.add({ months: 0 }).month, 5, `${cal}: add P0M must be identity`); + shouldBe(base.add({ months: 1 }).month, 6, `${cal}: add P1M`); + shouldBe(base.subtract({ months: 1 }).month, 4, `${cal}: sub P1M`); +} +// Outside the affected ISO-year range the same calendars are correct, which pins the boundary. +shouldBe(Temporal.PlainYearMonth.from({ year: 2200, month: 5, calendar: "buddhist" }) + .add({ months: 0 }).toString(), "1657-05-01[u-ca=buddhist]", "buddhist add P0M above range"); +shouldBe(Temporal.PlainYearMonth.from({ year: 543, month: 5, calendar: "buddhist" }) + .add({ months: 0 }).toString(), "0000-05-01[u-ca=buddhist]", "buddhist add P0M below range"); +// gregory at the same ISO year is unaffected, isolating the proleptic-Gregorian era path. +shouldBe(Temporal.PlainYearMonth.from({ year: 1478, month: 5, calendar: "gregory" }) + .add({ months: 0 }).month, 5, "gregory add P0M at ISO 1478"); + +// --- PlainYearMonth.toPlainDate: ISODateToFields(~year-month~) then CalendarMergeFields(«day») --- +shouldBe(ym("iso8601").toPlainDate({ day: 15 }), "2021-05-15", "iso toPlainDate day=15"); +shouldBe(ym("iso8601").toPlainDate({ day: 31 }), "2021-05-31", "iso toPlainDate day=31"); +shouldBe(ym("japanese").toPlainDate({ day: 1 }).toString(), "2021-05-01[u-ca=japanese]", "japanese toPlainDate day=1"); +shouldBe(ym("japanese").toPlainDate({ day: 31 }).toString(), "2021-05-31[u-ca=japanese]", "japanese toPlainDate day=31"); +shouldBe(ym("hebrew").toPlainDate({ day: 1 }).toString(), "-001739-01-07[u-ca=hebrew]", "hebrew toPlainDate day=1"); +shouldBe(ym("hebrew").toPlainDate({ day: 31 }).toString(), "-001739-02-05[u-ca=hebrew]", "hebrew toPlainDate day=31"); +shouldBe(ym("chinese").toPlainDate({ day: 15 }).toString(), "2021-06-24[u-ca=chinese]", "chinese toPlainDate day=15"); +shouldBe(ym("chinese").toPlainDate({ day: 31 }).toString(), "2021-07-09[u-ca=chinese]", "chinese toPlainDate day=31"); + +// --- ToTemporalYearMonth step 12 / PlainDate.toPlainYearMonth --- +for (const [cal, expected] of [["japanese", "2021-05-01[u-ca=japanese]"], ["hebrew", "2021-05-12[u-ca=hebrew]"], + ["chinese", "2021-05-12[u-ca=chinese]"]]) { + shouldBe(Temporal.PlainYearMonth.from(`2021-05-17[u-ca=${cal}]`).toString(), expected, `${cal} YM.from string`); + shouldBe(Temporal.PlainDate.from(`2021-05-17[u-ca=${cal}]`).toPlainYearMonth().toString(), expected, `${cal} PD.toPlainYearMonth`); +} +shouldBe(Temporal.PlainYearMonth.from("2021-05-17"), "2021-05", "iso YM.from string"); +// The reference day is canonical (1 for ISO), which is why step 14 forces ~constrain~. +shouldBe(Temporal.PlainYearMonth.from("2020-05-23[u-ca=chinese]").toString(), "2020-05-23[u-ca=chinese]", "chinese YM.from leap-month year"); +shouldBe(Temporal.PlainYearMonth.from("2022-03-05[u-ca=hebrew]").toString(), "2022-03-04[u-ca=hebrew]", "hebrew YM.from leap-month year"); + +// --- ToTemporalMonthDay step 13 / PlainDate.toPlainMonthDay --- +shouldBe(Temporal.PlainMonthDay.from("2021-05-17"), "05-17", "iso MD.from string"); +shouldBe(Temporal.PlainMonthDay.from("2024-02-29"), "02-29", "iso MD.from Feb 29"); +for (const [cal, expected] of [["japanese", "1972-05-17[u-ca=japanese]"], ["hebrew", "1972-05-19[u-ca=hebrew]"], + ["chinese", "1972-05-18[u-ca=chinese]"]]) { + shouldBe(Temporal.PlainMonthDay.from(`2021-05-17[u-ca=${cal}]`).toString(), expected, `${cal} MD.from string`); + shouldBe(Temporal.PlainDate.from(`2021-05-17[u-ca=${cal}]`).toPlainMonthDay().toString(), expected, `${cal} PD.toPlainMonthDay`); +} + +// --- getter/resolution agreement --- +for (const cal of ["iso8601", "gregory", "hebrew", "chinese", "dangi", "islamic-civil", + "islamic-tbla", "islamic-umalqura", "japanese", "buddhist", "roc", "coptic", "ethiopic", + "ethioaa", "persian", "indian"]) { + for (const iso of ["2020-05-01", "2020-06-21", "1978-02-28", "2024-02-29", "2023-06-18"]) { + const d = Temporal.PlainDate.from(iso).withCalendar(cal); + const tag = `${cal} ${iso}`; + shouldBe(d.with({ day: d.day }).equals(d), true, `${tag}: with({day: d.day}) is identity`); + shouldBe(d.with({ year: d.year }).equals(d), true, `${tag}: with({year: d.year}) is identity`); + shouldBe(d.with({ monthCode: d.monthCode }).equals(d), true, `${tag}: with({monthCode}) is identity`); + shouldBe(d.with({ month: d.month }).equals(d), true, `${tag}: with({month: d.month}) is identity`); + if (d.era !== undefined) + shouldBe(d.with({ era: d.era, eraYear: d.eraYear }).equals(d), true, `${tag}: with({era, eraYear}) is identity`); + } +}
diff --git a/Source/JavaScriptCore/API/tests/TemporalCoreTest.cpp b/Source/JavaScriptCore/API/tests/TemporalCoreTest.cpp index 96d16e5..34cc7bf 100644 --- a/Source/JavaScriptCore/API/tests/TemporalCoreTest.cpp +++ b/Source/JavaScriptCore/API/tests/TemporalCoreTest.cpp
@@ -2600,10 +2600,11 @@ auto rWith2 = plainYearMonthWith(id("iso8601"_s), { 2025, 3, 1 }, partialMonth, TemporalOverflow::Constrain); TCHECK_TRUE(rWith2.has_value() && rWith2->isoDate.year() == 2025 && rWith2->isoDate.month() == 7, "plainYearMonthWith: override month -> 2025-07"); - // empty partial fields: ISO falls back year+monthCode from current, succeeds + // empty partial fields: ISO falls back year+monthCode from current, succeeds. Unreachable from + // JS — PrepareCalendarFields(..., ~partial~) throws before .with() ever gets here. CalendarFieldsIn emptyPartial; auto rWithEmpty = plainYearMonthWith(id("iso8601"_s), { 2025, 3, 1 }, emptyPartial, TemporalOverflow::Constrain); - TCHECK_TRUE(!rWithEmpty.has_value(), "plainYearMonthWith: empty partial -> TypeError (temporal_rs: fields.is_empty())"); + TCHECK_TRUE(rWithEmpty.has_value() && rWithEmpty->isoDate.year() == 2025 && rWithEmpty->isoDate.month() == 3, "plainYearMonthWith: empty partial -> receiver 2025-03"); f = { }; f.year = 275761; // one past maxYear @@ -2705,6 +2706,87 @@ TCHECK_TRUE(!rDW6.has_value() && rDW6.error().kind == TemporalErrorKind::RangeError, "plainDateWith: inconsistent year+era+eraYear -> RangeError"); } +static void testCalendarMergeFieldsWith() +{ + using MC = ParsedMonthCode; + auto id = calendarIDFromString; + + // chinese (lunisolar): year-only change with month/monthCode absent must fall back via + // monthCode, not a raw ordinal, since 2023's leap-month layout differs from 2020's. + { + CalendarFieldsIn partial; + partial.year = 2023; + auto r = plainDateWith(id("chinese"_s), { 2020, 5, 23 }, partial, TemporalOverflow::Constrain); + TCHECK_TRUE(r.has_value() && r->isoDate.year() == 2023 && r->isoDate.month() == 5 && r->isoDate.day() == 19, "plainDateWith: chinese year-only change through leap-month year -> 2023-05-19"); + } + { + CalendarFieldsIn partial; + partial.day = 10; + auto r = plainDateWith(id("chinese"_s), { 2020, 5, 23 }, partial, TemporalOverflow::Constrain); + TCHECK_TRUE(r.has_value() && r->isoDate.year() == 2020 && r->isoDate.month() == 6 && r->isoDate.day() == 1, "plainDateWith: chinese day-only change -> 2020-06-01"); + } + + // Same chinese leap-month case via plainYearMonthWith. + { + CalendarFieldsIn partial; + partial.year = 2023; + auto r = plainYearMonthWith(id("chinese"_s), { 2020, 5, 23 }, partial, TemporalOverflow::Constrain); + TCHECK_TRUE(r.has_value() && r->isoDate.year() == 2023 && r->isoDate.month() == 5 && r->isoDate.day() == 19, "plainYearMonthWith: chinese year-only change through leap-month year -> 2023-05-19"); + } + { + // PlainYearMonth's stored ISO date always has day=1. Showa 64 Jan 1 = 1989-01-01 + // (Heisei starts 1989-01-08); .with({month: 6}) re-derives era into Heisei. + CalendarFieldsIn partial; + partial.month = 6; + auto r = plainYearMonthWith(id("japanese"_s), { 1989, 1, 1 }, partial, TemporalOverflow::Constrain); + TCHECK_TRUE(r.has_value() && r->isoDate.year() == 1989 && r->isoDate.month() == 6, "plainYearMonthWith: japanese month change re-derives era -> 1989-06"); + } + + // plainMonthDayWith: ISO day/month-only changes. + { + CalendarFieldsIn base; + base.month = 6; + base.day = 15; + auto baseResolved = monthDayFromFields(id("iso8601"_s), base, TemporalOverflow::Constrain); + TCHECK_TRUE(baseResolved.has_value(), "plainMonthDayWith setup: iso base 06-15"); + + CalendarFieldsIn partialDay; + partialDay.day = 20; + auto r1 = plainMonthDayWith(id("iso8601"_s), baseResolved->isoDate, partialDay, TemporalOverflow::Constrain); + TCHECK_TRUE(r1.has_value() && r1->isoDate.month() == 6 && r1->isoDate.day() == 20, "plainMonthDayWith: iso day-only -> 06-20"); + + CalendarFieldsIn partialMonth; + partialMonth.month = 3; + auto r2 = plainMonthDayWith(id("iso8601"_s), baseResolved->isoDate, partialMonth, TemporalOverflow::Constrain); + TCHECK_TRUE(r2.has_value() && r2->isoDate.month() == 3 && r2->isoDate.day() == 15, "plainMonthDayWith: iso month-only -> 03-15"); + } + + // plainMonthDayWith: hebrew — month-without-year now throws via nonISOResolveFields. + { + CalendarFieldsIn base; + base.monthCode = MC { 5, false }; + base.day = 1; + auto baseResolved = monthDayFromFields(id("hebrew"_s), base, TemporalOverflow::Constrain); + TCHECK_TRUE(baseResolved.has_value() && baseResolved->isoDate.year() == 1972 && baseResolved->isoDate.month() == 1 && baseResolved->isoDate.day() == 17, "plainMonthDayWith setup: hebrew M05 day=1 -> 1972-01-17"); + + CalendarFieldsIn partialDay; + partialDay.day = 10; + auto r1 = plainMonthDayWith(id("hebrew"_s), baseResolved->isoDate, partialDay, TemporalOverflow::Constrain); + TCHECK_TRUE(r1.has_value() && r1->isoDate.year() == 1972 && r1->isoDate.month() == 1 && r1->isoDate.day() == 26, "plainMonthDayWith: hebrew day-only -> 1972-01-26"); + + CalendarFieldsIn partialMonthNoYear; + partialMonthNoYear.month = 2; + auto r2 = plainMonthDayWith(id("hebrew"_s), baseResolved->isoDate, partialMonthNoYear, TemporalOverflow::Constrain); + TCHECK_TRUE(!r2.has_value() && r2.error().kind == TemporalErrorKind::TypeError, "plainMonthDayWith: hebrew month without year -> TypeError"); + + CalendarFieldsIn partialMonthYear; + partialMonthYear.month = 2; + partialMonthYear.year = 2020; + auto r3 = plainMonthDayWith(id("hebrew"_s), baseResolved->isoDate, partialMonthYear, TemporalOverflow::Constrain); + TCHECK_TRUE(r3.has_value() && r3->isoDate.year() == 1972 && r3->isoDate.month() == 10 && r3->isoDate.day() == 9, "plainMonthDayWith: hebrew month+year -> 1972-10-09"); + } +} + static void testNonISOCalendarDateToISO() { using MC = ParsedMonthCode; @@ -3867,6 +3949,7 @@ testNonISOCalendarDateToISO(); // nonISOCalendarDateToISO: era, monthCode, overflow, ROC, Japanese, Buddhist, Coptic, Ethiopic testIsBuiltinCalendar(); // Canonical Temporal calendar set + legacy aliases testCalendarFieldsFunctions(); // yearMonthFromFields, monthDayFromFields, differenceYearMonth, plainYearMonthAdd, etc. + testCalendarMergeFieldsWith(); // calendarMergeFields: chinese leap-month, japanese era-suppress, hebrew MonthDay // parseISODateTime testParseInstantString();
diff --git a/Source/JavaScriptCore/runtime/TemporalPlainDate.h b/Source/JavaScriptCore/runtime/TemporalPlainDate.h index 32d109c..ab8a1f780 100644 --- a/Source/JavaScriptCore/runtime/TemporalPlainDate.h +++ b/Source/JavaScriptCore/runtime/TemporalPlainDate.h
@@ -63,10 +63,6 @@ #undef JSC_DEFINE_TEMPORAL_PLAIN_DATE_FIELD String monthCode() const { return ISO8601::monthCode(m_plainDate.month()); } - uint8_t dayOfWeek() const { return ISO8601::dayOfWeek(m_plainDate); } - uint16_t dayOfYear() const { return ISO8601::dayOfYear(m_plainDate); } - uint8_t weekOfYear() const { return ISO8601::weekOfYear(m_plainDate); } - int32_t yearOfWeek() const { return ISO8601::yearOfWeek(m_plainDate); } String toString() const;
diff --git a/Source/JavaScriptCore/runtime/TemporalPlainDatePrototype.cpp b/Source/JavaScriptCore/runtime/TemporalPlainDatePrototype.cpp index 1182fe6..811cc1c 100644 --- a/Source/JavaScriptCore/runtime/TemporalPlainDatePrototype.cpp +++ b/Source/JavaScriptCore/runtime/TemporalPlainDatePrototype.cpp
@@ -297,8 +297,7 @@ TemporalOverflow overflow = toTemporalOverflow(globalObject, callFrame->argument(1)); RETURN_IF_EXCEPTION(scope, { }); - // Steps 5+7+10: ISODateToFields + CalendarMergeFields + CalendarDateFromFields — fused into - // plainDateWith (which has its own ISO fast path, so this is calendar-agnostic here). + // Steps 5+7+10: ISODateToFields + CalendarMergeFields + CalendarDateFromFields, in plainDateWith. auto resolved = TemporalCore::plainDateWith(calendarId, plainDate->plainDate(), partialFields, overflow); if (!resolved) [[unlikely]] { if (resolved.error().kind == TemporalErrorKind::TypeError) @@ -681,7 +680,7 @@ if (!plainDate) [[unlikely]] return throwVMTypeError(globalObject, scope, "Temporal.PlainDate.prototype.dayOfWeek called on value that's not a PlainDate"_s); - return JSValue::encode(jsNumber(plainDate->dayOfWeek())); + return JSValue::encode(jsNumber(TemporalCore::calendarDayOfWeek(plainDate->calendarID(), plainDate->plainDate()))); } // https://tc39.es/proposal-temporal/#sec-get-temporal.plaindate.prototype.dayofyear @@ -710,9 +709,10 @@ if (!plainDate) [[unlikely]] return throwVMTypeError(globalObject, scope, "Temporal.PlainDate.prototype.weekOfYear called on value that's not a PlainDate"_s); - if (plainDate->calendarID() != iso8601CalendarID()) + auto week = TemporalCore::calendarWeekOfYear(plainDate->calendarID(), plainDate->plainDate()); + if (!week) return JSValue::encode(jsUndefined()); - return JSValue::encode(jsNumber(plainDate->weekOfYear())); + return JSValue::encode(jsNumber(*week)); } // https://tc39.es/proposal-temporal/#sec-get-temporal.plaindate.prototype.daysinweek @@ -725,7 +725,7 @@ if (!plainDate) [[unlikely]] return throwVMTypeError(globalObject, scope, "Temporal.PlainDate.prototype.daysInWeek called on value that's not a PlainDate"_s); - return JSValue::encode(jsNumber(7)); // ISO8601 calendar always returns 7. + return JSValue::encode(jsNumber(ISO8601::daysPerWeek)); } // https://tc39.es/proposal-temporal/#sec-get-temporal.plaindate.prototype.daysinmonth @@ -848,9 +848,10 @@ if (!plainDate) [[unlikely]] return throwVMTypeError(globalObject, scope, "Temporal.PlainDate.prototype.yearOfWeek called on value that's not a PlainDate"_s); - if (plainDate->calendarID() != iso8601CalendarID()) + auto yearOfWeek = TemporalCore::calendarYearOfWeek(plainDate->calendarID(), plainDate->plainDate()); + if (!yearOfWeek) return JSValue::encode(jsUndefined()); - return JSValue::encode(jsNumber(plainDate->yearOfWeek())); + return JSValue::encode(jsNumber(*yearOfWeek)); } // https://tc39.es/proposal-temporal/#sec-temporal.plaindate.prototype.withcalendar
diff --git a/Source/JavaScriptCore/runtime/TemporalPlainDateTime.h b/Source/JavaScriptCore/runtime/TemporalPlainDateTime.h index 10fa85f..fbefa95 100644 --- a/Source/JavaScriptCore/runtime/TemporalPlainDateTime.h +++ b/Source/JavaScriptCore/runtime/TemporalPlainDateTime.h
@@ -64,10 +64,6 @@ #undef JSC_DEFINE_TEMPORAL_PLAIN_TIME_FIELD String monthCode() const { return ISO8601::monthCode(m_plainDate.month()); } - uint8_t dayOfWeek() const { return ISO8601::dayOfWeek(m_plainDate); } - uint16_t dayOfYear() const { return ISO8601::dayOfYear(m_plainDate); } - uint8_t weekOfYear() const { return ISO8601::weekOfYear(m_plainDate); } - int32_t yearOfWeek() const { return ISO8601::yearOfWeek(m_plainDate); } template<DifferenceOperation> ISO8601::Duration differenceTemporalPlainDateTime(JSGlobalObject*, TemporalPlainDateTime*, JSValue optionsValue);
diff --git a/Source/JavaScriptCore/runtime/TemporalPlainDateTimePrototype.cpp b/Source/JavaScriptCore/runtime/TemporalPlainDateTimePrototype.cpp index 18fbafa..a2c1def 100644 --- a/Source/JavaScriptCore/runtime/TemporalPlainDateTimePrototype.cpp +++ b/Source/JavaScriptCore/runtime/TemporalPlainDateTimePrototype.cpp
@@ -266,31 +266,32 @@ TemporalOverflow overflow = toTemporalOverflow(globalObject, options); RETURN_IF_EXCEPTION(scope, { }); - // Steps 5, 13, 16: ISODateToFields + CalendarMergeFields + CalendarDateFromFields - // — fused into plainDateWith. - auto dateResult = TemporalCore::plainDateWith(calendarId, plainDateTime->plainDate(), partialDate, overflow); - if (!dateResult) [[unlikely]] { - if (dateResult.error().kind == TemporalErrorKind::TypeError) - throwTypeError(globalObject, scope, String(dateResult.error().message)); - else - throwRangeError(globalObject, scope, String(dateResult.error().message)); + // Step 5: Let fields be ISODateToFields(calendar, plainDateTime.[[ISODateTime]].[[ISODate]], ~date~). + auto dateFields = TemporalCore::isoDateToFields(calendarId, plainDateTime->plainDate(), TemporalCore::ResolveType::Date); + if (!dateFields) [[unlikely]] { + throwTemporalError(globalObject, scope, dateFields.error()); return { }; } + // Step 13: Set fields to CalendarMergeFields(calendar, fields, partialDateTime). + auto mergedDate = TemporalCore::calendarMergeFields(calendarId, *dateFields, partialDate); - // Steps 6-11, 16: time fields merged with this's current values, then RegulateTime. + // Steps 6-11, merged into step 13 inline: see CalendarFieldKey in CalendarFields.cpp. auto curTime = plainDateTime->plainTime(); - ISO8601::Duration timeDur { }; - timeDur.setField(TemporalUnit::Hour, partialTime.hour.value_or(curTime.hour())); - timeDur.setField(TemporalUnit::Minute, partialTime.minute.value_or(curTime.minute())); - timeDur.setField(TemporalUnit::Second, partialTime.second.value_or(curTime.second())); - timeDur.setField(TemporalUnit::Millisecond, partialTime.millisecond.value_or(curTime.millisecond())); - timeDur.setField(TemporalUnit::Microsecond, partialTime.microsecond.value_or(curTime.microsecond())); - timeDur.setField(TemporalUnit::Nanosecond, partialTime.nanosecond.value_or(curTime.nanosecond())); - auto newTime = TemporalPlainTime::regulateTime(globalObject, WTF::move(timeDur), overflow); + TemporalCore::TimeFieldsIn mergedTime { + partialTime.hour.value_or(curTime.hour()), + partialTime.minute.value_or(curTime.minute()), + partialTime.second.value_or(curTime.second()), + partialTime.millisecond.value_or(curTime.millisecond()), + partialTime.microsecond.value_or(curTime.microsecond()), + partialTime.nanosecond.value_or(curTime.nanosecond()), + }; + + // Step 16: result = ? InterpretTemporalDateTimeFields(calendar, fields, overflow). + auto pdt = interpretTemporalDateTimeFields(globalObject, calendarId, mergedDate, mergedTime, overflow); RETURN_IF_EXCEPTION(scope, { }); - // Step 17: CreateTemporalDateTime. - auto* withResult = createTemporalDateTime(globalObject, ISO8601::PlainDate(dateResult->isoDate), WTF::move(newTime), calendarId); + // Step 17: Return ? CreateTemporalDateTime(result, calendar). + auto* withResult = createTemporalDateTime(globalObject, ISO8601::PlainDate(pdt.date), ISO8601::PlainTime(pdt.time), calendarId); RETURN_IF_EXCEPTION(scope, { }); return JSValue::encode(withResult); } @@ -720,7 +721,7 @@ if (!plainDateTime) [[unlikely]] return throwVMTypeError(globalObject, scope, "Temporal.PlainDateTime.prototype.dayOfWeek called on value that's not a PlainDateTime"_s); - return JSValue::encode(jsNumber(plainDateTime->dayOfWeek())); + return JSValue::encode(jsNumber(TemporalCore::calendarDayOfWeek(plainDateTime->calendarID(), plainDateTime->plainDate()))); } // https://tc39.es/proposal-temporal/#sec-get-temporal.plaindatetime.prototype.dayofyear @@ -749,9 +750,10 @@ if (!plainDateTime) [[unlikely]] return throwVMTypeError(globalObject, scope, "Temporal.PlainDateTime.prototype.weekOfYear called on value that's not a PlainDateTime"_s); - if (plainDateTime->calendarID() != iso8601CalendarID()) + auto week = TemporalCore::calendarWeekOfYear(plainDateTime->calendarID(), plainDateTime->plainDate()); + if (!week) return JSValue::encode(jsUndefined()); - return JSValue::encode(jsNumber(plainDateTime->weekOfYear())); + return JSValue::encode(jsNumber(*week)); } // https://tc39.es/proposal-temporal/#sec-get-temporal.plaindatetime.prototype.daysinweek @@ -764,7 +766,7 @@ if (!plainDateTime) [[unlikely]] return throwVMTypeError(globalObject, scope, "Temporal.PlainDateTime.prototype.daysInWeek called on value that's not a PlainDateTime"_s); - return JSValue::encode(jsNumber(7)); // ISO8601 calendar always returns 7. + return JSValue::encode(jsNumber(ISO8601::daysPerWeek)); } // https://tc39.es/proposal-temporal/#sec-get-temporal.plaindatetime.prototype.daysinmonth @@ -913,9 +915,10 @@ if (!plainDateTime) [[unlikely]] return throwVMTypeError(globalObject, scope, "Temporal.PlainDateTime.prototype.yearOfWeek called on value that's not a PlainDateTime"_s); - if (plainDateTime->calendarID() != iso8601CalendarID()) + auto yearOfWeek = TemporalCore::calendarYearOfWeek(plainDateTime->calendarID(), plainDateTime->plainDate()); + if (!yearOfWeek) return JSValue::encode(jsUndefined()); - return JSValue::encode(jsNumber(plainDateTime->yearOfWeek())); + return JSValue::encode(jsNumber(*yearOfWeek)); } // https://tc39.es/proposal-temporal/#sec-get-temporal.plaindatetime.prototype.era
diff --git a/Source/JavaScriptCore/runtime/TemporalPlainMonthDayPrototype.cpp b/Source/JavaScriptCore/runtime/TemporalPlainMonthDayPrototype.cpp index 70a67b1..cb16a45 100644 --- a/Source/JavaScriptCore/runtime/TemporalPlainMonthDayPrototype.cpp +++ b/Source/JavaScriptCore/runtime/TemporalPlainMonthDayPrototype.cpp
@@ -201,50 +201,8 @@ TemporalOverflow overflow = toTemporalOverflow(globalObject, options); RETURN_IF_EXCEPTION(scope, { }); - // Steps 5+7: fields = ISODateToFields(calendar, plainMonthDay.[[ISODate]], ~month-day~); - // fields = CalendarMergeFields(calendar, fields, partialMonthDay). - // Fused inline: fill unset merged fields from the receiver's own monthCode/day. - TemporalCore::CalendarFieldsIn merged; - - // day from ISODateToFields (use calendar day for non-ISO). - uint8_t currentCalDay = static_cast<uint8_t>(monthDay->plainMonthDay().day()); - bool isISO = TemporalCore::calendarIsISO(calendarId); - if (!isISO) { - auto dayResult = TemporalCore::calendarDay(calendarId, monthDay->plainMonthDay().isoPlainDate()); - if (dayResult) - currentCalDay = *dayResult; - } - // User's day takes priority over the receiver's. - merged.day = partialFields.day.has_value() ? partialFields.day : std::optional<uint8_t>(currentCalDay); - - if (partialFields.month.has_value()) - merged.month = partialFields.month; - if (partialFields.monthCode) - merged.monthCode = partialFields.monthCode; - if (partialFields.year) - merged.year = partialFields.year; - if (partialFields.era) - merged.era = partialFields.era; - if (partialFields.eraYear) - merged.eraYear = partialFields.eraYear; - if (!partialFields.month.has_value() && !partialFields.monthCode) { - // Neither given — fall back to current monthCode (non-ISO) or numeric month (ISO). - if (!isISO) { - auto mcStr = TemporalCore::calendarMonthCode(calendarId, monthDay->plainMonthDay().isoPlainDate()); - if (!mcStr) [[unlikely]] { - throwRangeError(globalObject, scope, String(mcStr.error().message)); - return { }; - } - merged.monthCode = ISO8601::parseMonthCode(*mcStr); - } else - merged.month = std::optional<uint32_t>(monthDay->plainMonthDay().month()); - } - - if (!isISO && merged.month.has_value() && !merged.year && !(merged.era && merged.eraYear)) [[unlikely]] - return throwVMTypeError(globalObject, scope, "year is required with month for non-ISO calendar PlainMonthDay.with()"_s); - - // Step 10: isoDate = ? CalendarMonthDayFromFields(calendar, fields, overflow). - auto resolved = TemporalCore::monthDayFromFields(calendarId, merged, overflow); + // Steps 5+7+10: ISODateToFields + CalendarMergeFields + CalendarMonthDayFromFields, in plainMonthDayWith. + auto resolved = TemporalCore::plainMonthDayWith(calendarId, monthDay->plainMonthDay().isoPlainDate(), partialFields, overflow); if (!resolved) [[unlikely]] { if (resolved.error().kind == TemporalErrorKind::TypeError) throwTypeError(globalObject, scope, String(resolved.error().message));
diff --git a/Source/JavaScriptCore/runtime/TemporalPlainTime.cpp b/Source/JavaScriptCore/runtime/TemporalPlainTime.cpp index cb60fb2..bf24d3c 100644 --- a/Source/JavaScriptCore/runtime/TemporalPlainTime.cpp +++ b/Source/JavaScriptCore/runtime/TemporalPlainTime.cpp
@@ -634,48 +634,6 @@ static_cast<Int128>(plainTime.nanosecond()) + static_cast<Int128>(duration.nanoseconds())); } -// https://tc39.es/proposal-temporal/#sec-temporal.plaintime.prototype.with -ISO8601::PlainTime TemporalPlainTime::with(JSGlobalObject* globalObject, JSObject* temporalTimeLike, JSValue optionsValue) const -{ - VM& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - - // Steps 1-2: branding done by caller. - - // Step 3: If ? IsPartialTemporalObject(temporalTimeLike) is false, throw TypeError. - bool isPartial = isPartialTemporalObject(globalObject, JSValue(temporalTimeLike)); - RETURN_IF_EXCEPTION(scope, { }); - if (!isPartial) [[unlikely]] { - throwTypeError(globalObject, scope, "argument must be a partial Temporal object"_s); - return { }; - } - - // Step 4: Let partialTime be ? ToTemporalTimeRecord(temporalTimeLike, ~partial~). - auto [hourOptional, minuteOptional, secondOptional, millisecondOptional, microsecondOptional, nanosecondOptional] = toPartialTime(globalObject, temporalTimeLike); - RETURN_IF_EXCEPTION(scope, { }); - - // Step 17: resolvedOptions = ? GetOptionsObject(options). - JSObject* options = intlGetOptionsObject(globalObject, optionsValue); - RETURN_IF_EXCEPTION(scope, { }); - - // Step 18: overflow = ? GetTemporalOverflowOption(resolvedOptions). - TemporalOverflow overflow = toTemporalOverflow(globalObject, options); - RETURN_IF_EXCEPTION(scope, { }); - - // Steps 5-16 (per field): if partialTime.X is not undefined, x = partialTime.X; else x = this.X. - ISO8601::Duration duration { }; - duration.setField(TemporalUnit::Hour, hourOptional.value_or(hour())); - duration.setField(TemporalUnit::Minute, minuteOptional.value_or(minute())); - duration.setField(TemporalUnit::Second, secondOptional.value_or(second())); - duration.setField(TemporalUnit::Millisecond, millisecondOptional.value_or(millisecond())); - duration.setField(TemporalUnit::Microsecond, microsecondOptional.value_or(microsecond())); - duration.setField(TemporalUnit::Nanosecond, nanosecondOptional.value_or(nanosecond())); - - // Step 19: result = ? RegulateTime(h, m, s, ms, us, ns, overflow). - // Step 20: Return ! CreateTemporalTime(result). (Caller wraps.) - RELEASE_AND_RETURN(scope, regulateTime(globalObject, WTF::move(duration), overflow)); -} - // DifferenceTime ( time1, time2 ) — returns a time duration (in nanoseconds). // https://tc39.es/proposal-temporal/#sec-temporal-differencetime static Int128 differenceTime(ISO8601::PlainTime time1, ISO8601::PlainTime time2)
diff --git a/Source/JavaScriptCore/runtime/TemporalPlainTime.h b/Source/JavaScriptCore/runtime/TemporalPlainTime.h index 9ee3a90..086eedc 100644 --- a/Source/JavaScriptCore/runtime/TemporalPlainTime.h +++ b/Source/JavaScriptCore/runtime/TemporalPlainTime.h
@@ -61,7 +61,6 @@ JSC_TEMPORAL_PLAIN_TIME_UNITS(JSC_DEFINE_TEMPORAL_PLAIN_TIME_FIELD); #undef JSC_DEFINE_TEMPORAL_PLAIN_TIME_FIELD - ISO8601::PlainTime with(JSGlobalObject*, JSObject* temporalTimeLike, JSValue options) const; ISO8601::PlainTime round(JSGlobalObject*, JSValue options) const; String toString(JSGlobalObject*, JSValue options) const; String toString(std::tuple<Precision, unsigned> precision = { Precision::Auto, 0 }) const
diff --git a/Source/JavaScriptCore/runtime/TemporalPlainTimePrototype.cpp b/Source/JavaScriptCore/runtime/TemporalPlainTimePrototype.cpp index d470344..c475383 100644 --- a/Source/JavaScriptCore/runtime/TemporalPlainTimePrototype.cpp +++ b/Source/JavaScriptCore/runtime/TemporalPlainTimePrototype.cpp
@@ -27,6 +27,7 @@ #include "TemporalPlainTimePrototype.h" #include "IntlDateTimeFormat.h" +#include "IntlObjectInlines.h" #include "JSCInlines.h" #include "ObjectConstructor.h" #include "TemporalDuration.h" @@ -163,13 +164,42 @@ if (!plainTime) [[unlikely]] return throwVMTypeError(globalObject, scope, "Temporal.PlainTime.prototype.with called on value that's not a PlainTime"_s); - JSValue temporalTimeLike = callFrame->argument(0); + JSValue temporalTimeLike = callFrame->argument(0); if (!temporalTimeLike.isObject()) [[unlikely]] return throwVMTypeError(globalObject, scope, "First argument to Temporal.PlainTime.prototype.with must be an object"_s); - auto result = plainTime->with(globalObject, asObject(temporalTimeLike), callFrame->argument(1)); + // Step 3: If ? IsPartialTemporalObject(temporalTimeLike) is false, throw TypeError. + bool isPartial = isPartialTemporalObject(globalObject, temporalTimeLike); + RETURN_IF_EXCEPTION(scope, { }); + if (!isPartial) [[unlikely]] + return throwVMTypeError(globalObject, scope, "argument must be a partial Temporal object"_s); + + // Step 4: Let partialTime be ? ToTemporalTimeRecord(temporalTimeLike, ~partial~). + auto [hourOptional, minuteOptional, secondOptional, millisecondOptional, microsecondOptional, nanosecondOptional] = TemporalPlainTime::toPartialTime(globalObject, asObject(temporalTimeLike)); RETURN_IF_EXCEPTION(scope, { }); + // Step 17: resolvedOptions = ? GetOptionsObject(options). + JSObject* options = intlGetOptionsObject(globalObject, callFrame->argument(1)); + RETURN_IF_EXCEPTION(scope, { }); + + // Step 18: overflow = ? GetTemporalOverflowOption(resolvedOptions). + TemporalOverflow overflow = toTemporalOverflow(globalObject, options); + RETURN_IF_EXCEPTION(scope, { }); + + // Steps 5-16 (per field): if partialTime.X is not undefined, x = partialTime.X; else x = this.X. + ISO8601::Duration duration { }; + duration.setField(TemporalUnit::Hour, hourOptional.value_or(plainTime->hour())); + duration.setField(TemporalUnit::Minute, minuteOptional.value_or(plainTime->minute())); + duration.setField(TemporalUnit::Second, secondOptional.value_or(plainTime->second())); + duration.setField(TemporalUnit::Millisecond, millisecondOptional.value_or(plainTime->millisecond())); + duration.setField(TemporalUnit::Microsecond, microsecondOptional.value_or(plainTime->microsecond())); + duration.setField(TemporalUnit::Nanosecond, nanosecondOptional.value_or(plainTime->nanosecond())); + + // Step 19: result = ? RegulateTime(h, m, s, ms, us, ns, overflow). + auto result = TemporalPlainTime::regulateTime(globalObject, WTF::move(duration), overflow); + RETURN_IF_EXCEPTION(scope, { }); + + // Step 20: Return ! CreateTemporalTime(result). return JSValue::encode(TemporalPlainTime::create(vm, globalObject->plainTimeStructure(), WTF::move(result))); }
diff --git a/Source/JavaScriptCore/runtime/TemporalPlainYearMonthPrototype.cpp b/Source/JavaScriptCore/runtime/TemporalPlainYearMonthPrototype.cpp index aab6e82..36b0d23 100644 --- a/Source/JavaScriptCore/runtime/TemporalPlainYearMonthPrototype.cpp +++ b/Source/JavaScriptCore/runtime/TemporalPlainYearMonthPrototype.cpp
@@ -223,7 +223,7 @@ TemporalOverflow overflow = toTemporalOverflow(globalObject, options); RETURN_IF_EXCEPTION(scope, { }); - // Steps 5+7+10: ISODateToFields + CalendarMergeFields + CalendarYearMonthFromFields — fused into plainYearMonthWith. + // Steps 5+7+10: ISODateToFields + CalendarMergeFields + CalendarYearMonthFromFields, in plainYearMonthWith. auto result = TemporalCore::plainYearMonthWith(yearMonth->calendarID(), yearMonth->plainYearMonth().isoPlainDate(), partialFields, overflow); if (!result) [[unlikely]] { if (result.error().kind == TemporalErrorKind::TypeError)
diff --git a/Source/JavaScriptCore/runtime/TemporalZonedDateTimePrototype.cpp b/Source/JavaScriptCore/runtime/TemporalZonedDateTimePrototype.cpp index 1593c38..1e8264c 100644 --- a/Source/JavaScriptCore/runtime/TemporalZonedDateTimePrototype.cpp +++ b/Source/JavaScriptCore/runtime/TemporalZonedDateTimePrototype.cpp
@@ -48,7 +48,6 @@ #include "TemporalZonedDateTime.h" #include "TimeZoneICUBridge.h" #include "ZonedDateTimeCore.h" -#include <wtf/DateMath.h> #include <wtf/text/MakeString.h> namespace JSC { @@ -642,23 +641,26 @@ ASSERT(curOffsetNsOpt); int64_t curOffsetNs = *curOffsetNsOpt; - // Steps 9-16: ISODateToFields(calendar, isoDate, ~date~) + set time/offset fields. - // (Fused with CalendarMergeFields below; current field values serve as the base.) + // Step 9: Let fields be ISODateToFields(calendar, isoDateTime.[[ISODate]], ~date~). + auto dateFields = TemporalCore::isoDateToFields(zdt->calendarID(), curDate, TemporalCore::ResolveType::Date); + if (!dateFields) [[unlikely]] { + throwTemporalError(globalObject, scope, dateFields.error()); + return { }; + } + // Step 17: partialZonedDateTime = ? PrepareCalendarFields(calendar, temporalZonedDateTimeLike, // «year,month,month-code,day», «hour,...,nanosecond,offset», ~partial~). - // Calendar comes from `this`; timeZone is not read in with(). - auto partialFields = readZonedDateTimeFieldsFromObject<ZonedDateTimeFieldMode::Partial>(globalObject, fields, zdt->calendarID()); + auto pf = readZonedDateTimeFieldsFromObject<ZonedDateTimeFieldMode::Partial>(globalObject, fields, zdt->calendarID()); RETURN_IF_EXCEPTION(scope, { }); - // Step 18: fields = CalendarMergeFields(calendar, fields, partialZonedDateTime). - // Implemented by merging partialFields into the current ZDT's field values. - // For non-ISO calendars, fall back to calendar-coordinate values from ISODateToFields. + // Step 18: Set fields to CalendarMergeFields(calendar, fields, partialZonedDateTime). + auto merged = TemporalCore::calendarMergeFields(zdt->calendarID(), *dateFields, pf.dateFields); // Step 19: resolvedOptions = ? GetOptionsObject(options). JSObject* options = intlGetOptionsObject(globalObject, callFrame->argument(1)); RETURN_IF_EXCEPTION(scope, { }); - // Steps 20-22: disambiguation, offset, overflow (alphabetical order per spec NOTE). + // Steps 20-22: disambiguation, offset, overflow (read in alphabetical order per the spec NOTE). auto disambiguation = toTemporalDisambiguation(globalObject, options); RETURN_IF_EXCEPTION(scope, { }); auto offsetOpt = toTemporalOffset(globalObject, options, TemporalOffsetDisambiguation::Prefer); @@ -666,41 +668,7 @@ TemporalOverflow overflow = toTemporalOverflow(globalObject, options); RETURN_IF_EXCEPTION(scope, { }); - // Step 23 fallback prep: for non-ISO calendars, absent patch fields fall back to - // the calendar-native values (not the raw ISO year/month/day). - int32_t fallbackYear = curDate.year(); - uint32_t fallbackMonth = curDate.month(); - uint32_t fallbackDay = curDate.day(); - std::optional<ParsedMonthCode> fallbackMonthCode; - if (!TemporalCore::calendarIsISO(zdt->calendarID())) { - auto calFields = TemporalCore::isoToCalendarFields(zdt->calendarID(), curDate); - if (calFields) { - fallbackYear = calFields->year; - fallbackMonth = calFields->month; - fallbackDay = calFields->day; - if (!calFields->monthCode.isEmpty()) - fallbackMonthCode = ISO8601::parseMonthCode(calFields->monthCode); - } - } - - auto& pf = partialFields; - auto& pDate = pf.dateFields; - - // Step 23: dateTimeResult = ? InterpretTemporalDateTimeFields(calendar, fields, overflow). - // Merge patch onto current ZDT. Only populate month/monthCode from what the user - // actually provided so nonISOResolveFields' consistency check doesn't fire spuriously. - TemporalCore::CalendarFieldsIn merged = pDate; - if (!pDate.day) - merged.day = static_cast<uint8_t>(fallbackDay); - if (!pDate.year && !(pDate.era && pDate.eraYear)) - merged.year = fallbackYear; - if (!pDate.month && !pDate.monthCode) { - // Prefer fallback monthCode (calendar-native) over numeric month for non-ISO. - if (fallbackMonthCode) - merged.monthCode = fallbackMonthCode; - else - merged.month = fallbackMonth; - } + // Steps 10-16, merged into step 18 inline: see CalendarFieldKey in CalendarFields.cpp. TemporalCore::TimeFieldsIn timeFields { pf.hour.value_or(static_cast<double>(curTime.hour())), pf.minute.value_or(static_cast<double>(curTime.minute())), @@ -709,13 +677,16 @@ pf.microsecond.value_or(static_cast<double>(curTime.microsecond())), pf.nanosecond.value_or(static_cast<double>(curTime.nanosecond())), }; + + // Step 23: dateTimeResult = ? InterpretTemporalDateTimeFields(calendar, fields, overflow). auto pdt = interpretTemporalDateTimeFields(globalObject, zdt->calendarID(), merged, timeFields, overflow); RETURN_IF_EXCEPTION(scope, { }); ISO8601::PlainDate newDate = pdt.date; ISO8601::PlainTime newTime = pdt.time; - // Step 24: newOffsetNanoseconds — the offset from the property bag, else the current one. - // temporal_rs: with_with_provider — offset is always Some (explicit or current). + // Step 24: newOffsetNanoseconds = ParseDateTimeUTCOffset(fields.[[OffsetString]]). + // The [[OffsetString]] merge folds in here: format-then-parse of an unchanged offset is an + // identity, so this is just "the partial's offset if given, else the receiver's". int64_t givenOffsetNs = pf.offsetNs.value_or(curOffsetNs); // Step 25: epochNanoseconds = ? InterpretISODateTimeOffset(dateTimeResult.[[ISODate]], @@ -1379,8 +1350,7 @@ auto [date, time] = zdt->getLocalDateTime(globalObject); RETURN_IF_EXCEPTION(scope, { }); // Step 4: Return 𝔽(CalendarISOToDate(calendar, isoDateTime.[[ISODate]]).[[DayOfWeek]]). - // DayOfWeek is universal across all calendar systems (Mon=1 … Sun=7). - return JSValue::encode(jsNumber(ISO8601::dayOfWeek(date))); + return JSValue::encode(jsNumber(TemporalCore::calendarDayOfWeek(zdt->calendarID(), date))); } // https://tc39.es/proposal-temporal/#sec-get-temporal.zoneddatetime.prototype.dayofyear @@ -1416,10 +1386,11 @@ // Step 3: isoDateTime = GetISODateTimeFor(timeZone, epochNanoseconds). auto [date, time] = zdt->getLocalDateTime(globalObject); RETURN_IF_EXCEPTION(scope, { }); - // Step 4: Return ? CalendarWeekOfYear(calendar, isoDateTime.[[ISODate]]). Undefined for non-ISO. - if (!TemporalCore::calendarIsISO(zdt->calendarID())) + // Step 4: Return ? CalendarWeekOfYear(calendar, isoDateTime.[[ISODate]]). + auto week = TemporalCore::calendarWeekOfYear(zdt->calendarID(), date); + if (!week) return JSValue::encode(jsUndefined()); - return JSValue::encode(jsNumber(ISO8601::weekOfYear(date))); + return JSValue::encode(jsNumber(*week)); } // https://tc39.es/proposal-temporal/#sec-get-temporal.zoneddatetime.prototype.yearofweek @@ -1435,10 +1406,11 @@ // Step 3: isoDateTime = GetISODateTimeFor(timeZone, epochNanoseconds). auto [date, time] = zdt->getLocalDateTime(globalObject); RETURN_IF_EXCEPTION(scope, { }); - // Step 4: Return ? CalendarYearOfWeek(calendar, isoDateTime.[[ISODate]]). Undefined for non-ISO. - if (!TemporalCore::calendarIsISO(zdt->calendarID())) + // Step 4: Return ? CalendarYearOfWeek(calendar, isoDateTime.[[ISODate]]). + auto yearOfWeek = TemporalCore::calendarYearOfWeek(zdt->calendarID(), date); + if (!yearOfWeek) return JSValue::encode(jsUndefined()); - return JSValue::encode(jsNumber(ISO8601::yearOfWeek(date))); + return JSValue::encode(jsNumber(*yearOfWeek)); } // https://tc39.es/proposal-temporal/#sec-get-temporal.zoneddatetime.prototype.hoursinday @@ -1498,8 +1470,7 @@ RETURN_IF_EXCEPTION(scope, { }); // Step 4: Return 𝔽(CalendarISOToDate(calendar, isoDate).[[DaysInWeek]]). - // All calendar systems have a 7-day week. - return JSValue::encode(jsNumber(7)); + return JSValue::encode(jsNumber(ISO8601::daysPerWeek)); } // https://tc39.es/proposal-temporal/#sec-get-temporal.zoneddatetime.prototype.daysinmonth
diff --git a/Source/JavaScriptCore/runtime/temporal/core/CalendarFields.cpp b/Source/JavaScriptCore/runtime/temporal/core/CalendarFields.cpp index d48a60d..7d898de 100644 --- a/Source/JavaScriptCore/runtime/temporal/core/CalendarFields.cpp +++ b/Source/JavaScriptCore/runtime/temporal/core/CalendarFields.cpp
@@ -37,6 +37,7 @@ #include <cstdint> #include <wtf/DateMath.h> #include <wtf/MathExtras.h> +#include <wtf/OptionSet.h> namespace JSC { namespace TemporalCore { @@ -458,78 +459,151 @@ return ResolvedCalendarDate { *result, calendarId }; } +// https://tc39.es/proposal-temporal/#sec-temporal-isodatetofields +TemporalResult<CalendarFieldsIn> isoDateToFields(CalendarID calendarId, const ISO8601::PlainDate& isoDate, ResolveType type) +{ + // Step 1: Let fields be an empty Calendar Fields Record with all fields set to ~unset~. + CalendarFieldsIn fields; + + // Step 2: Let calendarDate be CalendarISOToDate(calendar, isoDate). + if (calendarIsISO(calendarId)) { + // Identity for "iso8601", so no ICU round-trip; the month code is the ISO month, never leap. + fields.monthCode = ParsedMonthCode { isoDate.month(), /* isLeapMonth */ false }; // Step 3 + if (type == ResolveType::MonthDay || type == ResolveType::Date) // Step 4 + fields.day = isoDate.day(); + if (type == ResolveType::YearMonth || type == ResolveType::Date) // Step 5 + fields.year = isoDate.year(); + return fields; // Step 6 + } + + auto calendarDate = isoToCalendarFields(calendarId, isoDate); + if (!calendarDate) + return makeUnexpected(calendarDate.error()); + + // Step 3: Set fields.[[MonthCode]] to calendarDate.[[MonthCode]]. + if (!calendarDate->monthCode.isEmpty()) + fields.monthCode = ISO8601::parseMonthCode(calendarDate->monthCode); + // Step 4: If type is either ~month-day~ or ~date~, set fields.[[Day]] to calendarDate.[[Day]]. + if (type == ResolveType::MonthDay || type == ResolveType::Date) + fields.day = calendarDate->day; + // Step 5: If type is either ~year-month~ or ~date~, set fields.[[Year]] to calendarDate.[[Year]]. + if (type == ResolveType::YearMonth || type == ResolveType::Date) + fields.year = calendarDate->year; + // Step 6: Return fields. + return fields; +} + +// The Calendar Fields Record table's Enumeration Key column, restricted to the six calendar-date +// keys CalendarFieldsIn carries; a bitset rather than the spec's List to stay allocation-free. No +// ignore rule names the time keys or ~offset~/~time-zone~, so for those the merge degenerates to +// "the partial wins, else the receiver", which the two .with callers do inline. +enum class CalendarFieldKey : uint8_t { + Era = 1 << 0, + EraYear = 1 << 1, + Year = 1 << 2, + Month = 1 << 3, + MonthCode = 1 << 4, + Day = 1 << 5, +}; +using CalendarFieldKeys = OptionSet<CalendarFieldKey>; + +// https://tc39.es/proposal-temporal/#sec-temporal-calendarfieldkeyspresent +static CalendarFieldKeys calendarFieldKeysPresent(const CalendarFieldsIn& fields) +{ + CalendarFieldKeys keys; + if (fields.era) + keys.add(CalendarFieldKey::Era); + if (fields.eraYear) + keys.add(CalendarFieldKey::EraYear); + if (fields.year) + keys.add(CalendarFieldKey::Year); + if (fields.month) + keys.add(CalendarFieldKey::Month); + if (fields.monthCode) + keys.add(CalendarFieldKey::MonthCode); + if (fields.day) + keys.add(CalendarFieldKey::Day); + return keys; +} + +// https://tc39.es/proposal-intl-era-monthcode/#sup-temporal-nonisofieldkeystoignore +static CalendarFieldKeys nonISOFieldKeysToIgnore(CalendarID calendarId, CalendarFieldKeys keys) +{ + ASSERT(!calendarIsISO(calendarId)); + // Step 1: Let ignoredKeys be a copy of keys — "a field always invalidates at least itself". + CalendarFieldKeys ignoredKeys = keys; + // Steps 2.a-2.b: month and monthCode are two encodings of the same field. + if (keys.contains(CalendarFieldKey::Month)) + ignoredKeys.add(CalendarFieldKey::MonthCode); + if (keys.contains(CalendarFieldKey::MonthCode)) + ignoredKeys.add(CalendarFieldKey::Month); + // Step 2.c: era+eraYear and year are two encodings of the same year, so changing either + // encoding invalidates the other. + if (calendarHasEras(calendarId) && keys.containsAny({ CalendarFieldKey::Era, CalendarFieldKey::EraYear, CalendarFieldKey::Year })) + ignoredKeys.add({ CalendarFieldKey::Era, CalendarFieldKey::EraYear, CalendarFieldKey::Year }); + // Step 2.d: with mid-year eras a day/month move can cross an era boundary without changing the + // arithmetic year, so drop the era pair but keep year. + if (calendarHasMidYearEras(calendarId) && keys.containsAny({ CalendarFieldKey::Day, CalendarFieldKey::Month, CalendarFieldKey::MonthCode })) + ignoredKeys.add({ CalendarFieldKey::Era, CalendarFieldKey::EraYear }); + // Step 4: Return ignoredKeys. + return ignoredKeys; +} + +// https://tc39.es/proposal-temporal/#sec-temporal-calendarfieldkeystoignore +static CalendarFieldKeys calendarFieldKeysToIgnore(CalendarID calendarId, CalendarFieldKeys keys) +{ + // Step 1: If calendar is "iso8601" — only the month/monthCode pairing applies. + if (calendarIsISO(calendarId)) { + CalendarFieldKeys ignoredKeys = keys; // Steps 1.a-1.b.i + if (keys.contains(CalendarFieldKey::Month)) // Step 1.b.ii + ignoredKeys.add(CalendarFieldKey::MonthCode); + if (keys.contains(CalendarFieldKey::MonthCode)) // Step 1.b.iii + ignoredKeys.add(CalendarFieldKey::Month); + return ignoredKeys; // Step 1.d + } + // Step 2: Return NonISOFieldKeysToIgnore(calendar, keys). + return nonISOFieldKeysToIgnore(calendarId, keys); +} + +// https://tc39.es/proposal-temporal/#sec-temporal-calendarmergefields +CalendarFieldsIn calendarMergeFields(CalendarID calendarId, const CalendarFieldsIn& fields, const CalendarFieldsIn& additionalFields) +{ + // Step 1: Let additionalKeys be CalendarFieldKeysPresent(additionalFields). + auto additionalKeys = calendarFieldKeysPresent(additionalFields); + // Step 2: Let overriddenKeys be CalendarFieldKeysToIgnore(calendar, additionalKeys). + auto overriddenKeys = calendarFieldKeysToIgnore(calendarId, additionalKeys); + // Step 3: Let merged be a Calendar Fields Record with all fields set to ~unset~. + CalendarFieldsIn merged; + // Step 4: fieldsKeys is CalendarFieldKeysPresent(fields) — each engaged optional below is its + // own membership test, so there is nothing to precompute. + // Step 5: For each row of the Calendar Fields Record table. + auto mergeKey = [&](CalendarFieldKey key, auto& mergedField, const auto& field, const auto& additionalField) { + // Step 5.b: fieldsKeys contains key and overriddenKeys does not contain key. + if (field && !overriddenKeys.contains(key)) + mergedField = field; + // Step 5.c: additionalKeys contains key. + if (additionalField) + mergedField = additionalField; + }; + mergeKey(CalendarFieldKey::Era, merged.era, fields.era, additionalFields.era); + mergeKey(CalendarFieldKey::EraYear, merged.eraYear, fields.eraYear, additionalFields.eraYear); + mergeKey(CalendarFieldKey::Year, merged.year, fields.year, additionalFields.year); + mergeKey(CalendarFieldKey::Month, merged.month, fields.month, additionalFields.month); + mergeKey(CalendarFieldKey::MonthCode, merged.monthCode, fields.monthCode, additionalFields.monthCode); + mergeKey(CalendarFieldKey::Day, merged.day, fields.day, additionalFields.day); + // Step 6: Return merged. + return merged; +} + // plainYearMonthWith — temporal_rs: PlainYearMonth::with (src/builtins/core/plain_year_month.rs) // https://tc39.es/proposal-temporal/#sec-temporal.plainyearmonth.prototype.with -// Implements inner merge steps; IsPartialTemporalObject and PrepareCalendarFields done by JS-layer caller. +// Implements steps 5, 7 and 10; IsPartialTemporalObject and PrepareCalendarFields done by JS-layer caller. TemporalResult<ResolvedCalendarDate> plainYearMonthWith(CalendarID calendarId, const ISO8601::PlainDate& currentISODate, const CalendarFieldsIn& partialFields, TemporalOverflow overflow) { - // Defensive: JS-layer caller guarantees at least one field via IsPartialTemporalObject; - // guard against direct internal calls with an empty partialFields. - if (!partialFields.year && !partialFields.month && !partialFields.monthCode - && !partialFields.era && !partialFields.eraYear) - return makeUnexpected(typeError("at least one field (year, month, monthCode, era, or eraYear) must be provided"_s)); - - bool isISO = calendarIsISO(calendarId); - - if (isISO) { - // ISO: same merge logic as temporal_rs with_fallback_year_month. - CalendarFieldsIn merged; - merged.year = partialFields.year.has_value() ? partialFields.year : std::optional<int32_t>(currentISODate.year()); - // temporal_rs: impl_with_fallback_method! — month: user's value only, no fallback - merged.month = partialFields.month; - // monthCode: user's overrides; fallback from current only if neither month nor monthCode provided - merged.monthCode = partialFields.monthCode; - if (!partialFields.month.has_value() && !partialFields.monthCode) - merged.monthCode = ISO8601::parseMonthCode(ISO8601::monthCode(currentISODate.month())); - return yearMonthFromFields(calendarId, merged, overflow); - } - - // Non-ISO: get calendar fields from current date, merge with user partial fields. - // temporal_rs: impl_with_fallback_method! macro and impl_field_keys_to_ignore! macro (fields.rs). - auto calFields = isoToCalendarFields(calendarId, currentISODate); - if (!calFields) - return makeUnexpected(calFields.error()); - - // temporal_rs: impl_field_keys_to_ignore! — determine which fallback fields to suppress - bool keysToIgnoreMonth = partialFields.month.has_value() || partialFields.monthCode.has_value(); - bool hasEras = calendarHasEras(calendarId); - bool keysToIgnoreEra = false; - bool keysToIgnoreYear = false; - if (hasEras) { - if (partialFields.year.has_value() || partialFields.eraYear.has_value() || partialFields.era.has_value()) { - keysToIgnoreEra = true; - keysToIgnoreYear = true; - } - } - - CalendarFieldsIn merged; - - // temporal_rs: impl_with_fallback_method! — era/eraYear: fallback from current if !keysToIgnoreEra - merged.era = partialFields.era; - merged.eraYear = partialFields.eraYear; - if (!keysToIgnoreEra) { - if (!merged.era && calFields->era.has_value() && !calFields->era->isEmpty()) - merged.era = *calFields->era; - if (!merged.eraYear && calFields->eraYear.has_value()) - merged.eraYear = *calFields->eraYear; - } - - // temporal_rs: impl_with_fallback_method! — year: fallback from current if !keysToIgnoreYear - merged.year = partialFields.year; - if (!keysToIgnoreYear && !merged.year) - merged.year = std::optional<int32_t>(calFields->year); - - // temporal_rs: impl_with_fallback_method! — month: user's value only, NEVER fallback - merged.month = partialFields.month; - - // temporal_rs: impl_with_fallback_method! — monthCode: fallback ONLY when neither month nor monthCode provided - merged.monthCode = partialFields.monthCode; - if (!partialFields.month.has_value() && !partialFields.monthCode.has_value() && !keysToIgnoreMonth) { - if (!calFields->monthCode.isEmpty()) - merged.monthCode = ISO8601::parseMonthCode(calFields->monthCode); - } - - return yearMonthFromFields(calendarId, merged, overflow); + auto fields = isoDateToFields(calendarId, currentISODate, ResolveType::YearMonth); + if (!fields) + return makeUnexpected(fields.error()); + return yearMonthFromFields(calendarId, calendarMergeFields(calendarId, *fields, partialFields), overflow); } // plainDateWith — temporal_rs: PlainDate::with (src/builtins/core/plain_date.rs) @@ -540,73 +614,21 @@ // Steps 6, 8-9 (PrepareCalendarFields, overflow) done by PlainDate::with(); step 11 (CreateTemporalDate) by prototype caller. TemporalResult<ResolvedCalendarDate> plainDateWith(CalendarID calendarId, const ISO8601::PlainDate& currentISODate, const CalendarFieldsIn& partialFields, TemporalOverflow overflow) { - // ISO fast path — mirrors plainYearMonthWith's ISO branch (plus day). Avoids isoToCalendarFields, - // which for a non-ROC/Buddhist/extreme-year calendar falls through to ICU calendar construction; - // iso8601 never needs that. - if (calendarIsISO(calendarId)) { - CalendarFieldsIn merged; - merged.year = partialFields.year.has_value() ? partialFields.year : std::optional<int32_t>(currentISODate.year()); - merged.month = partialFields.month; - merged.monthCode = partialFields.monthCode; - if (!partialFields.month.has_value() && !partialFields.monthCode) - merged.monthCode = ISO8601::parseMonthCode(ISO8601::monthCode(currentISODate.month())); - merged.day = partialFields.day.has_value() ? *partialFields.day : currentISODate.day(); - return dateFromFields(calendarId, merged, overflow); - } + auto fields = isoDateToFields(calendarId, currentISODate, ResolveType::Date); + if (!fields) + return makeUnexpected(fields.error()); + return dateFromFields(calendarId, calendarMergeFields(calendarId, *fields, partialFields), overflow); +} - // Step 5: fields = ISODateToFields(calendar, plainDate.[[ISODate]], ~date~). - auto calFields = isoToCalendarFields(calendarId, currentISODate); - if (!calFields) - return makeUnexpected(calFields.error()); - - bool hasEras = calendarHasEras(calendarId); - bool isJapanese = (calendarId == japaneseCalendarID()); - - // Step 7: fields = CalendarMergeFields(calendar, fields, partialDate). - // NonISOFieldKeysToIgnore (temporal_rs: impl_field_keys_to_ignore!): - // year/era/eraYear in additionalKeys → both era AND year suppressed from base. - // Japanese month/day changes → era suppressed (but NOT year; year still inherited). - bool keysToIgnoreEra = hasEras && (partialFields.year.has_value() || partialFields.era.has_value() || partialFields.eraYear.has_value()); - bool keysToIgnoreYear = keysToIgnoreEra; - if (isJapanese && (partialFields.month.has_value() || partialFields.monthCode.has_value() || partialFields.day.has_value())) - keysToIgnoreEra = true; // suppress era but not year for Japanese month/day changes - bool keysToIgnoreMonth = partialFields.month.has_value() || partialFields.monthCode.has_value(); - - CalendarFieldsIn merged; - - merged.era = partialFields.era; - merged.eraYear = partialFields.eraYear; - if (!keysToIgnoreEra) { - if (!merged.era && calFields->era.has_value() && !calFields->era->isEmpty()) - merged.era = *calFields->era; - if (!merged.eraYear && calFields->eraYear.has_value()) - merged.eraYear = *calFields->eraYear; - } - - merged.year = partialFields.year; - if (!keysToIgnoreYear && !merged.year) - merged.year = std::optional<int32_t>(calFields->year); - - // month: user's value if provided; suppress base month when monthCode is provided (they're mutually exclusive). - // Also drop base's month for lunisolar calendars when the user changes year (or era) — the ordinal shifts - // between years with different leap-month layouts and monthCode alone is the correct calendar-native identifier. - // Keep an inherited month as monthCode only (not raw month) so it can be constrained in a different year. - bool lunisolarYearChange = calendarIsLunisolar(calendarId) - && (partialFields.year.has_value() || partialFields.era.has_value() || partialFields.eraYear.has_value()); - if (partialFields.month.has_value()) - merged.month = *partialFields.month; - else if (!partialFields.monthCode.has_value() && calFields->monthCode.isEmpty() && !lunisolarYearChange) - merged.month = static_cast<uint32_t>(calFields->month); - merged.day = partialFields.day.has_value() ? *partialFields.day : calFields->day; - - merged.monthCode = partialFields.monthCode; - if (!partialFields.month.has_value() && !partialFields.monthCode.has_value() && !keysToIgnoreMonth) { - if (!calFields->monthCode.isEmpty()) - merged.monthCode = ISO8601::parseMonthCode(calFields->monthCode); - } - - // Step 10: isoDate = CalendarDateFromFields(calendar, fields, overflow). - return dateFromFields(calendarId, merged, overflow); +// plainMonthDayWith — temporal_rs: PlainMonthDay::with (src/builtins/core/plain_month_day.rs) +// https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.prototype.with +// Implements steps 5, 7 and 10 (CalendarMonthDayFromFields). +TemporalResult<ResolvedCalendarDate> plainMonthDayWith(CalendarID calendarId, const ISO8601::PlainDate& currentISODate, const CalendarFieldsIn& partialFields, TemporalOverflow overflow) +{ + auto fields = isoDateToFields(calendarId, currentISODate, ResolveType::MonthDay); + if (!fields) + return makeUnexpected(fields.error()); + return monthDayFromFields(calendarId, calendarMergeFields(calendarId, *fields, partialFields), overflow); } // differenceYearMonth — temporal_rs: PlainYearMonth::diff (src/builtins/core/plain_year_month.rs) @@ -628,32 +650,21 @@ } // Non-ISO: resolve both to day=1 via dateFromFields (matching temporal_rs). - // This re-resolves the ISO date through the calendar pipeline with day=1. - // Step 7: ISODateToFields(calendar, thisISODate, year-month); set [[Day]] to 1; CalendarDateFromFields. - auto thisCalFields = isoToCalendarFields(calendarId, thisISODate); - if (!thisCalFields) - return makeUnexpected(thisCalFields.error()); - CalendarFieldsIn thisFields; - thisFields.year = thisCalFields->year; - thisFields.month = thisCalFields->month; - thisFields.day = 1; - if (!thisCalFields->monthCode.isEmpty()) - thisFields.monthCode = ISO8601::parseMonthCode(thisCalFields->monthCode); - auto thisResolved = dateFromFields(calendarId, thisFields, TemporalOverflow::Constrain); + // Steps 7-9 for each side: ISODateToFields(calendar, isoDate, ~year-month~); set [[Day]] to 1; + // ? CalendarDateFromFields(calendar, fields, ~constrain~). + auto thisFields = isoDateToFields(calendarId, thisISODate, ResolveType::YearMonth); + if (!thisFields) + return makeUnexpected(thisFields.error()); + thisFields->day = 1; + auto thisResolved = dateFromFields(calendarId, *thisFields, TemporalOverflow::Constrain); if (!thisResolved) return makeUnexpected(thisResolved.error()); - // Steps 8-9: ISODateToFields(calendar, otherISODate, year-month); set [[Day]] to 1; CalendarDateFromFields. - auto otherCalFields = isoToCalendarFields(calendarId, otherISODate); - if (!otherCalFields) - return makeUnexpected(otherCalFields.error()); - CalendarFieldsIn otherFields; - otherFields.year = otherCalFields->year; - otherFields.month = otherCalFields->month; - otherFields.day = 1; - if (!otherCalFields->monthCode.isEmpty()) - otherFields.monthCode = ISO8601::parseMonthCode(otherCalFields->monthCode); - auto otherResolved = dateFromFields(calendarId, otherFields, TemporalOverflow::Constrain); + auto otherFields = isoDateToFields(calendarId, otherISODate, ResolveType::YearMonth); + if (!otherFields) + return makeUnexpected(otherFields.error()); + otherFields->day = 1; + auto otherResolved = dateFromFields(calendarId, *otherFields, TemporalOverflow::Constrain); if (!otherResolved) return makeUnexpected(otherResolved.error()); @@ -668,78 +679,34 @@ // and step 15 (CreateTemporalYearMonth wrap) are done by the JS-layer caller. TemporalResult<ResolvedCalendarDate> plainYearMonthAdd(CalendarID calendarId, const ISO8601::PlainDate& currentISODate, const ISO8601::Duration& duration, TemporalOverflow overflow) { - bool isISO = calendarIsISO(calendarId); + // Step 9: Let fields be ISODateToFields(calendar, yearMonth.[[ISODate]], ~year-month~). + auto fields = isoDateToFields(calendarId, currentISODate, ResolveType::YearMonth); + if (!fields) + return makeUnexpected(fields.error()); + // Step 10: Set fields.[[Day]] to 1. + fields->day = 1; - if (isISO) { - // Step 1: Get calendar fields from currentISODate (year + monthCode), set day=1. - CalendarFieldsIn fields; - fields.year = currentISODate.year(); - fields.month = currentISODate.month(); - fields.monthCode = ISO8601::parseMonthCode(ISO8601::monthCode(currentISODate.month())); - fields.day = 1; - // Step 2: CalendarDateFromFields -> PlainDate. - auto dateResult = dateFromFields(calendarId, fields, TemporalOverflow::Constrain); - if (!dateResult) - return makeUnexpected(dateResult.error()); - // Step 3: CalendarDateAdd(duration) -> addedDate. - // ISO: bypass ICU bridge and use pure isoDateAdd — handles extreme boundary dates - // correctly (year ±271821) without ICU calendar clamping. - auto addedISO = calendarDateAdd(dateResult->isoDate, duration, overflow); - if (!addedISO) - return makeUnexpected(addedISO.error()); - // Steps 4-5: Get year + monthCode from addedDate; CalendarYearMonthFromFields -> result PYM ISO date. - CalendarFieldsIn addedFields; - addedFields.year = addedISO->year(); - addedFields.monthCode = ISO8601::parseMonthCode(ISO8601::monthCode(addedISO->month())); - return yearMonthFromFields(calendarId, addedFields, overflow); - } - - // Non-ISO: temporal_rs add_duration steps 9-15. - // Step 1: ISODateToFields(calendar, yearMonth.[[ISODate]], year-month) -> year + monthCode, day=1. - auto calFields = isoToCalendarFields(calendarId, currentISODate); - if (!calFields) - return makeUnexpected(calFields.error()); - - CalendarFieldsIn fieldsDay1; - fieldsDay1.year = calFields->year; - fieldsDay1.day = 1; - if (!calFields->monthCode.isEmpty()) - fieldsDay1.monthCode = ISO8601::parseMonthCode(calFields->monthCode); - else - fieldsDay1.month = calFields->month; - if (calFields->era.has_value() && calFields->eraYear.has_value()) { - fieldsDay1.era = *calFields->era; - fieldsDay1.eraYear = *calFields->eraYear; - } - - // Step 2: CalendarDateFromFields -> PlainDate ISO. - auto dateResult = dateFromFields(calendarId, fieldsDay1, TemporalOverflow::Constrain); + // Step 11: Let date be ? CalendarDateFromFields(calendar, fields, ~constrain~). + auto dateResult = dateFromFields(calendarId, *fields, TemporalOverflow::Constrain); if (!dateResult) return makeUnexpected(dateResult.error()); - // Step 3: CalendarDateAdd(duration) -> addedDate ISO. - auto addedISO = calendarDateAdd(calendarId, dateResult->isoDate, duration, overflow); + // Step 12: Let addedDate be ? CalendarDateAdd(calendar, date, durationToAdd, overflow). + // ISO takes the pure isoDateAdd overload, which handles the extreme boundary years + // (±271821) that ICU calendar arithmetic clamps. + auto addedISO = calendarIsISO(calendarId) + ? calendarDateAdd(dateResult->isoDate, duration, overflow) + : calendarDateAdd(calendarId, dateResult->isoDate, duration, overflow); if (!addedISO) return makeUnexpected(addedISO.error()); - // Step 4: Get year + monthCode from addedDate. - auto addedCalFields = isoToCalendarFields(calendarId, *addedISO); - if (!addedCalFields) - return makeUnexpected(addedCalFields.error()); + // Step 13: Let addedDateFields be ISODateToFields(calendar, addedDate, ~year-month~). + auto addedFields = isoDateToFields(calendarId, *addedISO, ResolveType::YearMonth); + if (!addedFields) + return makeUnexpected(addedFields.error()); - CalendarFieldsIn addedFields; - addedFields.year = addedCalFields->year; - if (!addedCalFields->monthCode.isEmpty()) - addedFields.monthCode = ISO8601::parseMonthCode(addedCalFields->monthCode); - else - addedFields.month = addedCalFields->month; - if (addedCalFields->era.has_value() && addedCalFields->eraYear.has_value()) { - addedFields.era = *addedCalFields->era; - addedFields.eraYear = *addedCalFields->eraYear; - } - - // Step 5: CalendarYearMonthFromFields -> result PYM ISO date. - return yearMonthFromFields(calendarId, addedFields, overflow); + // Step 14: Return ? CalendarYearMonthFromFields(calendar, addedDateFields, overflow). + return yearMonthFromFields(calendarId, *addedFields, overflow); } // plainYearMonthToPlainDate — temporal_rs: PlainYearMonth::to_plain_date (src/builtins/core/plain_year_month.rs) @@ -747,94 +714,48 @@ // Implements steps 5, 7, 8; steps 1-4, 6, 9 done by JS-layer caller. TemporalResult<ResolvedCalendarDate> plainYearMonthToPlainDate(CalendarID calendarId, const ISO8601::PlainDate& pymISODate, uint8_t day) { - bool isISO = calendarIsISO(calendarId); + // Step 5: Let fields be ISODateToFields(calendar, plainYearMonth.[[ISODate]], ~year-month~). + auto fields = isoDateToFields(calendarId, pymISODate, ResolveType::YearMonth); + if (!fields) + return makeUnexpected(fields.error()); - if (isISO) { - // Steps 5+7 (ISO): year/month from pymISODate; merge day. - CalendarFieldsIn fields; - fields.year = pymISODate.year(); - fields.month = pymISODate.month(); - fields.day = day; - // Step 8: CalendarDateFromFields(~constrain~). - return dateFromFields(calendarId, fields, TemporalOverflow::Constrain); - } + // Step 6's PrepareCalendarFields(«~day~») is the caller's; it arrives as `day`. + CalendarFieldsIn inputFields; + inputFields.day = day; - // Steps 5+7 (non-ISO): ISODateToFields via ICU bridge; merge day. - auto yearResult = calendarYear(calendarId, pymISODate); - if (!yearResult) - return makeUnexpected(yearResult.error()); - auto monthCodeStr = calendarMonthCode(calendarId, pymISODate); - if (!monthCodeStr) - return makeUnexpected(monthCodeStr.error()); - - CalendarFieldsIn fields; - fields.year = *yearResult; - fields.day = day; - fields.monthCode = ISO8601::parseMonthCode(*monthCodeStr); - // Step 8: CalendarDateFromFields(~constrain~). - return dateFromFields(calendarId, fields, TemporalOverflow::Constrain); + // Step 7: Let mergedFields be CalendarMergeFields(calendar, fields, inputFields). + // Step 8: Return ? CalendarDateFromFields(calendar, mergedFields, ~constrain~). + return dateFromFields(calendarId, calendarMergeFields(calendarId, *fields, inputFields), TemporalOverflow::Constrain); } // plainYearMonthFromISODate — no 1:1 temporal_rs function; inlined in PlainYearMonth::from_parsed // https://tc39.es/proposal-temporal/#sec-temporal-totemporalyearmonth (string parse path) -// Implements steps 10 and 12; the within-limits check (spec step 9) runs inside -// yearMonthFromFields (see ISOYearMonthWithinLimits guard below in that function). +// Implements steps 12 and 14; step 11's ISOYearMonthWithinLimits check runs inside +// yearMonthFromFields (see the guard in that function). TemporalResult<ResolvedCalendarDate> plainYearMonthFromISODate(CalendarID calendarId, const ISO8601::PlainDate& fullISODate) { - bool isISO = calendarIsISO(calendarId); - - if (isISO) { - // ISO path: year/month from fullISODate; yearMonthFromFields stores with day=1. - CalendarFieldsIn fields; - fields.year = fullISODate.year(); - fields.month = fullISODate.month(); - return yearMonthFromFields(calendarId, fields, TemporalOverflow::Constrain); - } - - // Step 10 (non-ISO): ISODateToFields via ICU bridge — gets year + monthCode. - auto yearResult = calendarYear(calendarId, fullISODate); - if (!yearResult) - return makeUnexpected(yearResult.error()); - auto monthCodeStr = calendarMonthCode(calendarId, fullISODate); - if (!monthCodeStr) - return makeUnexpected(monthCodeStr.error()); - - CalendarFieldsIn fields; - fields.year = *yearResult; - fields.day = 1; - fields.monthCode = ISO8601::parseMonthCode(*monthCodeStr); - // Step 12: CalendarYearMonthFromFields(~constrain~) per spec note. - return yearMonthFromFields(calendarId, fields, TemporalOverflow::Constrain); + // Step 12: Set result to ISODateToFields(calendar, isoDate, ~year-month~). + auto fields = isoDateToFields(calendarId, fullISODate, ResolveType::YearMonth); + if (!fields) + return makeUnexpected(fields.error()); + // Step 14: Return ? CalendarYearMonthFromFields(calendar, result, ~constrain~) — ~constrain~ + // regardless of overflow, per Step 13's NOTE. yearMonthFromFields stores day=1 itself. + return yearMonthFromFields(calendarId, *fields, TemporalOverflow::Constrain); } // https://tc39.es/proposal-temporal/#sec-temporal-totemporalmonthday (string parse path) -// Implements steps 10 and 12; the within-limits check (spec step 9) runs inside -// monthDayFromFields (see ISODateWithinLimits guard in that function). +// Implements steps 13 and 15; step 12's ISODateWithinLimits check runs inside monthDayFromFields. +// Step 10's "iso8601" shortcut is not special-cased: routing it through CalendarMonthDayFromFields +// reaches the same reference year 1972 without a second code path. TemporalResult<ResolvedCalendarDate> plainMonthDayFromISODate(CalendarID calendarId, const ISO8601::PlainDate& fullISODate, TemporalOverflow overflow) { - bool isISO = calendarIsISO(calendarId); - - if (isISO) { - // ISO path (spec step 7): store directly with reference year 1972. - CalendarFieldsIn fields; - fields.month = fullISODate.month(); - fields.day = fullISODate.day(); - return monthDayFromFields(calendarId, fields, overflow); - } - - // Step 10: ISODateToFields(calendar, fullISODate, ~month-day~) — gets monthCode + day. - auto monthCodeStr = calendarMonthCode(calendarId, fullISODate); - if (!monthCodeStr) - return makeUnexpected(monthCodeStr.error()); - auto dayResult = calendarDay(calendarId, fullISODate); - if (!dayResult) - return makeUnexpected(dayResult.error()); - - CalendarFieldsIn fields; - fields.monthCode = ISO8601::parseMonthCode(*monthCodeStr); - fields.day = static_cast<uint8_t>(*dayResult); - // Step 12: caller must pass TemporalOverflow::Constrain per spec note. - return monthDayFromFields(calendarId, fields, overflow); + // Step 13: Set result to ISODateToFields(calendar, isoDate, ~month-day~). + auto fields = isoDateToFields(calendarId, fullISODate, ResolveType::MonthDay); + if (!fields) + return makeUnexpected(fields.error()); + // Step 15: Return ? CalendarMonthDayFromFields(calendar, result, ~constrain~) — every caller + // passes ~constrain~ per Step 14's NOTE. + return monthDayFromFields(calendarId, *fields, overflow); } } // namespace TemporalCore
diff --git a/Source/JavaScriptCore/runtime/temporal/core/CalendarFields.h b/Source/JavaScriptCore/runtime/temporal/core/CalendarFields.h index 31d573d..ae504a7 100644 --- a/Source/JavaScriptCore/runtime/temporal/core/CalendarFields.h +++ b/Source/JavaScriptCore/runtime/temporal/core/CalendarFields.h
@@ -77,10 +77,16 @@ JS_EXPORT_PRIVATE TemporalResult<ResolvedCalendarDate> monthDayFromFields(CalendarID, const CalendarFieldsIn&, TemporalOverflow); +JS_EXPORT_PRIVATE TemporalResult<CalendarFieldsIn> isoDateToFields(CalendarID, const ISO8601::PlainDate&, ResolveType); + +JS_EXPORT_PRIVATE CalendarFieldsIn calendarMergeFields(CalendarID, const CalendarFieldsIn&, const CalendarFieldsIn& additionalFields); + JS_EXPORT_PRIVATE TemporalResult<ResolvedCalendarDate> plainYearMonthWith(CalendarID, const ISO8601::PlainDate& currentISODate, const CalendarFieldsIn& partialFields, TemporalOverflow); JS_EXPORT_PRIVATE TemporalResult<ResolvedCalendarDate> plainDateWith(CalendarID, const ISO8601::PlainDate& currentISODate, const CalendarFieldsIn& partialFields, TemporalOverflow); +JS_EXPORT_PRIVATE TemporalResult<ResolvedCalendarDate> plainMonthDayWith(CalendarID, const ISO8601::PlainDate& currentISODate, const CalendarFieldsIn& partialFields, TemporalOverflow); + JS_EXPORT_PRIVATE TemporalResult<ISO8601::Duration> differenceYearMonth(CalendarID, const ISO8601::PlainDate& thisISODate, const ISO8601::PlainDate& otherISODate, TemporalUnit largestUnit); JS_EXPORT_PRIVATE TemporalResult<ResolvedCalendarDate> plainYearMonthAdd(CalendarID, const ISO8601::PlainDate& currentISODate, const ISO8601::Duration&, TemporalOverflow);
diff --git a/Source/JavaScriptCore/runtime/temporal/core/CalendarICUBridge.cpp b/Source/JavaScriptCore/runtime/temporal/core/CalendarICUBridge.cpp index b493db2..4af667b 100644 --- a/Source/JavaScriptCore/runtime/temporal/core/CalendarICUBridge.cpp +++ b/Source/JavaScriptCore/runtime/temporal/core/CalendarICUBridge.cpp
@@ -918,7 +918,9 @@ return (calendarId == chineseCalendarID() || calendarId == dangiCalendarID()) && std::abs(isoYear) > 10000; } -// isoToCalendarFields — no single temporal_rs equivalent; aggregates Calendar::year/month/month_code/day/era. +// https://tc39.es/proposal-temporal/#sec-temporal-calendarisotodate +// CalendarISOToDate's six field-resolution fields in one ICU open. The calendar* accessors compute the +// same fields one at a time and must agree with these: they feed the getters, these feed resolution. TemporalResult<CalendarFields> isoToCalendarFields(CalendarID calendarId, const ISO8601::PlainDate& isoDate) { if (calendarId == rocCalendarID() || calendarId == buddhistCalendarID()) { @@ -1150,6 +1152,41 @@ }); } +// calendarDayOfWeek — temporal_rs: Calendar::day_of_week (src/builtins/core/calendar.rs) +// https://tc39.es/proposal-temporal/#sec-temporal-calendarisotodate +// CalendarISOToDate [[DayOfWeek]] field: +// 1. (all calendars) ISODayOfWeek(isoDate). +// The 7-day cycle is unbroken across every calendar CLDR exposes, so the ISO value is also the +// calendar value; calendarId is unused and unnamed to say so. +uint8_t calendarDayOfWeek(CalendarID, const ISO8601::PlainDate& isoDate) +{ + return ISO8601::dayOfWeek(isoDate); +} + +// calendarWeekOfYear — temporal_rs: Calendar::week_of_year (src/builtins/core/calendar.rs) +// https://tc39.es/proposal-temporal/#sec-temporal-calendarisotodate +// CalendarISOToDate [[WeekOfYear]].[[Week]] field: +// 1. (iso8601) ISOWeekOfYear(isoDate).[[Week]]. +// 2. (non-ISO) ~undefined~ — no well-defined week calendar system. +std::optional<uint8_t> calendarWeekOfYear(CalendarID calendarId, const ISO8601::PlainDate& isoDate) +{ + if (calendarId != iso8601CalendarID()) + return std::nullopt; + return ISO8601::weekOfYear(isoDate); +} + +// calendarYearOfWeek — temporal_rs: Calendar::year_of_week (src/builtins/core/calendar.rs) +// https://tc39.es/proposal-temporal/#sec-temporal-calendarisotodate +// CalendarISOToDate [[WeekOfYear]].[[Year]] field: differs from [[Year]] only at a year boundary whose week belongs to the neighbouring year. +// 1. (iso8601) ISOWeekOfYear(isoDate).[[Year]]. +// 2. (non-ISO) ~undefined~ — no well-defined week calendar system. +std::optional<int32_t> calendarYearOfWeek(CalendarID calendarId, const ISO8601::PlainDate& isoDate) +{ + if (calendarId != iso8601CalendarID()) + return std::nullopt; + return ISO8601::yearOfWeek(isoDate); +} + // calendarDayOfYear — 1-based day within the calendar's year (ISO's if calendarUsesISODateArithmetic). TemporalResult<int32_t> calendarDayOfYear(CalendarID calendarId, const ISO8601::PlainDate& isoDate) {
diff --git a/Source/JavaScriptCore/runtime/temporal/core/CalendarICUBridge.h b/Source/JavaScriptCore/runtime/temporal/core/CalendarICUBridge.h index 8b988e9..5aaf8a8 100644 --- a/Source/JavaScriptCore/runtime/temporal/core/CalendarICUBridge.h +++ b/Source/JavaScriptCore/runtime/temporal/core/CalendarICUBridge.h
@@ -69,6 +69,9 @@ || id == japaneseCalendarID() || id == persianCalendarID() || id == rocCalendarID(); } +// https://tc39.es/proposal-intl-era-monthcode/#sec-temporal-calendarhasmidyeareras +inline bool calendarHasMidYearEras(CalendarID id) { return id == japaneseCalendarID(); } + // calendarIsLunisolar — true for calendars with leap months (Chinese, Dangi, Hebrew). // NOTE: temporal_rs Calendar::is_iso() returns true for ISO8601 (opposite semantic). inline bool calendarIsLunisolar(CalendarID id) @@ -88,6 +91,12 @@ JS_EXPORT_PRIVATE TemporalResult<int32_t> calendarDayOfYear(CalendarID, const ISO8601::PlainDate& isoDate); +JS_EXPORT_PRIVATE uint8_t calendarDayOfWeek(CalendarID, const ISO8601::PlainDate& isoDate); + +JS_EXPORT_PRIVATE std::optional<uint8_t> calendarWeekOfYear(CalendarID, const ISO8601::PlainDate& isoDate); + +JS_EXPORT_PRIVATE std::optional<int32_t> calendarYearOfWeek(CalendarID, const ISO8601::PlainDate& isoDate); + JS_EXPORT_PRIVATE bool isValidMonthCodeForCalendar(CalendarID, ParsedMonthCode); JS_EXPORT_PRIVATE bool yearContainsMonthCode(CalendarID, int32_t year, ParsedMonthCode);