diff --git a/Jint.Tests.Test262/Test262Harness.settings.json b/Jint.Tests.Test262/Test262Harness.settings.json
index d8d02a0005..951a2c7069 100644
--- a/Jint.Tests.Test262/Test262Harness.settings.json
+++ b/Jint.Tests.Test262/Test262Harness.settings.json
@@ -110,105 +110,29 @@
// Requires Spanish unit patterns (ICU4N doesn't include unit data, only core locale data)
"intl402/DurationFormat/prototype/format/mixed-non-numeric-styles-es.js",
- // NumberFormat - locale-specific formatting and formatRange
+ // NumberFormat - string arguments need mathematical value precision (not IEEE 754 double)
"intl402/NumberFormat/prototype/format/format-significant-digits.js",
- "intl402/NumberFormat/prototype/format/numbering-systems.js",
- "intl402/NumberFormat/prototype/format/useGrouping-extended-en-IN.js",
"intl402/NumberFormat/prototype/format/value-decimal-string.js",
- // formatRange requires locale-specific CLDR range patterns and part collapsing
+
+ // Locale fallback chains - zh-CN should match zh-Hans-CN in supportedLocalesOf
+ "intl402/fallback-locales-are-supported.js",
+ "intl402/supportedLocalesOf-consistent-with-resolvedOptions.js",
+ // NumberFormat - Indian lakh/crore grouping and compact patterns
+ "intl402/NumberFormat/prototype/format/useGrouping-extended-en-IN.js",
+ // formatRange - sign collapsing and string precision
"intl402/NumberFormat/prototype/formatRange/en-US.js",
"intl402/NumberFormat/prototype/formatRange/pt-PT.js",
"intl402/NumberFormat/prototype/formatRangeToParts/en-US.js",
- // Unit formatting formatToParts not yet implemented
+ // Unit formatting formatToParts - locale-specific unit patterns
"intl402/NumberFormat/prototype/formatToParts/unit-de-DE.js",
"intl402/NumberFormat/prototype/formatToParts/unit-ja-JP.js",
"intl402/NumberFormat/prototype/formatToParts/unit-ko-KR.js",
"intl402/NumberFormat/prototype/formatToParts/unit-zh-TW.js",
- // PluralRules - notation requires compact support
+ // PluralRules - compact notation affects plural category
"intl402/PluralRules/prototype/select/notation.js",
- // Misc intl402 tests requiring full locale support
- "intl402/fallback-locales-are-supported.js",
- "intl402/supportedLocalesOf-consistent-with-resolvedOptions.js",
-
// === INTL402 TEMPORAL EXCLUSIONS ===
- // Non-ISO calendar arithmetic not yet implemented (Chinese/Dangi lunisolar, Hebrew, Persian, etc.)
- // These require CalendarDateAdd, CalendarDateUntil, monthCode, monthsInYear, inLeapYear
- // for non-Gregorian-based calendars.
-
- // Chinese calendar - lunisolar calendar arithmetic
- "intl402/Temporal/PlainDate/prototype/inLeapYear/chinese-calendar-dates.js",
- "intl402/Temporal/PlainDate/prototype/monthCode/chinese-calendar-dates.js",
- "intl402/Temporal/PlainDate/prototype/monthsInYear/chinese-calendar-leap-days.js",
- "intl402/Temporal/PlainDate/prototype/since/chinese-calendar-dates.js",
- "intl402/Temporal/PlainDate/prototype/subtract/chinese-calendar-dates.js",
- "intl402/Temporal/PlainDate/prototype/until/chinese-calendar-dates.js",
- "intl402/Temporal/PlainDate/prototype/with/chinese-calendar-leap-dates.js",
- "intl402/Temporal/PlainDateTime/prototype/inLeapYear/chinese-calendar-dates.js",
- "intl402/Temporal/PlainDateTime/prototype/monthCode/chinese-calendar-dates.js",
- "intl402/Temporal/PlainDateTime/prototype/monthsInYear/chinese-calendar-leap-days.js",
- "intl402/Temporal/PlainDateTime/prototype/since/chinese-calendar-dates.js",
- "intl402/Temporal/PlainDateTime/prototype/until/chinese-calendar-dates.js",
- "intl402/Temporal/PlainDateTime/prototype/with/chinese-calendar-leap-dates.js",
- "intl402/Temporal/PlainMonthDay/from/chinese-calendar-dates.js",
- "intl402/Temporal/PlainMonthDay/prototype/monthCode/chinese-calendar-dates.js",
- "intl402/Temporal/PlainYearMonth/from/reference-day-chinese.js",
- "intl402/Temporal/PlainYearMonth/prototype/inLeapYear/chinese-calendar-dates.js",
- "intl402/Temporal/PlainYearMonth/prototype/monthCode/chinese-calendar-dates.js",
- "intl402/Temporal/PlainYearMonth/prototype/monthsInYear/chinese-calendar-leap-days.js",
- "intl402/Temporal/PlainYearMonth/prototype/since/chinese-calendar-dates.js",
- "intl402/Temporal/PlainYearMonth/prototype/until/chinese-calendar-dates.js",
- "intl402/Temporal/PlainYearMonth/prototype/with/chinese-calendar-leap-dates.js",
- "intl402/Temporal/ZonedDateTime/prototype/inLeapYear/chinese-calendar-dates.js",
- "intl402/Temporal/ZonedDateTime/prototype/monthCode/chinese-calendar-dates.js",
- "intl402/Temporal/ZonedDateTime/prototype/monthsInYear/chinese-calendar-leap-days.js",
- "intl402/Temporal/ZonedDateTime/prototype/since/chinese-calendar-dates.js",
- "intl402/Temporal/ZonedDateTime/prototype/until/chinese-calendar-dates.js",
- "intl402/Temporal/ZonedDateTime/prototype/with/chinese-calendar-leap-dates.js",
-
- // Dangi (Korean) calendar - lunisolar calendar arithmetic
- "intl402/Temporal/PlainDate/prototype/inLeapYear/dangi-calendar-dates.js",
- "intl402/Temporal/PlainDate/prototype/monthCode/dangi-calendar-dates.js",
- "intl402/Temporal/PlainDate/prototype/monthsInYear/dangi-calendar-leap-days.js",
- "intl402/Temporal/PlainDate/prototype/since/dangi-calendar-dates.js",
- "intl402/Temporal/PlainDate/prototype/subtract/dangi-calendar-dates.js",
- "intl402/Temporal/PlainDate/prototype/until/dangi-calendar-dates.js",
- "intl402/Temporal/PlainDate/prototype/with/dangi-calendar-leap-dates.js",
- "intl402/Temporal/PlainDateTime/prototype/inLeapYear/dangi-calendar-dates.js",
- "intl402/Temporal/PlainDateTime/prototype/monthCode/dangi-calendar-dates.js",
- "intl402/Temporal/PlainDateTime/prototype/monthsInYear/dangi-calendar-leap-days.js",
- "intl402/Temporal/PlainDateTime/prototype/since/dangi-calendar-dates.js",
- "intl402/Temporal/PlainDateTime/prototype/until/dangi-calendar-dates.js",
- "intl402/Temporal/PlainDateTime/prototype/with/dangi-calendar-leap-dates.js",
- "intl402/Temporal/PlainMonthDay/from/dangi-30-day-leap-months.js",
- "intl402/Temporal/PlainMonthDay/from/dangi-calendar-dates.js",
- "intl402/Temporal/PlainMonthDay/prototype/monthCode/dangi-calendar-dates.js",
- "intl402/Temporal/PlainYearMonth/prototype/inLeapYear/dangi-calendar-dates.js",
- "intl402/Temporal/PlainYearMonth/prototype/monthCode/dangi-calendar-dates.js",
- "intl402/Temporal/PlainYearMonth/prototype/monthsInYear/dangi-calendar-leap-days.js",
- "intl402/Temporal/PlainYearMonth/prototype/since/dangi-calendar-dates.js",
- "intl402/Temporal/PlainYearMonth/prototype/until/dangi-calendar-dates.js",
- "intl402/Temporal/PlainYearMonth/prototype/with/dangi-calendar-leap-dates.js",
- "intl402/Temporal/ZonedDateTime/prototype/inLeapYear/dangi-calendar-dates.js",
- "intl402/Temporal/ZonedDateTime/prototype/monthCode/dangi-calendar-dates.js",
- "intl402/Temporal/ZonedDateTime/prototype/monthsInYear/dangi-calendar-leap-days.js",
- "intl402/Temporal/ZonedDateTime/prototype/since/dangi-calendar-dates.js",
- "intl402/Temporal/ZonedDateTime/prototype/until/dangi-calendar-dates.js",
- "intl402/Temporal/ZonedDateTime/prototype/with/dangi-calendar-leap-dates.js",
-
- // Non-ISO calendar fields - Hebrew/multi-calendar with() operations
- "intl402/Temporal/PlainDateTime/prototype/with/non-iso-calendar-fields.js",
- "intl402/Temporal/PlainYearMonth/prototype/with/non-iso-calendar-fields.js",
- "intl402/Temporal/ZonedDateTime/prototype/with/non-iso-calendar-fields.js",
-
- // Leap year since/until across non-ISO calendars (Chinese + Hebrew)
- "intl402/Temporal/PlainDate/prototype/since/leap-year-since.js",
- "intl402/Temporal/PlainDate/prototype/until/leap-year-until.js",
- "intl402/Temporal/PlainDateTime/prototype/since/leap-year-since.js",
- "intl402/Temporal/PlainDateTime/prototype/until/leap-year-until.js",
- "intl402/Temporal/ZonedDateTime/prototype/since/leap-year-since.js",
- "intl402/Temporal/ZonedDateTime/prototype/until/leap-year-until.js",
// dateStyle with non-ISO calendar formatting (Islamic month names etc.)
"intl402/Temporal/Instant/prototype/toLocaleString/dateStyle.js",
@@ -218,56 +142,36 @@
"intl402/Temporal/PlainYearMonth/prototype/toLocaleString/dateStyle.js",
"intl402/Temporal/ZonedDateTime/prototype/toLocaleString/dateStyle.js",
- // Persian calendar arithmetic
- "intl402/Temporal/PlainDate/prototype/toString/basic-persian.js",
-
- // Calendar era handling - requires Chinese/Hebrew calendar arithmetic
- "intl402/Temporal/PlainYearMonth/from/calendar-not-supporting-eras.js",
- "intl402/Temporal/ZonedDateTime/from/calendar-not-supporting-eras.js",
-
- // Hebrew calendar arithmetic (reference-day, add/subtract with leap months)
- "intl402/Temporal/PlainYearMonth/from/reference-day-hebrew.js",
- "intl402/Temporal/PlainYearMonth/prototype/add/options-undefined.js",
- "intl402/Temporal/PlainYearMonth/prototype/subtract/options-undefined.js",
+ // .NET ar-EG locale defaults to latn numbering system instead of arab (Arabic-Indic digits)
+ "intl402/DateTimeFormat/prototype/format/temporal-objects-no-time-clip-non-latin-numerals.js",
- // Japanese calendar era boundary resolution
+ // Japanese era boundary resolution (Heisei/Showa month-day for PlainYearMonth reference day)
"intl402/Temporal/PlainYearMonth/from/reference-day-japanese.js",
- "intl402/Temporal/PlainDate/from/japanese-pre-meiji.js",
- // .NET ar-EG locale defaults to latn numbering system instead of arab (Arabic-Indic digits)
- "intl402/DateTimeFormat/prototype/format/temporal-objects-no-time-clip-non-latin-numerals.js",
+ // Leap year until - Hebrew calendar year boundary alignment
+ "intl402/Temporal/PlainDate/prototype/until/leap-year-until.js",
+ "intl402/Temporal/PlainDateTime/prototype/until/leap-year-until.js",
+ "intl402/Temporal/ZonedDateTime/prototype/until/leap-year-until.js",
- // Leap month numerical months - Chinese/Dangi/Hebrew lunisolar calendar limitations
- "intl402/Temporal/PlainDate/prototype/add/leap-month-chinese-numerical-months.js",
- "intl402/Temporal/PlainDate/prototype/add/leap-month-dangi-numerical-months.js",
- "intl402/Temporal/PlainDate/prototype/add/leap-month-hebrew-numerical-months.js",
- "intl402/Temporal/PlainDate/prototype/subtract/leap-month-chinese-numerical-months.js",
- "intl402/Temporal/PlainDate/prototype/subtract/leap-month-dangi-numerical-months.js",
- "intl402/Temporal/PlainDate/prototype/subtract/leap-month-hebrew-numerical-months.js",
- "intl402/Temporal/PlainDateTime/prototype/add/leap-month-chinese-numerical-months.js",
- "intl402/Temporal/PlainDateTime/prototype/add/leap-month-dangi-numerical-months.js",
- "intl402/Temporal/PlainDateTime/prototype/add/leap-month-hebrew-numerical-months.js",
- "intl402/Temporal/PlainDateTime/prototype/subtract/leap-month-chinese-numerical-months.js",
- "intl402/Temporal/PlainDateTime/prototype/subtract/leap-month-dangi-numerical-months.js",
- "intl402/Temporal/PlainDateTime/prototype/subtract/leap-month-hebrew-numerical-months.js",
- "intl402/Temporal/PlainYearMonth/prototype/add/leap-month-chinese-numerical-months.js",
- "intl402/Temporal/PlainYearMonth/prototype/add/leap-month-dangi-numerical-months.js",
- "intl402/Temporal/PlainYearMonth/prototype/add/leap-month-hebrew-numerical-months.js",
- "intl402/Temporal/PlainYearMonth/prototype/subtract/leap-month-chinese-numerical-months.js",
- "intl402/Temporal/PlainYearMonth/prototype/subtract/leap-month-dangi-numerical-months.js",
- "intl402/Temporal/PlainYearMonth/prototype/subtract/leap-month-hebrew-numerical-months.js",
- "intl402/Temporal/ZonedDateTime/prototype/add/leap-month-chinese-numerical-months.js",
- "intl402/Temporal/ZonedDateTime/prototype/add/leap-month-dangi-numerical-months.js",
- "intl402/Temporal/ZonedDateTime/prototype/add/leap-month-hebrew-numerical-months.js",
- "intl402/Temporal/ZonedDateTime/prototype/subtract/leap-month-chinese-numerical-months.js",
- "intl402/Temporal/ZonedDateTime/prototype/subtract/leap-month-dangi-numerical-months.js",
- "intl402/Temporal/ZonedDateTime/prototype/subtract/leap-month-hebrew-numerical-months.js",
-
- // PlainMonthDay from - multi-calendar (constrain, reference-date, reference-year)
+ // PlainMonthDay non-ISO calendar support needs more work
+ // PlainMonthDay from - reference year selection, constrain-to-leap-day, and lunisolar dates
+ "intl402/Temporal/PlainMonthDay/from/chinese-calendar-dates.js",
"intl402/Temporal/PlainMonthDay/from/constrain-to-leap-day.js",
+ "intl402/Temporal/PlainMonthDay/from/dangi-30-day-leap-months.js",
+ "intl402/Temporal/PlainMonthDay/from/dangi-calendar-dates.js",
"intl402/Temporal/PlainMonthDay/from/reference-date-noniso-calendar.js",
"intl402/Temporal/PlainMonthDay/from/reference-year-1972.js",
+ // PlainMonthDay monthCode - reference year selection affects month code for Chinese/Dangi
+ "intl402/Temporal/PlainMonthDay/prototype/monthCode/chinese-calendar-dates.js",
+ "intl402/Temporal/PlainMonthDay/prototype/monthCode/dangi-calendar-dates.js",
+
+ // PlainYearMonth non-ISO calendar reference day and since
+ "intl402/Temporal/PlainYearMonth/from/reference-day-chinese.js",
+ "intl402/Temporal/PlainYearMonth/from/reference-day-hebrew.js",
+ "intl402/Temporal/PlainYearMonth/prototype/since/chinese-calendar-dates.js",
+ "intl402/Temporal/PlainYearMonth/prototype/since/dangi-calendar-dates.js",
+
// === ANNEX B EXCLUSIONS ===
// Acornima parser limitation: HTML-like comments () in dynamic Function() bodies/params
diff --git a/Jint/Native/Temporal/NonIsoCalendars.cs b/Jint/Native/Temporal/NonIsoCalendars.cs
new file mode 100644
index 0000000000..4085a66403
--- /dev/null
+++ b/Jint/Native/Temporal/NonIsoCalendars.cs
@@ -0,0 +1,1089 @@
+using System.Globalization;
+using System.Runtime.InteropServices;
+
+namespace Jint.Native.Temporal;
+
+///
+/// Provides calendar operations for non-ISO calendars using .NET's built-in Calendar classes.
+/// Supports: Chinese, Dangi, Hebrew, Persian.
+/// Gregorian-based calendars (iso8601, gregory, japanese, roc, buddhist) are handled
+/// directly in TemporalHelpers using ISO arithmetic.
+///
+internal static class NonIsoCalendars
+{
+ // Lazy calendar instances to avoid startup overhead
+ private static ChineseLunisolarCalendar? _chineseCalendar;
+ private static KoreanLunisolarCalendar? _koreanCalendar;
+ private static HebrewCalendar? _hebrewCalendar;
+ private static PersianCalendar? _persianCalendar;
+
+ private static ChineseLunisolarCalendar ChineseCal => _chineseCalendar ??= new ChineseLunisolarCalendar();
+ private static KoreanLunisolarCalendar DangiCal => _koreanCalendar ??= new KoreanLunisolarCalendar();
+ private static HebrewCalendar HebrewCal => _hebrewCalendar ??= new HebrewCalendar();
+ private static PersianCalendar PersianCal => _persianCalendar ??= new PersianCalendar();
+
+ ///
+ /// Result of converting an ISO date to a non-ISO calendar date.
+ ///
+ [StructLayout(LayoutKind.Auto)]
+ internal readonly record struct CalendarDate(
+ int Year,
+ int Month, // ordinal month (1-based, includes leap months)
+ string MonthCode, // M01-M12 or M##L for leap months
+ int Day,
+ bool IsLeapMonth,
+ int MonthsInYear,
+ int DaysInMonth,
+ int DaysInYear,
+ bool InLeapYear);
+
+ ///
+ /// Returns true if the calendar is a non-ISO calendar supported by this adapter.
+ ///
+ internal static bool IsNonIsoCalendar(string calendar)
+ => calendar is "chinese" or "dangi" or "hebrew" or "persian";
+
+ ///
+ /// Converts an ISO date to calendar-specific fields.
+ ///
+ internal static CalendarDate IsoToCalendarDate(string calendar, in IsoDate isoDate)
+ {
+ try
+ {
+ return calendar switch
+ {
+ "chinese" => LunisolarToCalendarDate(ChineseCal, isoDate),
+ "dangi" => LunisolarToCalendarDate(DangiCal, isoDate),
+ "hebrew" => HebrewToCalendarDate(isoDate),
+ "persian" => PersianToCalendarDate(isoDate),
+ _ => throw new NotSupportedException($"Calendar '{calendar}' not supported by NonIsoCalendars")
+ };
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ // Fall back to ISO-like fields when the ISO date is outside the .NET calendar's range
+ var monthCode = $"M{isoDate.Month:D2}";
+ var daysInMonth = IsoDate.IsoDateInMonth(isoDate.Year, isoDate.Month);
+ var daysInYear = IsoDate.IsLeapYear(isoDate.Year) ? 366 : 365;
+ return new CalendarDate(isoDate.Year, isoDate.Month, monthCode, isoDate.Day, false, 12, daysInMonth, daysInYear, IsoDate.IsLeapYear(isoDate.Year));
+ }
+ }
+
+ ///
+ /// Converts a calendar date to an ISO date. Returns null if the date is invalid with overflow "reject".
+ ///
+ internal static IsoDate? CalendarDateToIso(string calendar, int year, string? monthCode, int month, int day, string overflow)
+ {
+ var result = calendar switch
+ {
+ "chinese" => LunisolarDateToIso(ChineseCal, year, monthCode, month, day, overflow),
+ "dangi" => LunisolarDateToIso(DangiCal, year, monthCode, month, day, overflow),
+ "hebrew" => HebrewDateToIso(year, monthCode, month, day, overflow),
+ "persian" => PersianDateToIso(year, monthCode, month, day, overflow),
+ _ => throw new NotSupportedException($"Calendar '{calendar}' not supported by NonIsoCalendars")
+ };
+
+ // If calendar conversion failed (e.g., year out of .NET calendar range),
+ // fall back to treating fields as ISO (best effort)
+ if (result is null && !string.Equals(overflow, "reject", StringComparison.Ordinal))
+ {
+ // Don't fall back for fundamentally invalid monthCodes (display month > 12 or < 1)
+ if (monthCode is not null)
+ {
+ var displayMonth = int.Parse(monthCode.AsSpan(1, 2), System.Globalization.NumberStyles.None, System.Globalization.CultureInfo.InvariantCulture);
+ if (displayMonth < 1 || displayMonth > 12)
+ {
+ return null;
+ }
+ }
+
+ var isoMonth = month > 0 ? month : (monthCode is not null ? int.Parse(monthCode.AsSpan(1, 2), System.Globalization.NumberStyles.None, System.Globalization.CultureInfo.InvariantCulture) : 1);
+ return TemporalHelpers.RegulateIsoDate(year, Clamp(isoMonth, 1, 12), day, overflow);
+ }
+
+ return result;
+ }
+
+ ///
+ /// Adds years and months to an ISO date using calendar-specific reckoning.
+ /// Years are added preserving monthCode semantics (not ordinal month).
+ /// Months are added as ordinal month steps.
+ ///
+ internal static IsoDate CalendarDateAdd(string calendar, in IsoDate isoDate, int years, int months, string overflow)
+ {
+ var cal = GetCalendar(calendar);
+ var calDate = IsoToCalendarDate(calendar, isoDate);
+ var newYear = calDate.Year + years;
+ int newOrdinalMonth;
+
+ // When adding years, preserve monthCode (not ordinal month)
+ if (years != 0 && calDate.IsLeapMonth)
+ {
+ // Was on a leap month - check if it exists in the new year
+ var leapOrdinal = GetLeapMonthOrdinal(calendar, cal, newYear);
+ if (leapOrdinal > 0 && GetLeapDisplayMonth(calendar, leapOrdinal) == GetLeapDisplayMonth(calendar, GetLeapMonthOrdinal(calendar, cal, calDate.Year)))
+ {
+ // Same leap month exists in new year
+ newOrdinalMonth = leapOrdinal;
+ }
+ else
+ {
+ // Leap month doesn't exist in new year
+ if (string.Equals(overflow, "reject", StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException("reject");
+ }
+
+ // Constrain to the base month (non-leap version)
+ newOrdinalMonth = MonthCodeToOrdinal(calendar, cal, newYear, calDate.MonthCode.Substring(0, 3), overflow);
+ }
+ }
+ else if (years != 0)
+ {
+ // Not a leap month - resolve the same monthCode in the new year
+ newOrdinalMonth = MonthCodeToOrdinal(calendar, cal, newYear, calDate.MonthCode, overflow);
+ }
+ else
+ {
+ newOrdinalMonth = calDate.Month;
+ }
+
+ // Add months (ordinal stepping)
+ if (months != 0)
+ {
+ newOrdinalMonth += months;
+ while (newOrdinalMonth > GetMonthsInYear(calendar, cal, newYear))
+ {
+ newOrdinalMonth -= GetMonthsInYear(calendar, cal, newYear);
+ newYear++;
+ }
+
+ while (newOrdinalMonth < 1)
+ {
+ newYear--;
+ newOrdinalMonth += GetMonthsInYear(calendar, cal, newYear);
+ }
+ }
+
+ // Constrain day
+ var maxDay = GetDaysInMonthCal(cal, newYear, newOrdinalMonth);
+ var newDay = calDate.Day;
+ if (newDay > maxDay)
+ {
+ if (string.Equals(overflow, "reject", StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException("reject");
+ }
+
+ newDay = maxDay;
+ }
+
+ // Convert back to ISO
+ try
+ {
+ var dt = cal.ToDateTime(newYear, newOrdinalMonth, newDay, 0, 0, 0, 0);
+ return new IsoDate(dt.Year, dt.Month, dt.Day);
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ if (string.Equals(overflow, "reject", StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException("reject");
+ }
+
+ // Fallback: clamp to calendar's valid range
+ return ClampToCalendarRange(cal, newYear, newOrdinalMonth, newDay);
+ }
+ }
+
+ ///
+ /// Computes the difference between two ISO dates using calendar-specific reckoning.
+ ///
+ internal static DurationRecord CalendarDateUntil(string calendar, in IsoDate one, in IsoDate two, string largestUnit)
+ {
+ var sign = TemporalHelpers.CompareIsoDates(one, two);
+ if (sign == 0)
+ {
+ return new DurationRecord(0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
+ }
+
+ // For day and week, use epoch day arithmetic (same as ISO)
+ if (largestUnit is "day" or "week")
+ {
+ var epochOne = TemporalHelpers.IsoDateToDays(one.Year, one.Month, one.Day);
+ var epochTwo = TemporalHelpers.IsoDateToDays(two.Year, two.Month, two.Day);
+ var totalDays = (int) (epochTwo - epochOne);
+
+ if (string.Equals(largestUnit, "week", StringComparison.Ordinal))
+ {
+ var w = totalDays / 7;
+ var d = totalDays - w * 7;
+ return new DurationRecord(0, 0, w, d, 0, 0, 0, 0, 0, 0);
+ }
+
+ return new DurationRecord(0, 0, 0, totalDays, 0, 0, 0, 0, 0, 0);
+ }
+
+ // Calendar-aware year/month difference
+ sign = -sign; // negate per spec
+
+ var calOne = IsoToCalendarDate(calendar, one);
+ var calTwo = IsoToCalendarDate(calendar, two);
+
+ int years = 0;
+ if (string.Equals(largestUnit, "year", StringComparison.Ordinal))
+ {
+ years = calTwo.Year - calOne.Year;
+ // Check if we overshot
+ if (years != 0)
+ {
+ var check = CalendarDateAdd(calendar, one, years, 0, "constrain");
+ if (sign > 0 && TemporalHelpers.CompareIsoDates(check, two) > 0 ||
+ sign < 0 && TemporalHelpers.CompareIsoDates(check, two) < 0)
+ {
+ years -= sign;
+ }
+ }
+ }
+
+ int months = 0;
+ if (largestUnit is "year" or "month")
+ {
+ if (string.Equals(largestUnit, "month", StringComparison.Ordinal))
+ {
+ // Estimate total months
+ var cal = GetCalendar(calendar);
+ var totalMonths = 0;
+ if (calTwo.Year == calOne.Year)
+ {
+ totalMonths = calTwo.Month - calOne.Month;
+ }
+ else
+ {
+ // Count months between years
+ for (var y = calOne.Year; y < calTwo.Year; y++)
+ {
+ totalMonths += GetMonthsInYear(calendar, cal, y);
+ }
+
+ totalMonths += calTwo.Month - calOne.Month;
+ }
+
+ months = totalMonths;
+ // Check if we overshot
+ if (months != 0)
+ {
+ var check = CalendarDateAdd(calendar, one, 0, months, "constrain");
+ if (sign > 0 && TemporalHelpers.CompareIsoDates(check, two) > 0 ||
+ sign < 0 && TemporalHelpers.CompareIsoDates(check, two) < 0)
+ {
+ months -= sign;
+ }
+ }
+ }
+ else
+ {
+ // largestUnit is "year" - count remaining months after years
+ var intermediate = CalendarDateAdd(calendar, one, years, 0, "constrain");
+ var candidateMonths = sign;
+ while (true)
+ {
+ var check = CalendarDateAdd(calendar, one, years, candidateMonths, "constrain");
+ if (sign > 0 && TemporalHelpers.CompareIsoDates(check, two) > 0 ||
+ sign < 0 && TemporalHelpers.CompareIsoDates(check, two) < 0)
+ {
+ break;
+ }
+
+ months = candidateMonths;
+ candidateMonths += sign;
+ }
+ }
+ }
+
+ // Compute remaining days
+ var afterYearsMonths = CalendarDateAdd(calendar, one, years, months, "constrain");
+ var epochIntermediate = TemporalHelpers.IsoDateToDays(afterYearsMonths.Year, afterYearsMonths.Month, afterYearsMonths.Day);
+ var epochTwoVal = TemporalHelpers.IsoDateToDays(two.Year, two.Month, two.Day);
+ var remainingDays = (int) (epochTwoVal - epochIntermediate);
+
+ return new DurationRecord(years, months, 0, remainingDays, 0, 0, 0, 0, 0, 0);
+ }
+
+ ///
+ /// Returns the era string for a non-ISO calendar.
+ ///
+ internal static string? CalendarEra(string calendar, in CalendarDate calDate)
+ {
+ return calendar switch
+ {
+ "hebrew" => "am",
+ "persian" => "ap",
+ "chinese" or "dangi" => null, // no eras
+ _ => null
+ };
+ }
+
+ ///
+ /// Returns the eraYear for a non-ISO calendar.
+ ///
+ internal static int? CalendarEraYear(string calendar, in CalendarDate calDate)
+ {
+ return calendar switch
+ {
+ "hebrew" => calDate.Year,
+ "persian" => calDate.Year,
+ "chinese" or "dangi" => null, // no eras
+ _ => null
+ };
+ }
+
+ ///
+ /// Resolves a monthCode string to an ordinal month number for a given calendar year.
+ /// Returns the ordinal month, or throws/constrains based on overflow.
+ ///
+ internal static int MonthCodeToOrdinal(string calendar, Calendar cal, int year, string monthCode, string overflow)
+ {
+ var isLeap = monthCode.Length == 4 && monthCode[3] == 'L';
+ var displayMonth = int.Parse(monthCode.AsSpan(1, 2), NumberStyles.None, System.Globalization.CultureInfo.InvariantCulture);
+
+ if (calendar is "persian")
+ {
+ // Persian has no leap months
+ if (isLeap)
+ {
+ if (string.Equals(overflow, "reject", StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException("reject");
+ }
+
+ // Constrain: use the base month
+ }
+
+ return Clamp(displayMonth, 1, 12);
+ }
+
+ var leapOrdinal = GetLeapMonthOrdinal(calendar, cal, year);
+
+ if (isLeap)
+ {
+ if (leapOrdinal <= 0)
+ {
+ // No leap month this year
+ if (string.Equals(overflow, "reject", StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException("reject");
+ }
+
+ // Constrain: use the base month
+ return displayMonth < leapOrdinal || leapOrdinal <= 0 ? displayMonth : displayMonth + 1;
+ }
+
+ var leapDisplay = GetLeapDisplayMonth(calendar, leapOrdinal);
+ if (displayMonth != leapDisplay)
+ {
+ // Wrong leap month
+ if (string.Equals(overflow, "reject", StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException("reject");
+ }
+
+ // Constrain: use the base month
+ return displayMonth < leapOrdinal ? displayMonth : displayMonth + 1;
+ }
+
+ return leapOrdinal;
+ }
+
+ // Non-leap month
+ if (leapOrdinal > 0 && displayMonth >= leapOrdinal)
+ {
+ return displayMonth + 1;
+ }
+
+ return displayMonth;
+ }
+
+ ///
+ /// Validates that month and monthCode are consistent for a non-ISO calendar.
+ /// If only monthCode is provided, resolves it to an ordinal month.
+ /// If only month is provided, uses the ordinal directly.
+ /// If both are provided, validates consistency.
+ ///
+ internal static int ResolveMonthAndMonthCode(string calendar, int year, int month, string? monthCode, string overflow)
+ {
+ var cal = GetCalendar(calendar);
+
+ if (monthCode is not null && month > 0)
+ {
+ // Both provided - validate consistency
+ var resolvedOrdinal = MonthCodeToOrdinal(calendar, cal, year, monthCode, overflow);
+ if (resolvedOrdinal != month)
+ {
+ throw new InvalidOperationException("reject"); // month and monthCode do not match
+ }
+
+ return resolvedOrdinal;
+ }
+
+ if (monthCode is not null)
+ {
+ return MonthCodeToOrdinal(calendar, cal, year, monthCode, overflow);
+ }
+
+ if (month > 0)
+ {
+ // Validate ordinal month is in range
+ var maxMonths = GetMonthsInYear(calendar, cal, year);
+ if (string.Equals(overflow, "constrain", StringComparison.Ordinal))
+ {
+ return Clamp(month, 1, maxMonths);
+ }
+
+ if (month < 1 || month > maxMonths)
+ {
+ throw new InvalidOperationException("reject");
+ }
+
+ return month;
+ }
+
+ throw new InvalidOperationException("reject"); // neither month nor monthCode provided
+ }
+
+ ///
+ /// Finds a calendar year where (year, monthCode, day) converts to an ISO date
+ /// with the given ISO reference year. Used for PlainMonthDay without explicit year.
+ ///
+ internal static int FindCalendarReferenceYear(string calendar, int isoReferenceYear, string monthCode, int day)
+ {
+ var cal = GetCalendar(calendar);
+
+ // For lunisolar calendars, a calendar year spans parts of two ISO years
+ // Try multiple calendar years around the ISO reference year
+ var approxYear = IsoYearToCalendarYear(calendar, cal, isoReferenceYear);
+
+ for (var y = approxYear - 2; y <= approxYear + 2; y++)
+ {
+ try
+ {
+ var ordinal = MonthCodeToOrdinal(calendar, cal, y, monthCode, "reject");
+ var maxDay = GetDaysInMonthCal(cal, y, ordinal);
+ var clampedDay = System.Math.Min(day, maxDay);
+ var dt = cal.ToDateTime(y, ordinal, clampedDay, 0, 0, 0, 0);
+ if (dt.Year == isoReferenceYear)
+ {
+ return y;
+ }
+ }
+ catch
+ {
+ // This year doesn't have the requested monthCode
+ }
+ }
+
+ return approxYear;
+ }
+
+ #region Private Helpers
+
+ private static int Clamp(int value, int min, int max)
+ {
+ return value < min ? min : value > max ? max : value;
+ }
+
+ ///
+ /// Determines if a Hebrew year is a leap year using the 19-year Metonic cycle.
+ /// Works for any Hebrew year, including those outside .NET HebrewCalendar's range.
+ /// In each 19-year cycle, years 3, 6, 8, 11, 14, 17, 19 are leap years.
+ ///
+ private static bool IsHebrewLeapYearAlgorithmic(int year)
+ {
+ return (7 * year + 1) % 19 < 7;
+ }
+
+ private static Calendar GetCalendar(string calendar)
+ {
+ return calendar switch
+ {
+ "chinese" => ChineseCal,
+ "dangi" => DangiCal,
+ "hebrew" => HebrewCal,
+ "persian" => PersianCal,
+ _ => throw new NotSupportedException($"Calendar '{calendar}' not supported")
+ };
+ }
+
+ ///
+ /// Converts an ISO year to an approximate calendar year.
+ ///
+ private static int IsoYearToCalendarYear(string calendar, Calendar cal, int isoYear)
+ {
+ try
+ {
+ // Use July 1 of the ISO year as a reference point
+ var dt = new DateTime(System.Math.Max(1, System.Math.Min(isoYear, 9999)), 7, 1);
+ if (dt >= cal.MinSupportedDateTime && dt <= cal.MaxSupportedDateTime)
+ {
+ return cal.GetYear(dt);
+ }
+ }
+ catch
+ {
+ // Fall through
+ }
+
+ return isoYear;
+ }
+
+ ///
+ /// Gets the ordinal position of the leap month in a given year.
+ /// Returns 0 if the year has no leap month.
+ ///
+ private static int GetLeapMonthOrdinal(string calendar, Calendar cal, int year)
+ {
+ if (calendar is "persian")
+ {
+ return 0; // Persian never has leap months
+ }
+
+ if (calendar is "hebrew")
+ {
+ try
+ {
+ return HebrewCal.IsLeapYear(year) ? 6 : 0;
+ }
+ catch
+ {
+ // Out of .NET range: use algorithmic 19-year cycle
+ // Years 3, 6, 8, 11, 14, 17, 19 of each 19-year cycle are leap years
+ return IsHebrewLeapYearAlgorithmic(year) ? 6 : 0;
+ }
+ }
+
+ // Chinese/Dangi: use EastAsianLunisolarCalendar.GetLeapMonth
+ try
+ {
+ return ((EastAsianLunisolarCalendar) cal).GetLeapMonth(year);
+ }
+ catch
+ {
+ return 0;
+ }
+ }
+
+ ///
+ /// Gets the display month number of the leap month at the given ordinal.
+ /// For Chinese/Dangi: leapOrdinal 5 → display 4 (M04L).
+ /// For Hebrew: leapOrdinal 6 → display 5 (M05L).
+ ///
+ private static int GetLeapDisplayMonth(string calendar, int leapOrdinal)
+ {
+ return leapOrdinal - 1;
+ }
+
+ ///
+ /// Gets the number of months in a calendar year.
+ ///
+ private static int GetMonthsInYear(string calendar, Calendar cal, int year)
+ {
+ if (calendar is "persian")
+ {
+ return 12;
+ }
+
+ try
+ {
+ return cal.GetMonthsInYear(year);
+ }
+ catch
+ {
+ // For Hebrew out-of-range, use algorithmic leap year detection
+ if (calendar is "hebrew")
+ {
+ return IsHebrewLeapYearAlgorithmic(year) ? 13 : 12;
+ }
+
+ return 12;
+ }
+ }
+
+ ///
+ /// Gets the number of days in a calendar month.
+ ///
+ private static int GetDaysInMonthCal(Calendar cal, int year, int month)
+ {
+ try
+ {
+ return cal.GetDaysInMonth(year, month);
+ }
+ catch
+ {
+ return 30; // fallback
+ }
+ }
+
+ ///
+ /// Clamps a date to the calendar's supported range.
+ ///
+ private static IsoDate ClampToCalendarRange(Calendar cal, int year, int month, int day)
+ {
+ var dt = cal.MaxSupportedDateTime;
+ return new IsoDate(dt.Year, dt.Month, dt.Day);
+ }
+
+ #endregion
+
+ #region Lunisolar (Chinese/Dangi) Calendar
+
+ private static CalendarDate LunisolarToCalendarDate(EastAsianLunisolarCalendar cal, in IsoDate isoDate)
+ {
+ var dt = new DateTime(isoDate.Year, isoDate.Month, isoDate.Day);
+
+ // If outside supported range, fall back to ISO-like fields
+ if (dt < cal.MinSupportedDateTime || dt > cal.MaxSupportedDateTime)
+ {
+ throw new ArgumentOutOfRangeException(nameof(isoDate));
+ }
+
+ var year = cal.GetYear(dt);
+ var ordinalMonth = cal.GetMonth(dt);
+ var day = cal.GetDayOfMonth(dt);
+ var leapMonth = cal.GetLeapMonth(year);
+
+ bool isLeapMonth;
+ string monthCode;
+
+ if (leapMonth > 0 && ordinalMonth >= leapMonth)
+ {
+ if (ordinalMonth == leapMonth)
+ {
+ var displayMonth = leapMonth - 1;
+ isLeapMonth = true;
+ monthCode = $"M{displayMonth:D2}L";
+ }
+ else
+ {
+ var displayMonth = ordinalMonth - 1;
+ isLeapMonth = false;
+ monthCode = $"M{displayMonth:D2}";
+ }
+ }
+ else
+ {
+ isLeapMonth = false;
+ monthCode = $"M{ordinalMonth:D2}";
+ }
+
+ var monthsInYear = leapMonth > 0 ? 13 : 12;
+ var daysInMonth = cal.GetDaysInMonth(year, ordinalMonth);
+ var daysInYear = cal.GetDaysInYear(year);
+ var inLeapYear = leapMonth > 0;
+
+ return new CalendarDate(year, ordinalMonth, monthCode, day, isLeapMonth, monthsInYear, daysInMonth, daysInYear, inLeapYear);
+ }
+
+ private static IsoDate? LunisolarDateToIso(EastAsianLunisolarCalendar cal, int year, string? monthCode, int month, int day, string overflow)
+ {
+ int ordinalMonth;
+
+ if (monthCode is not null)
+ {
+ var isLeap = monthCode.Length == 4 && monthCode[3] == 'L';
+ var displayMonth = int.Parse(monthCode.AsSpan(1, 2), NumberStyles.None, CultureInfo.InvariantCulture);
+
+ // Display month must be 1-12 (no calendar has display month > 12)
+ if (displayMonth < 1 || displayMonth > 12)
+ {
+ return null;
+ }
+
+ var leapMonth = 0;
+ try
+ {
+ leapMonth = cal.GetLeapMonth(year);
+ }
+ catch
+ {
+ // Year out of range
+ }
+
+ if (isLeap)
+ {
+ if (leapMonth <= 0 || leapMonth - 1 != displayMonth)
+ {
+ if (string.Equals(overflow, "reject", StringComparison.Ordinal))
+ {
+ return null;
+ }
+
+ // Constrain: use the non-leap version of the month
+ ordinalMonth = leapMonth > 0 && displayMonth >= leapMonth ? displayMonth + 1 : displayMonth;
+ }
+ else
+ {
+ ordinalMonth = leapMonth;
+ }
+ }
+ else
+ {
+ if (leapMonth > 0 && displayMonth >= leapMonth)
+ {
+ ordinalMonth = displayMonth + 1;
+ }
+ else
+ {
+ ordinalMonth = displayMonth;
+ }
+ }
+
+ // Validate month matches ordinal if both provided
+ if (month > 0 && month != ordinalMonth)
+ {
+ if (string.Equals(overflow, "reject", StringComparison.Ordinal))
+ {
+ return null;
+ }
+
+ // monthCode takes precedence
+ }
+ }
+ else
+ {
+ ordinalMonth = month;
+ }
+
+ // Constrain ordinal month to valid range
+ var maxMonths = 12;
+ try
+ {
+ maxMonths = cal.GetLeapMonth(year) > 0 ? 13 : 12;
+ }
+ catch
+ {
+ // Year out of range
+ }
+
+ if (string.Equals(overflow, "constrain", StringComparison.Ordinal))
+ {
+ ordinalMonth = Clamp(ordinalMonth, 1, maxMonths);
+ }
+ else if (ordinalMonth < 1 || ordinalMonth > maxMonths)
+ {
+ return null;
+ }
+
+ // Constrain day
+ int maxDay;
+ try
+ {
+ maxDay = cal.GetDaysInMonth(year, ordinalMonth);
+ }
+ catch
+ {
+ maxDay = 30;
+ }
+
+ if (string.Equals(overflow, "constrain", StringComparison.Ordinal))
+ {
+ day = Clamp(day, 1, maxDay);
+ }
+ else if (day < 1 || day > maxDay)
+ {
+ return null;
+ }
+
+ try
+ {
+ var dt = cal.ToDateTime(year, ordinalMonth, day, 0, 0, 0, 0);
+ return new IsoDate(dt.Year, dt.Month, dt.Day);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ #endregion
+
+ #region Hebrew Calendar
+
+ private static CalendarDate HebrewToCalendarDate(in IsoDate isoDate)
+ {
+ var cal = HebrewCal;
+ var dt = new DateTime(isoDate.Year, isoDate.Month, isoDate.Day);
+
+ if (dt < cal.MinSupportedDateTime || dt > cal.MaxSupportedDateTime)
+ {
+ throw new ArgumentOutOfRangeException(nameof(isoDate));
+ }
+
+ var year = cal.GetYear(dt);
+ var ordinalMonth = cal.GetMonth(dt);
+ var day = cal.GetDayOfMonth(dt);
+ var isLeapYear = cal.IsLeapYear(year);
+
+ bool isLeapMonth;
+ string monthCode;
+
+ if (isLeapYear)
+ {
+ // Leap year: 13 months
+ // ordinals 1-5 → M01-M05
+ // ordinal 6 → M05L (Adar I, the intercalary month)
+ // ordinals 7-13 → M06-M12
+ if (ordinalMonth <= 5)
+ {
+ monthCode = $"M{ordinalMonth:D2}";
+ isLeapMonth = false;
+ }
+ else if (ordinalMonth == 6)
+ {
+ monthCode = "M05L";
+ isLeapMonth = true;
+ }
+ else
+ {
+ monthCode = $"M{ordinalMonth - 1:D2}";
+ isLeapMonth = false;
+ }
+ }
+ else
+ {
+ // Non-leap year: 12 months, ordinals 1-12 → M01-M12
+ monthCode = $"M{ordinalMonth:D2}";
+ isLeapMonth = false;
+ }
+
+ var monthsInYear = isLeapYear ? 13 : 12;
+ var daysInMonth = cal.GetDaysInMonth(year, ordinalMonth);
+ var daysInYear = cal.GetDaysInYear(year);
+
+ return new CalendarDate(year, ordinalMonth, monthCode, day, isLeapMonth, monthsInYear, daysInMonth, daysInYear, isLeapYear);
+ }
+
+ private static IsoDate? HebrewDateToIso(int year, string? monthCode, int month, int day, string overflow)
+ {
+ var cal = HebrewCal;
+ int ordinalMonth;
+
+ if (monthCode is not null)
+ {
+ var isLeap = monthCode.Length == 4 && monthCode[3] == 'L';
+ var displayMonth = int.Parse(monthCode.AsSpan(1, 2), NumberStyles.None, CultureInfo.InvariantCulture);
+
+ // Display month must be 1-12
+ if (displayMonth < 1 || displayMonth > 12)
+ {
+ return null;
+ }
+
+ bool yearIsLeap;
+ try
+ {
+ yearIsLeap = cal.IsLeapYear(year);
+ }
+ catch
+ {
+ yearIsLeap = IsHebrewLeapYearAlgorithmic(year);
+ }
+
+ if (isLeap)
+ {
+ if (!yearIsLeap || displayMonth != 5)
+ {
+ // M05L only exists in leap years
+ if (string.Equals(overflow, "reject", StringComparison.Ordinal))
+ {
+ return null;
+ }
+
+ // Constrain to M05 (non-leap version)
+ ordinalMonth = 5;
+ }
+ else
+ {
+ ordinalMonth = 6; // M05L → ordinal 6
+ }
+ }
+ else
+ {
+ if (yearIsLeap && displayMonth >= 6)
+ {
+ ordinalMonth = displayMonth + 1; // M06 → ordinal 7, etc.
+ }
+ else
+ {
+ ordinalMonth = displayMonth;
+ }
+ }
+
+ // Validate month matches ordinal if both provided
+ if (month > 0 && month != ordinalMonth)
+ {
+ if (string.Equals(overflow, "reject", StringComparison.Ordinal))
+ {
+ return null;
+ }
+ }
+ }
+ else
+ {
+ ordinalMonth = month;
+ }
+
+ // Constrain ordinal month
+ int maxMonths;
+ try
+ {
+ maxMonths = cal.IsLeapYear(year) ? 13 : 12;
+ }
+ catch
+ {
+ maxMonths = 12;
+ }
+
+ if (string.Equals(overflow, "constrain", StringComparison.Ordinal))
+ {
+ ordinalMonth = Clamp(ordinalMonth, 1, maxMonths);
+ }
+ else if (ordinalMonth < 1 || ordinalMonth > maxMonths)
+ {
+ return null;
+ }
+
+ // Constrain day
+ int maxDay;
+ try
+ {
+ maxDay = cal.GetDaysInMonth(year, ordinalMonth);
+ }
+ catch
+ {
+ maxDay = 30;
+ }
+
+ if (string.Equals(overflow, "constrain", StringComparison.Ordinal))
+ {
+ day = Clamp(day, 1, maxDay);
+ }
+ else if (day < 1 || day > maxDay)
+ {
+ return null;
+ }
+
+ try
+ {
+ var dt = cal.ToDateTime(year, ordinalMonth, day, 0, 0, 0, 0);
+ return new IsoDate(dt.Year, dt.Month, dt.Day);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ #endregion
+
+ #region Persian Calendar
+
+ private static CalendarDate PersianToCalendarDate(in IsoDate isoDate)
+ {
+ var cal = PersianCal;
+ var dt = new DateTime(isoDate.Year, isoDate.Month, isoDate.Day);
+
+ if (dt < cal.MinSupportedDateTime || dt > cal.MaxSupportedDateTime)
+ {
+ throw new ArgumentOutOfRangeException(nameof(isoDate));
+ }
+
+ var year = cal.GetYear(dt);
+ var ordinalMonth = cal.GetMonth(dt);
+ var day = cal.GetDayOfMonth(dt);
+ var isLeapYear = cal.IsLeapYear(year);
+
+ var monthCode = $"M{ordinalMonth:D2}";
+ var daysInMonth = cal.GetDaysInMonth(year, ordinalMonth);
+ var daysInYear = cal.GetDaysInYear(year);
+
+ return new CalendarDate(year, ordinalMonth, monthCode, day, false, 12, daysInMonth, daysInYear, isLeapYear);
+ }
+
+ private static IsoDate? PersianDateToIso(int year, string? monthCode, int month, int day, string overflow)
+ {
+ var cal = PersianCal;
+ var ordinalMonth = month;
+
+ if (monthCode is not null)
+ {
+ var isLeap = monthCode.Length == 4 && monthCode[3] == 'L';
+ var displayMonth = int.Parse(monthCode.AsSpan(1, 2), NumberStyles.None, CultureInfo.InvariantCulture);
+
+ if (isLeap)
+ {
+ // Persian has no leap months
+ if (string.Equals(overflow, "reject", StringComparison.Ordinal))
+ {
+ return null;
+ }
+
+ ordinalMonth = displayMonth;
+ }
+ else
+ {
+ ordinalMonth = displayMonth;
+ }
+
+ if (month > 0 && month != ordinalMonth)
+ {
+ if (string.Equals(overflow, "reject", StringComparison.Ordinal))
+ {
+ return null;
+ }
+ }
+ }
+
+ // Constrain month
+ if (string.Equals(overflow, "constrain", StringComparison.Ordinal))
+ {
+ ordinalMonth = Clamp(ordinalMonth, 1, 12);
+ }
+ else if (ordinalMonth < 1 || ordinalMonth > 12)
+ {
+ return null;
+ }
+
+ // Constrain day
+ int maxDay;
+ try
+ {
+ maxDay = cal.GetDaysInMonth(year, ordinalMonth);
+ }
+ catch
+ {
+ // Fallback: Persian months 1-6=31, 7-11=30, 12=29/30
+ maxDay = ordinalMonth <= 6 ? 31 : ordinalMonth <= 11 ? 30 : 29;
+ }
+
+ if (string.Equals(overflow, "constrain", StringComparison.Ordinal))
+ {
+ day = Clamp(day, 1, maxDay);
+ }
+ else if (day < 1 || day > maxDay)
+ {
+ return null;
+ }
+
+ try
+ {
+ var dt = cal.ToDateTime(year, ordinalMonth, day, 0, 0, 0, 0);
+ return new IsoDate(dt.Year, dt.Month, dt.Day);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ #endregion
+}
diff --git a/Jint/Native/Temporal/PlainDate/PlainDateConstructor.cs b/Jint/Native/Temporal/PlainDate/PlainDateConstructor.cs
index 05327598da..62f3672b62 100644
--- a/Jint/Native/Temporal/PlainDate/PlainDateConstructor.cs
+++ b/Jint/Native/Temporal/PlainDate/PlainDateConstructor.cs
@@ -161,13 +161,17 @@ private JsPlainDate ToTemporalDateFromObjectWithOptions(ObjectInstance obj, JsVa
// Validate well-formedness (format) - this happens before year type validation
monthFromCode = TemporalHelpers.ParseMonthCode(_realm, monthCodeStr);
- // If both month and monthCode are provided, they must match
- if (month != 0 && month != monthFromCode)
+ // If both month and monthCode are provided, they must match (ISO only)
+ // For non-ISO calendars, ordinal month ≠ display month (e.g., month 5 = M04L)
+ if (!NonIsoCalendars.IsNonIsoCalendar(calendar) && month != 0 && month != monthFromCode)
{
Throw.RangeError(_realm, "month and monthCode do not match");
}
- month = monthFromCode;
+ if (!NonIsoCalendars.IsNonIsoCalendar(calendar))
+ {
+ month = monthFromCode;
+ }
}
// 5. year - use eraYear if computed, otherwise read from property
@@ -193,27 +197,30 @@ private JsPlainDate ToTemporalDateFromObjectWithOptions(ObjectInstance obj, JsVa
// 6. Read options AFTER all fields (but BEFORE algorithmic validation)
var overflow = TemporalHelpers.GetOverflowOption(_realm, options);
- // Validate monthCode suitability - only for ISO/Gregorian calendars
- if (monthCodeStr is not null && TemporalHelpers.IsGregorianBasedCalendar(calendar))
+ // Validate monthCode suitability
+ if (monthCodeStr is not null)
{
- // For ISO 8601 calendar: validate monthCode is valid (01-12, no leap months)
- if (monthCodeStr.Length == 4 && monthCodeStr[3] == 'L')
+ if (TemporalHelpers.IsGregorianBasedCalendar(calendar))
{
- Throw.RangeError(_realm, $"Leap months are not valid for ISO 8601 calendar: {monthCodeStr}");
- }
+ // For ISO 8601 calendar: validate monthCode is valid (01-12, no leap months)
+ if (monthCodeStr.Length == 4 && monthCodeStr[3] == 'L')
+ {
+ Throw.RangeError(_realm, $"Leap months are not valid for ISO 8601 calendar: {monthCodeStr}");
+ }
- if (monthFromCode < 1 || monthFromCode > 12)
- {
- Throw.RangeError(_realm, $"Month {monthFromCode} is not valid for ISO 8601 calendar");
+ if (monthFromCode < 1 || monthFromCode > 12)
+ {
+ Throw.RangeError(_realm, $"Month {monthFromCode} is not valid for ISO 8601 calendar");
+ }
}
}
- if (month == 0)
+ if (month == 0 && monthCodeStr is null)
{
Throw.TypeError(_realm, "month or monthCode is required");
}
- var date = TemporalHelpers.CalendarDateToISO(_realm, calendar, year, month, day, overflow);
+ var date = TemporalHelpers.CalendarDateToISO(_realm, calendar, year, month, day, overflow, monthCodeStr);
if (date is null)
{
Throw.RangeError(_realm, "Invalid date");
diff --git a/Jint/Native/Temporal/PlainDate/PlainDatePrototype.cs b/Jint/Native/Temporal/PlainDate/PlainDatePrototype.cs
index 50bc54b850..662f849a81 100644
--- a/Jint/Native/Temporal/PlainDate/PlainDatePrototype.cs
+++ b/Jint/Native/Temporal/PlainDate/PlainDatePrototype.cs
@@ -88,25 +88,37 @@ private JsPlainDate ValidatePlainDate(JsValue thisObject)
private JsValue GetEra(JsValue thisObject, JsCallArguments arguments)
{
var pd = ValidatePlainDate(thisObject);
- var era = TemporalHelpers.CalendarEra(pd.Calendar, pd.IsoDate.Year);
+ var era = TemporalHelpers.CalendarEra(pd.Calendar, pd.IsoDate);
return era is not null ? new JsString(era) : Undefined;
}
private JsValue GetEraYear(JsValue thisObject, JsCallArguments arguments)
{
var pd = ValidatePlainDate(thisObject);
- var eraYear = TemporalHelpers.CalendarEraYear(pd.Calendar, pd.IsoDate.Year);
+ var eraYear = TemporalHelpers.CalendarEraYear(pd.Calendar, pd.IsoDate);
return eraYear.HasValue ? JsNumber.Create(eraYear.Value) : Undefined;
}
private JsNumber GetYear(JsValue thisObject, JsCallArguments arguments)
{
var pd = ValidatePlainDate(thisObject);
- return JsNumber.Create(TemporalHelpers.CalendarYear(pd.Calendar, pd.IsoDate.Year));
+ return JsNumber.Create(TemporalHelpers.CalendarYear(pd.Calendar, pd.IsoDate));
+ }
+ private JsNumber GetMonth(JsValue thisObject, JsCallArguments arguments)
+ {
+ var pd = ValidatePlainDate(thisObject);
+ return JsNumber.Create(TemporalHelpers.CalendarMonth(pd.Calendar, pd.IsoDate));
+ }
+ private JsString GetMonthCode(JsValue thisObject, JsCallArguments arguments)
+ {
+ var pd = ValidatePlainDate(thisObject);
+ return new JsString(TemporalHelpers.CalendarMonthCode(pd.Calendar, pd.IsoDate));
+ }
+ private JsNumber GetDay(JsValue thisObject, JsCallArguments arguments)
+ {
+ var pd = ValidatePlainDate(thisObject);
+ return JsNumber.Create(TemporalHelpers.CalendarDay(pd.Calendar, pd.IsoDate));
}
- private JsNumber GetMonth(JsValue thisObject, JsCallArguments arguments) => JsNumber.Create(ValidatePlainDate(thisObject).IsoDate.Month);
- private JsString GetMonthCode(JsValue thisObject, JsCallArguments arguments) => new JsString($"M{ValidatePlainDate(thisObject).IsoDate.Month:D2}");
- private JsNumber GetDay(JsValue thisObject, JsCallArguments arguments) => JsNumber.Create(ValidatePlainDate(thisObject).IsoDate.Day);
private JsNumber GetDayOfWeek(JsValue thisObject, JsCallArguments arguments) => JsNumber.Create(ValidatePlainDate(thisObject).IsoDate.DayOfWeek());
private JsNumber GetDayOfYear(JsValue thisObject, JsCallArguments arguments) => JsNumber.Create(ValidatePlainDate(thisObject).IsoDate.DayOfYear());
private JsValue GetWeekOfYear(JsValue thisObject, JsCallArguments arguments)
@@ -127,16 +139,28 @@ private JsNumber GetDaysInWeek(JsValue thisObject, JsCallArguments arguments)
return JsNumber.Create(7);
}
- private JsNumber GetDaysInMonth(JsValue thisObject, JsCallArguments arguments) => JsNumber.Create(ValidatePlainDate(thisObject).IsoDate.DaysInMonth());
- private JsNumber GetDaysInYear(JsValue thisObject, JsCallArguments arguments) => JsNumber.Create(ValidatePlainDate(thisObject).IsoDate.DaysInYear());
+ private JsNumber GetDaysInMonth(JsValue thisObject, JsCallArguments arguments)
+ {
+ var pd = ValidatePlainDate(thisObject);
+ return JsNumber.Create(TemporalHelpers.CalendarDaysInMonth(pd.Calendar, pd.IsoDate));
+ }
+ private JsNumber GetDaysInYear(JsValue thisObject, JsCallArguments arguments)
+ {
+ var pd = ValidatePlainDate(thisObject);
+ return JsNumber.Create(TemporalHelpers.CalendarDaysInYear(pd.Calendar, pd.IsoDate));
+ }
private JsNumber GetMonthsInYear(JsValue thisObject, JsCallArguments arguments)
{
- ValidatePlainDate(thisObject);
- return JsNumber.Create(12);
+ var pd = ValidatePlainDate(thisObject);
+ return JsNumber.Create(TemporalHelpers.CalendarMonthsInYear(pd.Calendar, pd.IsoDate));
}
- private JsBoolean GetInLeapYear(JsValue thisObject, JsCallArguments arguments) => IsoDate.IsLeapYear(ValidatePlainDate(thisObject).IsoDate.Year) ? JsBoolean.True : JsBoolean.False;
+ private JsBoolean GetInLeapYear(JsValue thisObject, JsCallArguments arguments)
+ {
+ var pd = ValidatePlainDate(thisObject);
+ return TemporalHelpers.CalendarInLeapYear(pd.Calendar, pd.IsoDate) ? JsBoolean.True : JsBoolean.False;
+ }
///
/// https://tc39.es/proposal-temporal/#sec-temporal.plaindate.prototype.with
@@ -228,10 +252,49 @@ private JsPlainDate With(JsValue thisObject, JsCallArguments arguments)
var options = GetOptionsObject(optionsArg);
var overflow = TemporalHelpers.GetOverflowOption(_realm, (JsValue?) options ?? JsValue.Undefined);
- // Merge with existing date values
+ // Handle non-ISO calendars
+ if (NonIsoCalendars.IsNonIsoCalendar(plainDate.Calendar))
+ {
+ // Get current calendar fields
+ var calDate = NonIsoCalendars.IsoToCalendarDate(plainDate.Calendar, plainDate.IsoDate);
+
+ // Merge with provided fields
+ var finalCalYear = year ?? calDate.Year;
+ var finalCalDay = day ?? calDate.Day;
+
+ // monthCode: use explicit if provided, else default from current date
+ // BUT: if month is explicitly provided without monthCode, don't use default monthCode
+ // (month takes precedence as ordinal month)
+ string? finalMonthCode;
+ if (monthCode is not null)
+ {
+ TemporalHelpers.ParseMonthCode(_realm, monthCode);
+ finalMonthCode = monthCode;
+ }
+ else if (month.HasValue)
+ {
+ // Explicit month provided, no monthCode → use month as ordinal, no monthCode
+ finalMonthCode = null;
+ }
+ else
+ {
+ finalMonthCode = calDate.MonthCode;
+ }
+
+ var date = TemporalHelpers.CalendarDateToISO(_realm, plainDate.Calendar, finalCalYear,
+ month ?? 0, finalCalDay, overflow, finalMonthCode);
+ if (date is null)
+ {
+ Throw.RangeError(_realm, "Invalid date");
+ }
+
+ return _constructor.Construct(date.Value, plainDate.Calendar);
+ }
+
+ // Merge with existing date values (ISO/Gregorian path)
var finalYear = year ?? plainDate.IsoDate.Year;
var finalMonth = month ?? plainDate.IsoDate.Month;
- var finalDay = day ?? plainDate.IsoDate.Day;
+ var finalDayVal = day ?? plainDate.IsoDate.Day;
// Handle monthCode - if provided, validate and use it
// Validation happens AFTER options are read
@@ -266,23 +329,19 @@ private JsPlainDate With(JsValue thisObject, JsCallArguments arguments)
}
// Check for fundamentally invalid values
- // - Year out of supported range (-271821 to 275760)
- // - Month or day <= 0 (negative or zero)
- // These throw RangeError even with overflow="constrain"
- // But allow constrainable values like month=13 or day=32
- if (finalYear < -271821 || finalYear > 275760 || finalMonth < 1 || finalDay < 1)
+ if (finalYear < -271821 || finalYear > 275760 || finalMonth < 1 || finalDayVal < 1)
{
Throw.RangeError(_realm, "Invalid date");
}
// Apply regulation with user's overflow option
- var date = TemporalHelpers.RegulateIsoDate(finalYear, finalMonth, finalDay, overflow);
- if (date is null)
+ var date2 = TemporalHelpers.RegulateIsoDate(finalYear, finalMonth, finalDayVal, overflow);
+ if (date2 is null)
{
Throw.RangeError(_realm, "Invalid date");
}
- return _constructor.Construct(date.Value, plainDate.Calendar);
+ return _constructor.Construct(date2.Value, plainDate.Calendar);
}
@@ -335,15 +394,25 @@ private JsPlainDate Subtract(JsValue thisObject, JsCallArguments arguments)
private JsPlainDate AddDurationToDate(JsPlainDate plainDate, DurationRecord duration, string overflow)
{
- var newDate = TemporalHelpers.AddDurationToDate(plainDate.IsoDate, duration);
+ // Balance time units into days first (for PlainDate, time components become extra days)
+ var totalNanoseconds = TemporalHelpers.TimeDurationFromComponents(duration);
+ var balancedDays = (long) (totalNanoseconds / TemporalHelpers.NanosecondsPerDay);
+
+ // Create a date-only duration with balanced days
+ var dateDuration = new DurationRecord(
+ duration.Years, duration.Months, duration.Weeks,
+ duration.Days + balancedDays, 0, 0, 0, 0, 0, 0);
+
+ // Use calendar-aware date addition
+ var newDate = TemporalHelpers.CalendarDateAdd(_realm, plainDate.Calendar, plainDate.IsoDate, dateDuration, overflow);
- var regulated = TemporalHelpers.RegulateIsoDate(newDate.Year, newDate.Month, newDate.Day, overflow);
- if (regulated is null)
+ // Validate the result is within Temporal's representable range
+ if (!TemporalHelpers.IsValidIsoDateTime(newDate.Year, newDate.Month, newDate.Day))
{
- Throw.RangeError(_realm, "Invalid date after addition");
+ Throw.RangeError(_realm, "Date is outside the valid range");
}
- return _constructor.Construct(regulated.Value, plainDate.Calendar);
+ return _constructor.Construct(newDate, plainDate.Calendar);
}
///
diff --git a/Jint/Native/Temporal/PlainDateTime/PlainDateTimeConstructor.cs b/Jint/Native/Temporal/PlainDateTime/PlainDateTimeConstructor.cs
index da331ba054..27ac313543 100644
--- a/Jint/Native/Temporal/PlainDateTime/PlainDateTimeConstructor.cs
+++ b/Jint/Native/Temporal/PlainDateTime/PlainDateTimeConstructor.cs
@@ -371,19 +371,25 @@ private JsPlainDateTime ToTemporalDateTimeFromFields(ObjectInstance obj, JsValue
}
// Now validate and combine month/monthCode
- // Validate: both month and monthCode provided - they must match
- if (month != 0 && monthFromCode.HasValue && month != monthFromCode.Value)
+ if (monthCodeStr is not null)
{
- Throw.RangeError(_realm, "month and monthCode must match");
- }
+ if (TemporalHelpers.IsGregorianBasedCalendar(calendar))
+ {
+ // Validate: both month and monthCode provided - they must match
+ if (month != 0 && monthFromCode.HasValue && month != monthFromCode.Value)
+ {
+ Throw.RangeError(_realm, "month and monthCode must match");
+ }
- // Use whichever is provided
- if (monthFromCode.HasValue)
- {
- month = monthFromCode.Value;
+ // For ISO, use parsed month number
+ if (monthFromCode.HasValue)
+ {
+ month = monthFromCode.Value;
+ }
+ }
}
- if (month == 0)
+ if (month == 0 && monthCodeStr is null)
{
Throw.TypeError(_realm, "month or monthCode is required");
}
@@ -394,7 +400,7 @@ private JsPlainDateTime ToTemporalDateTimeFromFields(ObjectInstance obj, JsValue
Throw.RangeError(_realm, "Date is outside valid range");
}
- var date = TemporalHelpers.CalendarDateToISO(_realm, calendar, year, month, day, overflow);
+ var date = TemporalHelpers.CalendarDateToISO(_realm, calendar, year, month, day, overflow, monthCodeStr);
if (date is null)
{
Throw.RangeError(_realm, "Invalid date");
diff --git a/Jint/Native/Temporal/PlainDateTime/PlainDateTimePrototype.cs b/Jint/Native/Temporal/PlainDateTime/PlainDateTimePrototype.cs
index 0a30f467aa..9e8c1f0682 100644
--- a/Jint/Native/Temporal/PlainDateTime/PlainDateTimePrototype.cs
+++ b/Jint/Native/Temporal/PlainDateTime/PlainDateTimePrototype.cs
@@ -96,25 +96,37 @@ private JsPlainDateTime ValidatePlainDateTime(JsValue thisObject)
private JsValue GetEra(JsValue thisObject, JsCallArguments arguments)
{
var pdt = ValidatePlainDateTime(thisObject);
- var era = TemporalHelpers.CalendarEra(pdt.Calendar, pdt.IsoDateTime.Year);
+ var era = TemporalHelpers.CalendarEra(pdt.Calendar, pdt.IsoDateTime.Date);
return era is not null ? new JsString(era) : Undefined;
}
private JsValue GetEraYear(JsValue thisObject, JsCallArguments arguments)
{
var pdt = ValidatePlainDateTime(thisObject);
- var eraYear = TemporalHelpers.CalendarEraYear(pdt.Calendar, pdt.IsoDateTime.Year);
+ var eraYear = TemporalHelpers.CalendarEraYear(pdt.Calendar, pdt.IsoDateTime.Date);
return eraYear.HasValue ? JsNumber.Create(eraYear.Value) : Undefined;
}
private JsNumber GetYear(JsValue thisObject, JsCallArguments arguments)
{
var pdt = ValidatePlainDateTime(thisObject);
- return JsNumber.Create(TemporalHelpers.CalendarYear(pdt.Calendar, pdt.IsoDateTime.Year));
+ return JsNumber.Create(TemporalHelpers.CalendarYear(pdt.Calendar, pdt.IsoDateTime.Date));
+ }
+ private JsNumber GetMonth(JsValue thisObject, JsCallArguments arguments)
+ {
+ var pdt = ValidatePlainDateTime(thisObject);
+ return JsNumber.Create(TemporalHelpers.CalendarMonth(pdt.Calendar, pdt.IsoDateTime.Date));
+ }
+ private JsString GetMonthCode(JsValue thisObject, JsCallArguments arguments)
+ {
+ var pdt = ValidatePlainDateTime(thisObject);
+ return new JsString(TemporalHelpers.CalendarMonthCode(pdt.Calendar, pdt.IsoDateTime.Date));
+ }
+ private JsNumber GetDay(JsValue thisObject, JsCallArguments arguments)
+ {
+ var pdt = ValidatePlainDateTime(thisObject);
+ return JsNumber.Create(TemporalHelpers.CalendarDay(pdt.Calendar, pdt.IsoDateTime.Date));
}
- private JsNumber GetMonth(JsValue thisObject, JsCallArguments arguments) => JsNumber.Create(ValidatePlainDateTime(thisObject).IsoDateTime.Month);
- private JsString GetMonthCode(JsValue thisObject, JsCallArguments arguments) => new JsString($"M{ValidatePlainDateTime(thisObject).IsoDateTime.Month:D2}");
- private JsNumber GetDay(JsValue thisObject, JsCallArguments arguments) => JsNumber.Create(ValidatePlainDateTime(thisObject).IsoDateTime.Day);
private JsNumber GetHour(JsValue thisObject, JsCallArguments arguments) => JsNumber.Create(ValidatePlainDateTime(thisObject).IsoDateTime.Hour);
private JsNumber GetMinute(JsValue thisObject, JsCallArguments arguments) => JsNumber.Create(ValidatePlainDateTime(thisObject).IsoDateTime.Minute);
private JsNumber GetSecond(JsValue thisObject, JsCallArguments arguments) => JsNumber.Create(ValidatePlainDateTime(thisObject).IsoDateTime.Second);
@@ -141,16 +153,28 @@ private JsNumber GetDaysInWeek(JsValue thisObject, JsCallArguments arguments)
return JsNumber.Create(7);
}
- private JsNumber GetDaysInMonth(JsValue thisObject, JsCallArguments arguments) => JsNumber.Create(ValidatePlainDateTime(thisObject).IsoDateTime.Date.DaysInMonth());
- private JsNumber GetDaysInYear(JsValue thisObject, JsCallArguments arguments) => JsNumber.Create(ValidatePlainDateTime(thisObject).IsoDateTime.Date.DaysInYear());
+ private JsNumber GetDaysInMonth(JsValue thisObject, JsCallArguments arguments)
+ {
+ var pdt = ValidatePlainDateTime(thisObject);
+ return JsNumber.Create(TemporalHelpers.CalendarDaysInMonth(pdt.Calendar, pdt.IsoDateTime.Date));
+ }
+ private JsNumber GetDaysInYear(JsValue thisObject, JsCallArguments arguments)
+ {
+ var pdt = ValidatePlainDateTime(thisObject);
+ return JsNumber.Create(TemporalHelpers.CalendarDaysInYear(pdt.Calendar, pdt.IsoDateTime.Date));
+ }
private JsNumber GetMonthsInYear(JsValue thisObject, JsCallArguments arguments)
{
- ValidatePlainDateTime(thisObject);
- return JsNumber.Create(12);
+ var pdt = ValidatePlainDateTime(thisObject);
+ return JsNumber.Create(TemporalHelpers.CalendarMonthsInYear(pdt.Calendar, pdt.IsoDateTime.Date));
}
- private JsBoolean GetInLeapYear(JsValue thisObject, JsCallArguments arguments) => IsoDate.IsLeapYear(ValidatePlainDateTime(thisObject).IsoDateTime.Year) ? JsBoolean.True : JsBoolean.False;
+ private JsBoolean GetInLeapYear(JsValue thisObject, JsCallArguments arguments)
+ {
+ var pdt = ValidatePlainDateTime(thisObject);
+ return TemporalHelpers.CalendarInLeapYear(pdt.Calendar, pdt.IsoDateTime.Date) ? JsBoolean.True : JsBoolean.False;
+ }
///
/// https://tc39.es/proposal-temporal/#sec-temporal.plaindatetime.prototype.with
@@ -204,7 +228,12 @@ private JsPlainDateTime With(JsValue thisObject, JsCallArguments arguments)
any = true;
day = TemporalHelpers.ToPositiveIntegerWithTruncation(_realm, v);
}
- else { day = plainDateTime.IsoDateTime.Day; }
+ else
+ {
+ day = NonIsoCalendars.IsNonIsoCalendar(plainDateTime.Calendar)
+ ? TemporalHelpers.CalendarDay(plainDateTime.Calendar, plainDateTime.IsoDateTime.Date)
+ : plainDateTime.IsoDateTime.Day;
+ }
v = obj.Get("hour");
int hour;
@@ -244,12 +273,19 @@ private JsPlainDateTime With(JsValue thisObject, JsCallArguments arguments)
v = obj.Get("month");
int month;
+ var monthExplicit = false;
if (!v.IsUndefined())
{
any = true;
+ monthExplicit = true;
month = TemporalHelpers.ToPositiveIntegerWithTruncation(_realm, v);
}
- else { month = plainDateTime.IsoDateTime.Month; }
+ else
+ {
+ month = NonIsoCalendars.IsNonIsoCalendar(plainDateTime.Calendar)
+ ? TemporalHelpers.CalendarMonth(plainDateTime.Calendar, plainDateTime.IsoDateTime.Date)
+ : plainDateTime.IsoDateTime.Month;
+ }
v = obj.Get("monthCode");
string? monthCode = null;
@@ -258,6 +294,10 @@ private JsPlainDateTime With(JsValue thisObject, JsCallArguments arguments)
any = true;
monthCode = TypeConverter.ToString(v);
}
+ else if (NonIsoCalendars.IsNonIsoCalendar(plainDateTime.Calendar) && !monthExplicit)
+ {
+ monthCode = TemporalHelpers.CalendarMonthCode(plainDateTime.Calendar, plainDateTime.IsoDateTime.Date);
+ }
v = obj.Get("nanosecond");
int nanosecond;
@@ -284,7 +324,12 @@ private JsPlainDateTime With(JsValue thisObject, JsCallArguments arguments)
any = true;
year = ConvertToInteger(v, "year");
}
- else { year = plainDateTime.IsoDateTime.Year; }
+ else
+ {
+ year = NonIsoCalendars.IsNonIsoCalendar(plainDateTime.Calendar)
+ ? TemporalHelpers.CalendarYear(plainDateTime.Calendar, plainDateTime.IsoDateTime.Date)
+ : plainDateTime.IsoDateTime.Year;
+ }
if (!any)
{
@@ -299,33 +344,47 @@ private JsPlainDateTime With(JsValue thisObject, JsCallArguments arguments)
int? monthFromCode = null;
if (monthCode is not null)
{
- if (monthCode.Length >= 2 && monthCode[0] == 'M')
+ // Use ParseMonthCode which properly handles "L" suffix for leap months
+ monthFromCode = TemporalHelpers.ParseMonthCode(_realm, monthCode);
+
+ // ISO/Gregorian calendars don't support leap months
+ if (!NonIsoCalendars.IsNonIsoCalendar(plainDateTime.Calendar))
{
- if (int.TryParse(monthCode.AsSpan(1), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedMonth))
+ if (monthCode.Length == 4 && monthCode[3] == 'L')
{
- monthFromCode = parsedMonth;
+ Throw.RangeError(_realm, $"Leap months are not valid for calendar: {monthCode}");
}
- }
- if (!monthFromCode.HasValue)
- {
- Throw.RangeError(_realm, $"Invalid monthCode: {monthCode}");
+ if (monthFromCode.Value < 1 || monthFromCode.Value > 12)
+ {
+ Throw.RangeError(_realm, $"Month {monthFromCode.Value} is not valid for calendar");
+ }
}
}
- // Validate month/monthCode consistency
- if (monthFromCode.HasValue && month != plainDateTime.IsoDateTime.Month && month != monthFromCode.Value)
+ // Validate month/monthCode consistency (ISO calendars only - non-ISO ordinal ≠ display)
+ if (!NonIsoCalendars.IsNonIsoCalendar(plainDateTime.Calendar) &&
+ monthFromCode.HasValue && month != plainDateTime.IsoDateTime.Month && month != monthFromCode.Value)
{
Throw.RangeError(_realm, "month and monthCode must match");
}
- // Use monthCode if provided
- if (monthFromCode.HasValue)
+ // Use monthCode if provided (for ISO calendars)
+ if (monthFromCode.HasValue && !NonIsoCalendars.IsNonIsoCalendar(plainDateTime.Calendar))
{
month = monthFromCode.Value;
}
- var date = TemporalHelpers.RegulateIsoDate(year, month, day, overflow);
+ IsoDate? date;
+ if (NonIsoCalendars.IsNonIsoCalendar(plainDateTime.Calendar))
+ {
+ date = TemporalHelpers.CalendarDateToISO(_realm, plainDateTime.Calendar, year, month, day, overflow, monthCode);
+ }
+ else
+ {
+ date = TemporalHelpers.RegulateIsoDate(year, month, day, overflow);
+ }
+
if (date is null)
{
Throw.RangeError(_realm, "Invalid date");
@@ -439,19 +498,15 @@ private JsPlainDateTime AddDurationToDateTime(JsPlainDateTime plainDateTime, Dur
Throw.RangeError(_realm, "Date is outside the valid range");
}
- // Step 4: Add date part with adjusted days
- var newDate = TemporalHelpers.AddDurationToDateCore(plainDateTime.IsoDateTime.Date, duration, (long) dayOverflow);
-
- // Step 5: Regulate and validate the result
- var regulated = TemporalHelpers.RegulateIsoDate(newDate.Year, newDate.Month, newDate.Day, overflow);
- if (regulated is null)
- {
- Throw.RangeError(_realm, "Invalid date after addition");
- }
+ // Step 4: Add date part using calendar-aware arithmetic
+ var adjustedDuration = new DurationRecord(
+ duration.Years, duration.Months, duration.Weeks,
+ duration.Days + (long) dayOverflow, 0, 0, 0, 0, 0, 0);
+ var regulated = TemporalHelpers.CalendarDateAdd(_realm, plainDateTime.Calendar, plainDateTime.IsoDateTime.Date, adjustedDuration, overflow);
// Step 6: Construct validates ISODateTimeWithinLimits
var newTime = IsoTime.FromNanoseconds((long) timeNs);
- return _constructor.Construct(new IsoDateTime(regulated.Value, newTime), plainDateTime.Calendar);
+ return _constructor.Construct(new IsoDateTime(regulated, newTime), plainDateTime.Calendar);
}
///
diff --git a/Jint/Native/Temporal/PlainMonthDay/PlainMonthDayConstructor.cs b/Jint/Native/Temporal/PlainMonthDay/PlainMonthDayConstructor.cs
index 7e185bf069..a57cf0afa2 100644
--- a/Jint/Native/Temporal/PlainMonthDay/PlainMonthDayConstructor.cs
+++ b/Jint/Native/Temporal/PlainMonthDay/PlainMonthDayConstructor.cs
@@ -293,19 +293,20 @@ private JsPlainMonthDay ToTemporalMonthDayFromFields(ObjectInstance obj, JsValue
Throw.TypeError(_realm, "monthCode is required for non-ISO calendars when year is not provided");
}
- // Validate: both month and monthCode provided - they must match
- if (month != 0 && monthFromCode.HasValue && month != monthFromCode.Value)
+ // Validate: both month and monthCode provided - they must match (ISO only)
+ // For non-ISO calendars, ordinal month ≠ display month (e.g., month 5 = M04L)
+ if (!NonIsoCalendars.IsNonIsoCalendar(calendar) && month != 0 && monthFromCode.HasValue && month != monthFromCode.Value)
{
Throw.RangeError(_realm, "month and monthCode must match");
}
- // Use whichever is provided
- if (monthFromCode.HasValue)
+ // Use whichever is provided (ISO only - non-ISO uses monthCode in CalendarDateToISO)
+ if (!NonIsoCalendars.IsNonIsoCalendar(calendar) && monthFromCode.HasValue)
{
month = monthFromCode.Value;
}
- if (month == 0)
+ if (month == 0 && monthCodeStr is null)
{
Throw.TypeError(_realm, "month or monthCode is required");
}
@@ -316,9 +317,9 @@ private JsPlainMonthDay ToTemporalMonthDayFromFields(ObjectInstance obj, JsValue
// When year is not explicitly provided, find the calendar year that maps to ISO 1972
var calendarYear = yearExplicitlyProvided
? year
- : TemporalHelpers.FindCalendarReferenceYear(calendar, 1972, month, day);
+ : TemporalHelpers.FindCalendarReferenceYear(calendar, 1972, month, day, monthCodeStr);
- var calDate = TemporalHelpers.CalendarDateToISO(_realm, calendar, calendarYear, month, day, overflow);
+ var calDate = TemporalHelpers.CalendarDateToISO(_realm, calendar, calendarYear, month, day, overflow, monthCodeStr);
if (calDate is null)
{
Throw.RangeError(_realm, "Invalid month-day");
diff --git a/Jint/Native/Temporal/PlainMonthDay/PlainMonthDayPrototype.cs b/Jint/Native/Temporal/PlainMonthDay/PlainMonthDayPrototype.cs
index ba4130b326..250a703315 100644
--- a/Jint/Native/Temporal/PlainMonthDay/PlainMonthDayPrototype.cs
+++ b/Jint/Native/Temporal/PlainMonthDay/PlainMonthDayPrototype.cs
@@ -61,8 +61,16 @@ private JsPlainMonthDay ValidatePlainMonthDay(JsValue thisObject)
// Getters
private JsString GetCalendarId(JsValue thisObject, JsCallArguments arguments) => new JsString(ValidatePlainMonthDay(thisObject).Calendar);
- private JsString GetMonthCode(JsValue thisObject, JsCallArguments arguments) => new JsString($"M{ValidatePlainMonthDay(thisObject).IsoDate.Month:D2}");
- private JsNumber GetDay(JsValue thisObject, JsCallArguments arguments) => JsNumber.Create(ValidatePlainMonthDay(thisObject).IsoDate.Day);
+ private JsString GetMonthCode(JsValue thisObject, JsCallArguments arguments)
+ {
+ var md = ValidatePlainMonthDay(thisObject);
+ return new JsString(TemporalHelpers.CalendarMonthCode(md.Calendar, md.IsoDate));
+ }
+ private JsNumber GetDay(JsValue thisObject, JsCallArguments arguments)
+ {
+ var md = ValidatePlainMonthDay(thisObject);
+ return JsNumber.Create(TemporalHelpers.CalendarDay(md.Calendar, md.IsoDate));
+ }
///
/// https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.prototype.with
@@ -101,13 +109,19 @@ private JsPlainMonthDay With(JsValue thisObject, JsCallArguments arguments)
}
// Read and convert properties in strict alphabetical order per spec: day, month, monthCode, year
+ var isNonIso = NonIsoCalendars.IsNonIsoCalendar(md.Calendar);
+
// 1. day
var dayProp = obj.Get("day");
- var day = dayProp.IsUndefined() ? md.IsoDate.Day : TemporalHelpers.ToPositiveIntegerWithTruncation(_realm, dayProp);
+ var day = dayProp.IsUndefined()
+ ? (isNonIso ? TemporalHelpers.CalendarDay(md.Calendar, md.IsoDate) : md.IsoDate.Day)
+ : TemporalHelpers.ToPositiveIntegerWithTruncation(_realm, dayProp);
// 2. month
var monthProp = obj.Get("month");
- var month = monthProp.IsUndefined() ? md.IsoDate.Month : TemporalHelpers.ToPositiveIntegerWithTruncation(_realm, monthProp);
+ var month = monthProp.IsUndefined()
+ ? (isNonIso ? TemporalHelpers.CalendarMonth(md.Calendar, md.IsoDate) : md.IsoDate.Month)
+ : TemporalHelpers.ToPositiveIntegerWithTruncation(_realm, monthProp);
// 3. monthCode - read but don't validate yet
var monthCodeProp = obj.Get("monthCode");
@@ -160,39 +174,62 @@ private JsPlainMonthDay With(JsValue thisObject, JsCallArguments arguments)
Throw.TypeError(_realm, "monthCode is required for non-ISO calendars");
}
+ // Default monthCode from calendar when not provided and month not explicitly set
+ if (monthCodeProp.IsUndefined() && monthProp.IsUndefined() && isNonIso)
+ {
+ monthCode = TemporalHelpers.CalendarMonthCode(md.Calendar, md.IsoDate);
+ }
+
// NOW validate monthCode (after options are read)
int? monthFromCode = null;
if (monthCode is not null)
{
monthFromCode = TemporalHelpers.ParseMonthCode(_realm, monthCode);
- // For ISO 8601 calendar: validate monthCode is valid (01-12, no leap months)
- if (monthCode.Length == 4 && monthCode[3] == 'L')
+ // For ISO/Gregorian calendars: validate monthCode is valid (01-12, no leap months)
+ if (!isNonIso)
{
- Throw.RangeError(_realm, $"Leap months are not valid for ISO 8601 calendar: {monthCode}");
- }
+ if (monthCode.Length == 4 && monthCode[3] == 'L')
+ {
+ Throw.RangeError(_realm, $"Leap months are not valid for calendar: {monthCode}");
+ }
- if (monthFromCode.Value < 1 || monthFromCode.Value > 12)
- {
- Throw.RangeError(_realm, $"Month {monthFromCode.Value} is not valid for ISO 8601 calendar");
+ if (monthFromCode.Value < 1 || monthFromCode.Value > 12)
+ {
+ Throw.RangeError(_realm, $"Month {monthFromCode.Value} is not valid for calendar");
+ }
}
}
- // Validate month/monthCode consistency
- if (monthFromCode.HasValue && !monthProp.IsUndefined() && month != monthFromCode.Value)
+ // Validate month/monthCode consistency (ISO only)
+ if (!isNonIso && monthFromCode.HasValue && !monthProp.IsUndefined() && month != monthFromCode.Value)
{
Throw.RangeError(_realm, "month and monthCode must match");
}
- // Use monthCode if provided
- if (monthFromCode.HasValue)
+ // Use monthCode if provided (ISO only - non-ISO handled by CalendarDateToISO)
+ if (!isNonIso && monthFromCode.HasValue)
{
month = monthFromCode.Value;
}
- // Validate using the provided year (important for leap day validation)
- // For PlainMonthDay with iso8601 calendar, the year is only used for overflow,
- // not range-checked (per spec CalendarMonthDayToISOReferenceDate)
+ if (isNonIso)
+ {
+ // For non-ISO calendars, use calendar-aware date construction
+ var calYear = isNonIso && yearProp.IsUndefined()
+ ? TemporalHelpers.CalendarYear(md.Calendar, md.IsoDate)
+ : year;
+ var calDate = TemporalHelpers.CalendarDateToISO(_realm, md.Calendar, calYear,
+ monthProp.IsUndefined() ? 0 : month, day, overflow, monthCode);
+ if (calDate is null)
+ {
+ Throw.RangeError(_realm, "Invalid month-day");
+ }
+
+ return _constructor.Construct(calDate.Value, md.Calendar);
+ }
+
+ // ISO path: validate using the provided year
var skipRangeCheck = string.Equals(md.Calendar, "iso8601", StringComparison.Ordinal);
var date = TemporalHelpers.RegulateIsoDate(year, month, day, overflow, skipRangeCheck);
if (date is null)
@@ -200,8 +237,6 @@ private JsPlainMonthDay With(JsValue thisObject, JsCallArguments arguments)
Throw.RangeError(_realm, "Invalid month-day");
}
- // Choose reference year for result (per spec: not the same as the validation year)
- // Use 1972 (a leap year) to ensure Feb 29 is always valid
var referenceYear = 1972;
var resultDate = new IsoDate(referenceYear, date.Value.Month, date.Value.Day);
diff --git a/Jint/Native/Temporal/PlainYearMonth/PlainYearMonthConstructor.cs b/Jint/Native/Temporal/PlainYearMonth/PlainYearMonthConstructor.cs
index cca58d790d..81917da0eb 100644
--- a/Jint/Native/Temporal/PlainYearMonth/PlainYearMonthConstructor.cs
+++ b/Jint/Native/Temporal/PlainYearMonth/PlainYearMonthConstructor.cs
@@ -281,13 +281,16 @@ private JsPlainYearMonth ToTemporalYearMonthFromObjectWithOptions(ObjectInstance
// Validate well-formedness (format) - this happens before year type validation
monthFromCode = TemporalHelpers.ParseMonthCode(_realm, monthCodeStr);
- // If both month and monthCode are provided, they must match
- if (month != 0 && month != monthFromCode.Value)
+ // If both month and monthCode are provided, they must match (ISO only)
+ if (!NonIsoCalendars.IsNonIsoCalendar(calendar) && month != 0 && month != monthFromCode.Value)
{
Throw.RangeError(_realm, "month and monthCode must match");
}
- month = monthFromCode.Value;
+ if (!NonIsoCalendars.IsNonIsoCalendar(calendar))
+ {
+ month = monthFromCode.Value;
+ }
}
// 5. year - use eraYear if computed, otherwise read from property
@@ -327,7 +330,7 @@ private JsPlainYearMonth ToTemporalYearMonthFromObjectWithOptions(ObjectInstance
}
// At least one of month or monthCode is required
- if (month == 0)
+ if (month == 0 && monthCodeStr is null)
{
Throw.TypeError(_realm, "month or monthCode is required");
}
@@ -335,7 +338,7 @@ private JsPlainYearMonth ToTemporalYearMonthFromObjectWithOptions(ObjectInstance
// For non-ISO calendars, convert calendar year/month to ISO
if (!TemporalHelpers.IsGregorianBasedCalendar(calendar))
{
- var date = TemporalHelpers.CalendarDateToISO(_realm, calendar, year, month, 1, overflow);
+ var date = TemporalHelpers.CalendarDateToISO(_realm, calendar, year, month, 1, overflow, monthCodeStr);
if (date is null)
{
Throw.RangeError(_realm, "Invalid year-month");
@@ -431,13 +434,16 @@ private JsPlainYearMonth ToTemporalYearMonthFromFields(ObjectInstance obj, strin
// Validate well-formedness (format) - this happens before year type validation
monthFromCode = TemporalHelpers.ParseMonthCode(_realm, monthCodeStr);
- // If both month and monthCode are provided, they must match
- if (month != 0 && month != monthFromCode.Value)
+ // If both month and monthCode are provided, they must match (ISO only)
+ if (!NonIsoCalendars.IsNonIsoCalendar(calendar) && month != 0 && month != monthFromCode.Value)
{
Throw.RangeError(_realm, "month and monthCode must match");
}
- month = monthFromCode.Value;
+ if (!NonIsoCalendars.IsNonIsoCalendar(calendar))
+ {
+ month = monthFromCode.Value;
+ }
}
// 5. year - use eraYear if computed, otherwise read from property
@@ -474,7 +480,7 @@ private JsPlainYearMonth ToTemporalYearMonthFromFields(ObjectInstance obj, strin
}
// At least one of month or monthCode is required
- if (month == 0)
+ if (month == 0 && monthCodeStr is null)
{
Throw.TypeError(_realm, "month or monthCode is required");
}
@@ -484,7 +490,7 @@ private JsPlainYearMonth ToTemporalYearMonthFromFields(ObjectInstance obj, strin
// For non-ISO calendars, convert calendar year/month to ISO
if (!TemporalHelpers.IsGregorianBasedCalendar(calendar))
{
- var date = TemporalHelpers.CalendarDateToISO(_realm, calendar, year, month, 1, overflow);
+ var date = TemporalHelpers.CalendarDateToISO(_realm, calendar, year, month, 1, overflow, monthCodeStr);
if (date is null)
{
Throw.RangeError(_realm, "Invalid year-month");
diff --git a/Jint/Native/Temporal/PlainYearMonth/PlainYearMonthPrototype.cs b/Jint/Native/Temporal/PlainYearMonth/PlainYearMonthPrototype.cs
index 0e34364b77..c3848fbda9 100644
--- a/Jint/Native/Temporal/PlainYearMonth/PlainYearMonthPrototype.cs
+++ b/Jint/Native/Temporal/PlainYearMonth/PlainYearMonthPrototype.cs
@@ -75,28 +75,48 @@ private JsPlainYearMonth ValidatePlainYearMonth(JsValue thisObject)
private JsNumber GetYear(JsValue thisObject, JsCallArguments arguments)
{
var ym = ValidatePlainYearMonth(thisObject);
- return JsNumber.Create(TemporalHelpers.CalendarYear(ym.Calendar, ym.IsoDate.Year));
+ return JsNumber.Create(TemporalHelpers.CalendarYear(ym.Calendar, ym.IsoDate));
+ }
+ private JsNumber GetMonth(JsValue thisObject, JsCallArguments arguments)
+ {
+ var ym = ValidatePlainYearMonth(thisObject);
+ return JsNumber.Create(TemporalHelpers.CalendarMonth(ym.Calendar, ym.IsoDate));
+ }
+ private JsString GetMonthCode(JsValue thisObject, JsCallArguments arguments)
+ {
+ var ym = ValidatePlainYearMonth(thisObject);
+ return new JsString(TemporalHelpers.CalendarMonthCode(ym.Calendar, ym.IsoDate));
+ }
+ private JsNumber GetDaysInMonth(JsValue thisObject, JsCallArguments arguments)
+ {
+ var ym = ValidatePlainYearMonth(thisObject);
+ return JsNumber.Create(TemporalHelpers.CalendarDaysInMonth(ym.Calendar, ym.IsoDate));
+ }
+ private JsNumber GetDaysInYear(JsValue thisObject, JsCallArguments arguments)
+ {
+ var ym = ValidatePlainYearMonth(thisObject);
+ return JsNumber.Create(TemporalHelpers.CalendarDaysInYear(ym.Calendar, ym.IsoDate));
}
- private JsNumber GetMonth(JsValue thisObject, JsCallArguments arguments) => JsNumber.Create(ValidatePlainYearMonth(thisObject).IsoDate.Month);
- private JsString GetMonthCode(JsValue thisObject, JsCallArguments arguments) => new JsString($"M{ValidatePlainYearMonth(thisObject).IsoDate.Month:D2}");
- private JsNumber GetDaysInMonth(JsValue thisObject, JsCallArguments arguments) => JsNumber.Create(ValidatePlainYearMonth(thisObject).IsoDate.DaysInMonth());
- private JsNumber GetDaysInYear(JsValue thisObject, JsCallArguments arguments) => JsNumber.Create(ValidatePlainYearMonth(thisObject).IsoDate.DaysInYear());
private JsNumber GetMonthsInYear(JsValue thisObject, JsCallArguments arguments)
{
- ValidatePlainYearMonth(thisObject);
- return JsNumber.Create(12);
+ var ym = ValidatePlainYearMonth(thisObject);
+ return JsNumber.Create(TemporalHelpers.CalendarMonthsInYear(ym.Calendar, ym.IsoDate));
+ }
+ private JsBoolean GetInLeapYear(JsValue thisObject, JsCallArguments arguments)
+ {
+ var ym = ValidatePlainYearMonth(thisObject);
+ return TemporalHelpers.CalendarInLeapYear(ym.Calendar, ym.IsoDate) ? JsBoolean.True : JsBoolean.False;
}
- private JsBoolean GetInLeapYear(JsValue thisObject, JsCallArguments arguments) => IsoDate.IsLeapYear(ValidatePlainYearMonth(thisObject).IsoDate.Year) ? JsBoolean.True : JsBoolean.False;
private JsValue GetEraYear(JsValue thisObject, JsCallArguments arguments)
{
var ym = ValidatePlainYearMonth(thisObject);
- var eraYear = TemporalHelpers.CalendarEraYear(ym.Calendar, ym.IsoDate.Year);
+ var eraYear = TemporalHelpers.CalendarEraYear(ym.Calendar, ym.IsoDate);
return eraYear.HasValue ? JsNumber.Create(eraYear.Value) : Undefined;
}
private JsValue GetEra(JsValue thisObject, JsCallArguments arguments)
{
var ym = ValidatePlainYearMonth(thisObject);
- var era = TemporalHelpers.CalendarEra(ym.Calendar, ym.IsoDate.Year);
+ var era = TemporalHelpers.CalendarEra(ym.Calendar, ym.IsoDate);
return era is not null ? new JsString(era) : Undefined;
}
@@ -138,14 +158,20 @@ private JsPlainYearMonth With(JsValue thisObject, JsCallArguments arguments)
// Read and convert properties in alphabetical order with immediate conversion per spec
// Order: month, monthCode, year
- var year = ym.IsoDate.Year;
- var month = ym.IsoDate.Month;
+ var isNonIso = NonIsoCalendars.IsNonIsoCalendar(ym.Calendar);
var monthProp = obj.Get("month");
+ int month;
if (!monthProp.IsUndefined())
{
month = TemporalHelpers.ToPositiveIntegerWithTruncation(_realm, monthProp);
}
+ else
+ {
+ month = isNonIso
+ ? TemporalHelpers.CalendarMonth(ym.Calendar, ym.IsoDate)
+ : ym.IsoDate.Month;
+ }
var monthCodeProp = obj.Get("monthCode");
string? monthCode = null;
@@ -176,12 +202,25 @@ private JsPlainYearMonth With(JsValue thisObject, JsCallArguments arguments)
monthCode = TypeConverter.ToString(monthCodeProp);
}
}
+ else if (isNonIso && monthProp.IsUndefined())
+ {
+ // Only default monthCode from the calendar when month was not explicitly provided;
+ // if the user set month explicitly, let month drive the conversion without monthCode
+ monthCode = TemporalHelpers.CalendarMonthCode(ym.Calendar, ym.IsoDate);
+ }
var yearProp = obj.Get("year");
+ int year;
if (!yearProp.IsUndefined())
{
year = TemporalHelpers.ToIntegerWithTruncationAsInt(_realm, yearProp);
}
+ else
+ {
+ year = isNonIso
+ ? TemporalHelpers.CalendarYear(ym.Calendar, ym.IsoDate)
+ : ym.IsoDate.Year;
+ }
// Validate that at least one temporal field was provided (IsPartialTemporalObject)
if (monthProp.IsUndefined() && monthCodeProp.IsUndefined() && yearProp.IsUndefined())
@@ -192,7 +231,30 @@ private JsPlainYearMonth With(JsValue thisObject, JsCallArguments arguments)
// Read options BEFORE any validation (per spec)
var overflow = TemporalHelpers.GetOverflowOption(_realm, options);
- // NOW validate monthCode (after options are read)
+ // Handle non-ISO calendars via CalendarDateToISO
+ if (isNonIso)
+ {
+ // Validate monthCode well-formedness if explicitly provided
+ if (!monthCodeProp.IsUndefined() && monthCode is not null)
+ {
+ TemporalHelpers.ParseMonthCode(_realm, monthCode);
+ }
+
+ var date = TemporalHelpers.CalendarDateToISO(_realm, ym.Calendar, year, month, 1, overflow, monthCode);
+ if (date is null)
+ {
+ Throw.RangeError(_realm, "Invalid year-month");
+ }
+
+ if (!TemporalHelpers.ISOYearMonthWithinLimits(date.Value.Year, date.Value.Month))
+ {
+ Throw.RangeError(_realm, "Year-month is outside the representable range");
+ }
+
+ return _constructor.Construct(date.Value, ym.Calendar);
+ }
+
+ // ISO calendar path
int? parsedMonthFromCode = null;
if (monthCode is not null)
{
@@ -290,29 +352,17 @@ private JsPlainYearMonth AddDurationToYearMonth(JsPlainYearMonth ym, DurationRec
var intermediateDate = new IsoDate(ym.IsoDate.Year, ym.IsoDate.Month, 1);
TemporalHelpers.CheckISODaysRange(_realm, intermediateDate);
- // Add years and months
- var currentYear = ym.IsoDate.Year + (int) duration.Years;
- var currentMonth = ym.IsoDate.Month + (int) duration.Months;
-
- // Normalize months
- while (currentMonth > 12)
- {
- currentMonth -= 12;
- currentYear++;
- }
- while (currentMonth < 1)
- {
- currentMonth += 12;
- currentYear--;
- }
+ // Use calendar-aware date addition
+ var yearMonthDuration = new DurationRecord(duration.Years, duration.Months, 0, 0, 0, 0, 0, 0, 0, 0);
+ var resultDate = TemporalHelpers.CalendarDateAdd(_realm, ym.Calendar, ym.IsoDate, yearMonthDuration, overflow);
// Validate the result is within Temporal's representable range
- if (!TemporalHelpers.ISOYearMonthWithinLimits(currentYear, currentMonth))
+ if (!TemporalHelpers.ISOYearMonthWithinLimits(resultDate.Year, resultDate.Month))
{
Throw.RangeError(_realm, "Year-month result is outside the representable range");
}
- return _constructor.Construct(new IsoDate(currentYear, currentMonth, 1), ym.Calendar);
+ return _constructor.Construct(resultDate, ym.Calendar);
}
///
@@ -618,7 +668,21 @@ private JsPlainDate ToPlainDate(JsValue thisObject, JsCallArguments arguments)
}
var day = TemporalHelpers.ToPositiveIntegerWithTruncation(_realm, dayProp);
- var date = TemporalHelpers.RegulateIsoDate(ym.IsoDate.Year, ym.IsoDate.Month, day, "constrain");
+
+ IsoDate? date;
+ if (NonIsoCalendars.IsNonIsoCalendar(ym.Calendar))
+ {
+ // For non-ISO calendars, get the calendar year/month and combine with the provided day
+ var calYear = TemporalHelpers.CalendarYear(ym.Calendar, ym.IsoDate);
+ var calMonth = TemporalHelpers.CalendarMonth(ym.Calendar, ym.IsoDate);
+ var calMonthCode = TemporalHelpers.CalendarMonthCode(ym.Calendar, ym.IsoDate);
+ date = TemporalHelpers.CalendarDateToISO(_realm, ym.Calendar, calYear, calMonth, day, "constrain", calMonthCode);
+ }
+ else
+ {
+ date = TemporalHelpers.RegulateIsoDate(ym.IsoDate.Year, ym.IsoDate.Month, day, "constrain");
+ }
+
if (date is null)
{
Throw.RangeError(_realm, "Invalid date");
diff --git a/Jint/Native/Temporal/TemporalHelpers.cs b/Jint/Native/Temporal/TemporalHelpers.cs
index 0302077472..5c3a09ad9b 100644
--- a/Jint/Native/Temporal/TemporalHelpers.cs
+++ b/Jint/Native/Temporal/TemporalHelpers.cs
@@ -1612,13 +1612,18 @@ public static bool ISOYearMonthWithinLimits(int year, int month)
/// For non-ISO calendars, the input fields are in the calendar's system
/// and need to be converted to the proleptic Gregorian (ISO 8601) calendar.
///
- public static IsoDate? CalendarDateToISO(Realm realm, string calendar, int year, int month, int day, string overflow)
+ public static IsoDate? CalendarDateToISO(Realm realm, string calendar, int year, int month, int day, string overflow, string? monthCode = null)
{
if (calendar is "iso8601" or "gregory")
{
return RegulateIsoDate(year, month, day, overflow);
}
+ if (NonIsoCalendars.IsNonIsoCalendar(calendar))
+ {
+ return NonIsoCalendars.CalendarDateToIso(calendar, year, monthCode, month, day, overflow);
+ }
+
if (calendar is "islamic-civil")
{
return IslamicCivilDateToISO(year, month, day, overflow);
@@ -1638,8 +1643,13 @@ public static bool ISOYearMonthWithinLimits(int year, int month)
/// converting (calendarYear, month, day) to ISO produces an ISO date with the given
/// ISO reference year (typically 1972).
///
- internal static int FindCalendarReferenceYear(string calendar, int isoReferenceYear, int month, int day)
+ internal static int FindCalendarReferenceYear(string calendar, int isoReferenceYear, int month, int day, string? monthCode = null)
{
+ if (NonIsoCalendars.IsNonIsoCalendar(calendar) && monthCode is not null)
+ {
+ return NonIsoCalendars.FindCalendarReferenceYear(calendar, isoReferenceYear, monthCode, day);
+ }
+
if (calendar is "islamic-civil" or "islamic-tbla")
{
// Approximate Islamic year for a given ISO year
@@ -1819,12 +1829,12 @@ internal static bool CalendarUsesEras(string calendar)
case "buddhist":
return "be";
case "japanese":
- // Simplified: use CE/BCE for years outside specific eras
+ // Temporal uses 1873 as Meiji start (when Japan adopted Gregorian calendar)
if (isoYear >= 2019) return "reiwa";
if (isoYear >= 1989) return "heisei";
if (isoYear >= 1926) return "showa";
if (isoYear >= 1912) return "taisho";
- if (isoYear >= 1868) return "meiji";
+ if (isoYear >= 1873) return "meiji";
return isoYear >= 1 ? "ce" : "bce";
default:
return null;
@@ -1849,7 +1859,7 @@ internal static bool CalendarUsesEras(string calendar)
if (isoYear >= 1989) return isoYear - 1988;
if (isoYear >= 1926) return isoYear - 1925;
if (isoYear >= 1912) return isoYear - 1911;
- if (isoYear >= 1868) return isoYear - 1867;
+ if (isoYear >= 1873) return isoYear - 1872;
return isoYear >= 1 ? isoYear : 1 - isoYear;
default:
return null;
@@ -1877,6 +1887,189 @@ internal static int CalendarYear(string calendar, int isoYear)
}
}
+ ///
+ /// Returns the calendar-specific year for any calendar, including non-ISO calendars.
+ /// For non-ISO calendars, converts the ISO date to a calendar date first.
+ ///
+ internal static int CalendarYear(string calendar, in IsoDate isoDate)
+ {
+ if (IsGregorianBasedCalendar(calendar))
+ {
+ return CalendarYear(calendar, isoDate.Year);
+ }
+
+ if (NonIsoCalendars.IsNonIsoCalendar(calendar))
+ {
+ return NonIsoCalendars.IsoToCalendarDate(calendar, isoDate).Year;
+ }
+
+ return isoDate.Year;
+ }
+
+ ///
+ /// Returns the calendar-specific ordinal month for a date.
+ ///
+ internal static int CalendarMonth(string calendar, in IsoDate isoDate)
+ {
+ if (IsGregorianBasedCalendar(calendar))
+ {
+ return isoDate.Month;
+ }
+
+ if (NonIsoCalendars.IsNonIsoCalendar(calendar))
+ {
+ return NonIsoCalendars.IsoToCalendarDate(calendar, isoDate).Month;
+ }
+
+ return isoDate.Month;
+ }
+
+ ///
+ /// Returns the calendar-specific monthCode for a date (e.g., "M01", "M05L").
+ ///
+ internal static string CalendarMonthCode(string calendar, in IsoDate isoDate)
+ {
+ if (IsGregorianBasedCalendar(calendar))
+ {
+ return $"M{isoDate.Month:D2}";
+ }
+
+ if (NonIsoCalendars.IsNonIsoCalendar(calendar))
+ {
+ return NonIsoCalendars.IsoToCalendarDate(calendar, isoDate).MonthCode;
+ }
+
+ return $"M{isoDate.Month:D2}";
+ }
+
+ ///
+ /// Returns the calendar-specific day for a date.
+ ///
+ internal static int CalendarDay(string calendar, in IsoDate isoDate)
+ {
+ if (IsGregorianBasedCalendar(calendar))
+ {
+ return isoDate.Day;
+ }
+
+ if (NonIsoCalendars.IsNonIsoCalendar(calendar))
+ {
+ return NonIsoCalendars.IsoToCalendarDate(calendar, isoDate).Day;
+ }
+
+ return isoDate.Day;
+ }
+
+ ///
+ /// Returns the number of days in the calendar month containing the given ISO date.
+ ///
+ internal static int CalendarDaysInMonth(string calendar, in IsoDate isoDate)
+ {
+ if (IsGregorianBasedCalendar(calendar))
+ {
+ return isoDate.DaysInMonth();
+ }
+
+ if (NonIsoCalendars.IsNonIsoCalendar(calendar))
+ {
+ return NonIsoCalendars.IsoToCalendarDate(calendar, isoDate).DaysInMonth;
+ }
+
+ return isoDate.DaysInMonth();
+ }
+
+ ///
+ /// Returns the number of days in the calendar year containing the given ISO date.
+ ///
+ internal static int CalendarDaysInYear(string calendar, in IsoDate isoDate)
+ {
+ if (IsGregorianBasedCalendar(calendar))
+ {
+ return isoDate.DaysInYear();
+ }
+
+ if (NonIsoCalendars.IsNonIsoCalendar(calendar))
+ {
+ return NonIsoCalendars.IsoToCalendarDate(calendar, isoDate).DaysInYear;
+ }
+
+ return isoDate.DaysInYear();
+ }
+
+ ///
+ /// Returns the number of months in the calendar year containing the given ISO date.
+ ///
+ internal static int CalendarMonthsInYear(string calendar, in IsoDate isoDate)
+ {
+ if (IsGregorianBasedCalendar(calendar))
+ {
+ return 12;
+ }
+
+ if (NonIsoCalendars.IsNonIsoCalendar(calendar))
+ {
+ return NonIsoCalendars.IsoToCalendarDate(calendar, isoDate).MonthsInYear;
+ }
+
+ return 12;
+ }
+
+ ///
+ /// Returns whether the calendar year containing the given ISO date is a leap year.
+ ///
+ internal static bool CalendarInLeapYear(string calendar, in IsoDate isoDate)
+ {
+ if (IsGregorianBasedCalendar(calendar))
+ {
+ return IsoDate.IsLeapYear(isoDate.Year);
+ }
+
+ if (NonIsoCalendars.IsNonIsoCalendar(calendar))
+ {
+ return NonIsoCalendars.IsoToCalendarDate(calendar, isoDate).InLeapYear;
+ }
+
+ return IsoDate.IsLeapYear(isoDate.Year);
+ }
+
+ ///
+ /// Returns the era string for any calendar, including non-ISO calendars.
+ ///
+ internal static string? CalendarEra(string calendar, in IsoDate isoDate)
+ {
+ if (IsGregorianBasedCalendar(calendar))
+ {
+ return CalendarEra(calendar, isoDate.Year);
+ }
+
+ if (NonIsoCalendars.IsNonIsoCalendar(calendar))
+ {
+ var calDate = NonIsoCalendars.IsoToCalendarDate(calendar, isoDate);
+ return NonIsoCalendars.CalendarEra(calendar, calDate);
+ }
+
+ return CalendarEra(calendar, isoDate.Year);
+ }
+
+ ///
+ /// Returns the eraYear for any calendar, including non-ISO calendars.
+ ///
+ internal static int? CalendarEraYear(string calendar, in IsoDate isoDate)
+ {
+ if (IsGregorianBasedCalendar(calendar))
+ {
+ return CalendarEraYear(calendar, isoDate.Year);
+ }
+
+ if (NonIsoCalendars.IsNonIsoCalendar(calendar))
+ {
+ var calDate = NonIsoCalendars.IsoToCalendarDate(calendar, isoDate);
+ return NonIsoCalendars.CalendarEraYear(calendar, calDate);
+ }
+
+ return CalendarEraYear(calendar, isoDate.Year);
+ }
+
///
/// Reads era and eraYear properties from a property bag for era-supporting calendars.
/// Returns the computed year if era/eraYear are present, or null if they should be ignored.
@@ -1933,8 +2126,13 @@ private static int ComputeYearFromEra(Realm realm, string calendar, string era,
return 1 - eraYear;
break;
case "japanese":
- if (era is "reiwa" or "heisei" or "showa" or "taisho" or "meiji" or "ce" or "bce")
- return eraYear; // simplified
+ if (era is "reiwa") return eraYear + 2018;
+ if (era is "heisei") return eraYear + 1988;
+ if (era is "showa") return eraYear + 1925;
+ if (era is "taisho") return eraYear + 1911;
+ if (era is "meiji") return eraYear + 1872;
+ if (era is "ce" or "gregory") return eraYear;
+ if (era is "bce" or "gregory-inverse") return 1 - eraYear;
break;
case "roc":
if (era is "roc" or "minguo")
@@ -2263,6 +2461,31 @@ public static IsoDate CalendarDateAdd(Realm? realm, string calendar, IsoDate iso
// Range validation happens at the caller (e.g., CreateTemporalDate, GetEpochNanosecondsFor).
return result;
}
+ else if (NonIsoCalendars.IsNonIsoCalendar(calendar))
+ {
+ try
+ {
+ // Add years and months using calendar-specific reckoning
+ var result = NonIsoCalendars.CalendarDateAdd(calendar, isoDate, (int) duration.Years, (int) duration.Months, overflow);
+
+ // Add weeks and days using ISO arithmetic
+ var days = duration.Days + 7 * duration.Weeks;
+ if (days != 0)
+ {
+ result = AddDaysToISODate(result, days);
+ }
+
+ return result;
+ }
+ catch (InvalidOperationException ex) when (string.Equals(ex.Message, "reject", StringComparison.Ordinal))
+ {
+ if (realm != null)
+ Throw.RangeError(realm, "Invalid date after adding duration");
+ else
+ throw new ArgumentException("Invalid date after adding duration", nameof(duration));
+ return default;
+ }
+ }
else
{
throw new NotSupportedException($"Calendar '{calendar}' not yet supported");
@@ -4143,16 +4366,19 @@ private static JsPlainDate CreatePlainDateFromFields(Realm realm, RelativeToFiel
// Determine month from either month or monthCode
int month;
+ string? monthCode = null;
+ var isNonIso = NonIsoCalendars.IsNonIsoCalendar(fields.Calendar);
if (fields.Month.HasValue && !string.IsNullOrEmpty(fields.MonthCode))
{
- // Both provided - parse monthCode and verify they match
+ // Both provided - verify they match (ISO only - non-ISO ordinal ≠ display)
var parsedMonthCode = ParseMonthCode(realm, fields.MonthCode!);
- if (parsedMonthCode != fields.Month.Value)
+ if (!isNonIso && parsedMonthCode != fields.Month.Value)
{
Throw.RangeError(realm, "month and monthCode do not match");
}
month = fields.Month.Value;
+ monthCode = fields.MonthCode;
}
else if (fields.Month.HasValue)
{
@@ -4160,7 +4386,8 @@ private static JsPlainDate CreatePlainDateFromFields(Realm realm, RelativeToFiel
}
else if (!string.IsNullOrEmpty(fields.MonthCode))
{
- month = ParseMonthCode(realm, fields.MonthCode!);
+ month = isNonIso ? 0 : ParseMonthCode(realm, fields.MonthCode!);
+ monthCode = fields.MonthCode;
}
else
{
@@ -4168,8 +4395,17 @@ private static JsPlainDate CreatePlainDateFromFields(Realm realm, RelativeToFiel
return null!;
}
- // Regulate the date
- var isoDate = RegulateIsoDate(fields.Year.Value, month, fields.Day.Value, "constrain");
+ // Use calendar-aware date construction for non-ISO calendars
+ IsoDate? isoDate;
+ if (isNonIso)
+ {
+ isoDate = CalendarDateToISO(realm, fields.Calendar, fields.Year.Value, month, fields.Day.Value, "constrain", monthCode);
+ }
+ else
+ {
+ isoDate = RegulateIsoDate(fields.Year.Value, month, fields.Day.Value, "constrain");
+ }
+
if (isoDate is null)
{
Throw.RangeError(realm, "Invalid date");
@@ -4202,15 +4438,18 @@ private static JsZonedDateTime CreateZonedDateTimeFromFields(Engine engine, Real
// Determine month
int month;
+ string? monthCode = null;
+ var isNonIso = NonIsoCalendars.IsNonIsoCalendar(fields.Calendar);
if (fields.Month.HasValue && !string.IsNullOrEmpty(fields.MonthCode))
{
var parsedMonthCode = ParseMonthCode(realm, fields.MonthCode!);
- if (parsedMonthCode != fields.Month.Value)
+ if (!isNonIso && parsedMonthCode != fields.Month.Value)
{
Throw.RangeError(realm, "month and monthCode do not match");
}
month = fields.Month.Value;
+ monthCode = fields.MonthCode;
}
else if (fields.Month.HasValue)
{
@@ -4218,7 +4457,8 @@ private static JsZonedDateTime CreateZonedDateTimeFromFields(Engine engine, Real
}
else if (!string.IsNullOrEmpty(fields.MonthCode))
{
- month = ParseMonthCode(realm, fields.MonthCode!);
+ month = isNonIso ? 0 : ParseMonthCode(realm, fields.MonthCode!);
+ monthCode = fields.MonthCode;
}
else
{
@@ -4227,7 +4467,16 @@ private static JsZonedDateTime CreateZonedDateTimeFromFields(Engine engine, Real
}
// Regulate date and time
- var isoDate = RegulateIsoDate(fields.Year.Value, month, fields.Day.Value, "constrain");
+ IsoDate? isoDate;
+ if (isNonIso)
+ {
+ isoDate = CalendarDateToISO(realm, fields.Calendar, fields.Year.Value, month, fields.Day.Value, "constrain", monthCode);
+ }
+ else
+ {
+ isoDate = RegulateIsoDate(fields.Year.Value, month, fields.Day.Value, "constrain");
+ }
+
if (isoDate is null)
{
Throw.RangeError(realm, "Invalid date");
@@ -6257,6 +6506,11 @@ internal static DurationRecord CalendarDateUntil(string calendar, IsoDate one, I
// Step 3: For ISO and Gregorian-based calendars (same arithmetic)
if (!IsGregorianBasedCalendar(calendar))
{
+ if (NonIsoCalendars.IsNonIsoCalendar(calendar))
+ {
+ return NonIsoCalendars.CalendarDateUntil(calendar, one, two, largestUnit);
+ }
+
throw new NotSupportedException($"Calendar '{calendar}' not yet supported");
}
diff --git a/Jint/Native/Temporal/ZonedDateTime/ZonedDateTimeConstructor.cs b/Jint/Native/Temporal/ZonedDateTime/ZonedDateTimeConstructor.cs
index 167fdd3afe..7c43987962 100644
--- a/Jint/Native/Temporal/ZonedDateTime/ZonedDateTimeConstructor.cs
+++ b/Jint/Native/Temporal/ZonedDateTime/ZonedDateTimeConstructor.cs
@@ -266,13 +266,16 @@ private JsZonedDateTime ToZonedDateTimeFromFieldsWithOptions(ObjectInstance obj,
// Validate well-formedness (format) - this happens before year type validation
monthFromCode = TemporalHelpers.ParseMonthCode(_realm, monthCodeStr);
- // If both month and monthCode are provided, they must match
- if (month != 0 && month != monthFromCode)
+ // If both month and monthCode are provided, they must match (ISO only)
+ if (!NonIsoCalendars.IsNonIsoCalendar(calendar) && month != 0 && month != monthFromCode)
{
Throw.RangeError(_realm, "month and monthCode do not match");
}
- month = monthFromCode;
+ if (!NonIsoCalendars.IsNonIsoCalendar(calendar))
+ {
+ month = monthFromCode;
+ }
}
// 9. nanosecond - read and convert immediately
@@ -344,13 +347,13 @@ private JsZonedDateTime ToZonedDateTimeFromFieldsWithOptions(ObjectInstance obj,
}
}
- if (month == 0)
+ if (month == 0 && monthCodeStr is null)
{
Throw.TypeError(_realm, "month or monthCode is required");
}
// Regulate date (with calendar conversion for non-ISO calendars)
- var date = TemporalHelpers.CalendarDateToISO(_realm, calendar, year, month, day, overflow);
+ var date = TemporalHelpers.CalendarDateToISO(_realm, calendar, year, month, day, overflow, monthCodeStr);
if (date is null)
{
Throw.RangeError(_realm, "Invalid date");
diff --git a/Jint/Native/Temporal/ZonedDateTime/ZonedDateTimePrototype.cs b/Jint/Native/Temporal/ZonedDateTime/ZonedDateTimePrototype.cs
index 25872fc2a7..9ec7725c8e 100644
--- a/Jint/Native/Temporal/ZonedDateTime/ZonedDateTimePrototype.cs
+++ b/Jint/Native/Temporal/ZonedDateTime/ZonedDateTimePrototype.cs
@@ -113,7 +113,7 @@ private JsValue GetEra(JsValue thisObject, JsCallArguments arguments)
{
var zdt = ValidateZonedDateTime(thisObject);
var isoDateTime = zdt.GetIsoDateTime();
- var era = TemporalHelpers.CalendarEra(zdt.Calendar, isoDateTime.Year);
+ var era = TemporalHelpers.CalendarEra(zdt.Calendar, isoDateTime.Date);
return era is not null ? new JsString(era) : Undefined;
}
@@ -121,7 +121,7 @@ private JsValue GetEraYear(JsValue thisObject, JsCallArguments arguments)
{
var zdt = ValidateZonedDateTime(thisObject);
var isoDateTime = zdt.GetIsoDateTime();
- var eraYear = TemporalHelpers.CalendarEraYear(zdt.Calendar, isoDateTime.Year);
+ var eraYear = TemporalHelpers.CalendarEraYear(zdt.Calendar, isoDateTime.Date);
return eraYear.HasValue ? JsNumber.Create(eraYear.Value) : Undefined;
}
@@ -129,17 +129,26 @@ private JsNumber GetYear(JsValue thisObject, JsCallArguments arguments)
{
var zdt = ValidateZonedDateTime(thisObject);
var isoDateTime = zdt.GetIsoDateTime();
- return JsNumber.Create(TemporalHelpers.CalendarYear(zdt.Calendar, isoDateTime.Year));
+ return JsNumber.Create(TemporalHelpers.CalendarYear(zdt.Calendar, isoDateTime.Date));
}
- private JsNumber GetMonth(JsValue thisObject, JsCallArguments arguments) =>
- JsNumber.Create(ValidateZonedDateTime(thisObject).GetIsoDateTime().Month);
+ private JsNumber GetMonth(JsValue thisObject, JsCallArguments arguments)
+ {
+ var zdt = ValidateZonedDateTime(thisObject);
+ return JsNumber.Create(TemporalHelpers.CalendarMonth(zdt.Calendar, zdt.GetIsoDateTime().Date));
+ }
- private JsString GetMonthCode(JsValue thisObject, JsCallArguments arguments) =>
- new JsString($"M{ValidateZonedDateTime(thisObject).GetIsoDateTime().Month:D2}");
+ private JsString GetMonthCode(JsValue thisObject, JsCallArguments arguments)
+ {
+ var zdt = ValidateZonedDateTime(thisObject);
+ return new JsString(TemporalHelpers.CalendarMonthCode(zdt.Calendar, zdt.GetIsoDateTime().Date));
+ }
- private JsNumber GetDay(JsValue thisObject, JsCallArguments arguments) =>
- JsNumber.Create(ValidateZonedDateTime(thisObject).GetIsoDateTime().Day);
+ private JsNumber GetDay(JsValue thisObject, JsCallArguments arguments)
+ {
+ var zdt = ValidateZonedDateTime(thisObject);
+ return JsNumber.Create(TemporalHelpers.CalendarDay(zdt.Calendar, zdt.GetIsoDateTime().Date));
+ }
private JsNumber GetHour(JsValue thisObject, JsCallArguments arguments) =>
JsNumber.Create(ValidateZonedDateTime(thisObject).GetIsoDateTime().Hour);
@@ -221,30 +230,25 @@ private JsNumber GetDaysInWeek(JsValue thisObject, JsCallArguments arguments)
private JsNumber GetDaysInMonth(JsValue thisObject, JsCallArguments arguments)
{
var zdt = ValidateZonedDateTime(thisObject);
- var date = zdt.GetIsoDateTime().Date;
- return JsNumber.Create(IsoDate.IsoDateInMonth(date.Year, date.Month));
+ return JsNumber.Create(TemporalHelpers.CalendarDaysInMonth(zdt.Calendar, zdt.GetIsoDateTime().Date));
}
private JsNumber GetDaysInYear(JsValue thisObject, JsCallArguments arguments)
{
var zdt = ValidateZonedDateTime(thisObject);
- var year = zdt.GetIsoDateTime().Year;
- var isLeap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
- return JsNumber.Create(isLeap ? 366 : 365);
+ return JsNumber.Create(TemporalHelpers.CalendarDaysInYear(zdt.Calendar, zdt.GetIsoDateTime().Date));
}
private JsNumber GetMonthsInYear(JsValue thisObject, JsCallArguments arguments)
{
- ValidateZonedDateTime(thisObject);
- return JsNumber.Create(12); // ISO 8601 always has 12 months
+ var zdt = ValidateZonedDateTime(thisObject);
+ return JsNumber.Create(TemporalHelpers.CalendarMonthsInYear(zdt.Calendar, zdt.GetIsoDateTime().Date));
}
private JsBoolean GetInLeapYear(JsValue thisObject, JsCallArguments arguments)
{
var zdt = ValidateZonedDateTime(thisObject);
- var year = zdt.GetIsoDateTime().Year;
- var isLeap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
- return isLeap ? JsBoolean.True : JsBoolean.False;
+ return TemporalHelpers.CalendarInLeapYear(zdt.Calendar, zdt.GetIsoDateTime().Date) ? JsBoolean.True : JsBoolean.False;
}
private JsNumber GetHoursInDay(JsValue thisObject, JsCallArguments arguments)
@@ -323,8 +327,20 @@ private JsZonedDateTime With(JsValue thisObject, JsCallArguments arguments)
// Each field is read and immediately converted, defaulting to current value if undefined
// Track if at least one property is present
+ var isNonIso = NonIsoCalendars.IsNonIsoCalendar(zdt.Calendar);
+
var dayValue = obj.Get("day");
- var day = dayValue.IsUndefined() ? current.Day : GetIntegerFromValue(dayValue);
+ int day;
+ if (!dayValue.IsUndefined())
+ {
+ day = GetIntegerFromValue(dayValue);
+ }
+ else
+ {
+ day = isNonIso
+ ? TemporalHelpers.CalendarDay(zdt.Calendar, current.Date)
+ : current.Day;
+ }
var hourValue = obj.Get("hour");
var hour = hourValue.IsUndefined() ? current.Hour : GetIntegerFromValue(hourValue);
@@ -339,7 +355,19 @@ private JsZonedDateTime With(JsValue thisObject, JsCallArguments arguments)
var minute = minuteValue.IsUndefined() ? current.Minute : GetIntegerFromValue(minuteValue);
var monthValue = obj.Get("month");
- var month = monthValue.IsUndefined() ? current.Month : GetIntegerFromValue(monthValue);
+ int month;
+ var monthExplicit = false;
+ if (!monthValue.IsUndefined())
+ {
+ monthExplicit = true;
+ month = GetIntegerFromValue(monthValue);
+ }
+ else
+ {
+ month = isNonIso
+ ? TemporalHelpers.CalendarMonth(zdt.Calendar, current.Date)
+ : current.Month;
+ }
// monthCode - read in alphabetical order, convert to string but defer validation
var monthCodeValue = obj.Get("monthCode");
@@ -373,6 +401,10 @@ private JsZonedDateTime With(JsValue thisObject, JsCallArguments arguments)
monthCode = TypeConverter.ToString(monthCodeValue);
}
}
+ else if (isNonIso && !monthExplicit)
+ {
+ monthCode = TemporalHelpers.CalendarMonthCode(zdt.Calendar, current.Date);
+ }
var nanosecondValue = obj.Get("nanosecond");
var nanosecond = nanosecondValue.IsUndefined() ? current.Nanosecond : GetIntegerFromValue(nanosecondValue);
@@ -389,7 +421,17 @@ private JsZonedDateTime With(JsValue thisObject, JsCallArguments arguments)
var second = secondValue.IsUndefined() ? current.Second : GetIntegerFromValue(secondValue);
var yearValue = obj.Get("year");
- var year = yearValue.IsUndefined() ? current.Year : GetIntegerFromValue(yearValue);
+ int year;
+ if (!yearValue.IsUndefined())
+ {
+ year = GetIntegerFromValue(yearValue);
+ }
+ else
+ {
+ year = isNonIso
+ ? TemporalHelpers.CalendarYear(zdt.Calendar, current.Date)
+ : current.Year;
+ }
// Check that at least one property is present
if (dayValue.IsUndefined() && hourValue.IsUndefined() && microsecondValue.IsUndefined() &&
@@ -440,15 +482,18 @@ private JsZonedDateTime With(JsValue thisObject, JsCallArguments arguments)
// Parse monthCode for well-formedness
parsedMonthCode = TemporalHelpers.ParseMonthCode(_realm, monthCode);
- // For ISO 8601 calendar: validate monthCode is valid (01-12, no leap months)
- if (monthCode.Length == 4 && monthCode[3] == 'L')
+ if (!isNonIso)
{
- Throw.RangeError(_realm, $"Leap months are not valid for ISO 8601 calendar: {monthCode}");
- }
+ // For ISO 8601 calendar: validate monthCode is valid (01-12, no leap months)
+ if (monthCode.Length == 4 && monthCode[3] == 'L')
+ {
+ Throw.RangeError(_realm, $"Leap months are not valid for ISO 8601 calendar: {monthCode}");
+ }
- if (parsedMonthCode.Value < 1 || parsedMonthCode.Value > 12)
- {
- Throw.RangeError(_realm, $"Month {parsedMonthCode.Value} is not valid for ISO 8601 calendar");
+ if (parsedMonthCode.Value < 1 || parsedMonthCode.Value > 12)
+ {
+ Throw.RangeError(_realm, $"Month {parsedMonthCode.Value} is not valid for ISO 8601 calendar");
+ }
}
// Check if month property was explicitly provided (not just defaulted)
@@ -462,12 +507,24 @@ private JsZonedDateTime With(JsValue thisObject, JsCallArguments arguments)
}
}
- // Use monthCode if provided
- month = parsedMonthCode.Value;
+ // Use monthCode if provided (for ISO calendars)
+ if (!isNonIso)
+ {
+ month = parsedMonthCode.Value;
+ }
+ }
+
+ // Regulate date with user's overflow option
+ IsoDate? date;
+ if (isNonIso)
+ {
+ date = TemporalHelpers.CalendarDateToISO(_realm, zdt.Calendar, year, month, day, overflow, monthCode);
+ }
+ else
+ {
+ date = TemporalHelpers.RegulateIsoDate(year, month, day, overflow);
}
- // Regulate date and time with user's overflow option
- var date = TemporalHelpers.RegulateIsoDate(year, month, day, overflow);
if (date is null)
{
Throw.RangeError(_realm, "Invalid date");