commit 584dd6fe83c9eb57869275809bf40851555e26e0
parent c239c1f224a35434175a2d7fe78c7e2fa7f7480a
Author: André Bargull <andre.bargull@gmail.com>
Date: Thu, 27 Nov 2025 10:03:38 +0000
Bug 1999316 - Part 5: Support eras for more calendars. r=spidermonkey-reviewers,mgaudet
Add eras specified in the proposal, except for the ones which will be removed
in <https://github.com/tc39/proposal-intl-era-monthcode/pull/72>.
Differential Revision: https://phabricator.services.mozilla.com/D272035
Diffstat:
3 files changed, 147 insertions(+), 39 deletions(-)
diff --git a/js/src/builtin/temporal/Calendar.cpp b/js/src/builtin/temporal/Calendar.cpp
@@ -754,9 +754,12 @@ static constexpr size_t EraNameMaxLength() {
return length;
}
-static mozilla::Maybe<EraCode> EraForString(CalendarId calendar,
- JSLinearString* string) {
- MOZ_ASSERT(CalendarEraRelevant(calendar));
+/**
+ * CanonicalizeEraInCalendar ( calendar, era )
+ */
+static mozilla::Maybe<EraCode> CanonicalizeEraInCalendar(
+ CalendarId calendar, JSLinearString* string) {
+ MOZ_ASSERT(CalendarSupportsEra(calendar));
// Note: Assigning MaxLength to EraNameMaxLength() breaks the CDT indexer.
constexpr size_t MaxLength = 24;
@@ -921,7 +924,7 @@ static mozilla::Result<UniqueICU4XDate, CalendarError> CreateDateFromCodes(
MOZ_ASSERT(icu4x::capi::icu4x_Calendar_kind_mv1(calendar) ==
ToAnyCalendarKind(calendarId));
MOZ_ASSERT(CalendarErasAsEnumSet(calendarId).contains(eraYear.era));
- MOZ_ASSERT_IF(CalendarEraRelevant(calendarId), eraYear.year > 0);
+ MOZ_ASSERT_IF(CalendarEraHasInverse(calendarId), eraYear.year > 0);
MOZ_ASSERT(mozilla::Abs(eraYear.year) <= MaximumCalendarYear(calendarId));
MOZ_ASSERT(IsValidMonthCodeForCalendar(calendarId, monthCode));
MOZ_ASSERT(day > 0);
@@ -1667,10 +1670,10 @@ struct EraYears {
static bool CalendarEraYear(JSContext* cx, CalendarId calendarId,
EraYear eraYear, EraYear* result) {
- MOZ_ASSERT(CalendarEraRelevant(calendarId));
+ MOZ_ASSERT(CalendarSupportsEra(calendarId));
MOZ_ASSERT(mozilla::Abs(eraYear.year) <= MaximumCalendarYear(calendarId));
- if (eraYear.year > 0) {
+ if (eraYear.year > 0 || !CalendarEraHasInverse(calendarId)) {
*result = eraYear;
return true;
}
@@ -1726,9 +1729,9 @@ static bool CalendarFieldYear(JSContext* cx, CalendarId calendar,
// |eraYear| is to be ignored when not relevant for |calendar| per
// CalendarResolveFields.
- bool hasRelevantEra =
- fields.has(CalendarField::Era) && CalendarEraRelevant(calendar);
- MOZ_ASSERT_IF(fields.has(CalendarField::Era), CalendarEraRelevant(calendar));
+ bool supportsEra =
+ fields.has(CalendarField::Era) && CalendarSupportsEra(calendar);
+ MOZ_ASSERT_IF(fields.has(CalendarField::Era), CalendarSupportsEra(calendar));
// Case 1: |year| field is present.
mozilla::Maybe<EraYear> fromEpoch;
@@ -1745,12 +1748,12 @@ static bool CalendarFieldYear(JSContext* cx, CalendarId calendar,
fromEpoch = mozilla::Some(CalendarEraYear(calendar, intYear));
} else {
- MOZ_ASSERT(hasRelevantEra);
+ MOZ_ASSERT(supportsEra);
}
// Case 2: |era| and |eraYear| fields are present and relevant for |calendar|.
mozilla::Maybe<EraYear> fromEra;
- if (hasRelevantEra) {
+ if (supportsEra) {
MOZ_ASSERT(fields.has(CalendarField::Era));
MOZ_ASSERT(fields.has(CalendarField::EraYear));
@@ -1766,7 +1769,7 @@ static bool CalendarFieldYear(JSContext* cx, CalendarId calendar,
}
// Ensure the requested era is valid for |calendar|.
- auto eraCode = EraForString(calendar, linearEra);
+ auto eraCode = CanonicalizeEraInCalendar(calendar, linearEra);
if (!eraCode) {
if (auto code = QuoteString(cx, era)) {
JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr,
@@ -1803,9 +1806,8 @@ struct Month {
};
/**
- * CalendarResolveFields ( calendar, fields, type )
- * CalendarDateToISO ( calendar, fields, overflow )
- * CalendarMonthDayToISOReferenceDate ( calendar, fields, overflow )
+ * NonISOCalendarDateToISO ( calendar, fields, overflow )
+ * NonISOMonthDayToISOReferenceDate ( calendar, fields, overflow )
*
* Extract `month` and `monthCode` from |fields| and perform some initial
* validation to ensure the values are valid for the requested calendar.
@@ -2441,7 +2443,7 @@ static bool NonISOResolveFields(JSContext* cx, CalendarId calendar,
missingField = "monthCode";
} else if (requireDay && !fields.has(CalendarField::Day)) {
missingField = "day";
- } else if (!CalendarEraRelevant(calendar)) {
+ } else if (!CalendarSupportsEra(calendar)) {
if (requireYear && !fields.has(CalendarField::Year)) {
missingField = "year";
}
@@ -2505,6 +2507,7 @@ static bool CalendarResolveFields(JSContext* cx, CalendarId calendar,
/**
* CalendarISOToDate ( calendar, isoDate )
* NonISOCalendarISOToDate ( calendar, isoDate )
+ * CalendarDateEra ( calendar, date )
*
* Return the Calendar Date Record's [[Era]] field.
*/
@@ -2520,20 +2523,28 @@ bool js::temporal::CalendarEra(JSContext* cx, Handle<CalendarValue> calendar,
}
// Step 2.
- if (!CalendarEraRelevant(calendarId)) {
+ if (!CalendarSupportsEra(calendarId)) {
result.setUndefined();
return true;
}
- auto cal = CreateICU4XCalendar(calendarId);
- auto dt = CreateICU4XDate(cx, date, calendarId, cal.get());
- if (!dt) {
- return false;
- }
+ auto era = EraCode::Standard;
- EraCode era;
- if (!CalendarDateEra(cx, calendarId, dt.get(), &era)) {
- return false;
+ // Call into ICU4X if the calendar has more than one era.
+ auto eras = CalendarEras(calendarId);
+ if (eras.size() > 1) {
+ auto cal = CreateICU4XCalendar(calendarId);
+ auto dt = CreateICU4XDate(cx, date, calendarId, cal.get());
+ if (!dt) {
+ return false;
+ }
+
+ if (!CalendarDateEra(cx, calendarId, dt.get(), &era)) {
+ return false;
+ }
+ } else {
+ MOZ_ASSERT(*eras.begin() == EraCode::Standard,
+ "single era calendars use only the standard era");
}
auto* str = NewStringCopy<CanGC>(cx, CalendarEraName(calendarId, era));
@@ -2548,6 +2559,7 @@ bool js::temporal::CalendarEra(JSContext* cx, Handle<CalendarValue> calendar,
/**
* CalendarISOToDate ( calendar, isoDate )
* NonISOCalendarISOToDate ( calendar, isoDate )
+ * CalendarDateEraYear ( calendar, date )
*
* Return the Calendar Date Record's [[EraYear]] field.
*/
@@ -2564,11 +2576,18 @@ bool js::temporal::CalendarEraYear(JSContext* cx,
}
// Step 2.
- if (!CalendarEraRelevant(calendarId)) {
+ if (!CalendarSupportsEra(calendarId)) {
result.setUndefined();
return true;
}
+ auto eras = CalendarEras(calendarId);
+ if (eras.size() == 1) {
+ // Return the calendar year for calendars with a single era.
+ return CalendarYear(cx, calendar, date, result);
+ }
+ MOZ_ASSERT(eras.size() > 1);
+
auto cal = CreateICU4XCalendar(calendarId);
auto dt = CreateICU4XDate(cx, date, calendarId, cal.get());
if (!dt) {
@@ -2583,6 +2602,7 @@ bool js::temporal::CalendarEraYear(JSContext* cx,
/**
* CalendarISOToDate ( calendar, isoDate )
* NonISOCalendarISOToDate ( calendar, isoDate )
+ * CalendarDateArithmeticYear ( calendar, date )
*
* Return the Calendar Date Record's [[Year]] field.
*/
diff --git a/js/src/builtin/temporal/CalendarFields.cpp b/js/src/builtin/temporal/CalendarFields.cpp
@@ -199,9 +199,9 @@ static mozilla::EnumSet<CalendarField> CalendarExtraFields(
// Step 2.
- // "era" and "eraYear" are relevant for calendars with multiple eras when
+ // "era" and "eraYear" are relevant for calendars supporting eras when
// "year" is present.
- if (fields.contains(CalendarField::Year) && CalendarEraRelevant(calendar)) {
+ if (fields.contains(CalendarField::Year) && CalendarSupportsEra(calendar)) {
return {CalendarField::Era, CalendarField::EraYear};
}
return {};
@@ -571,9 +571,9 @@ static auto NonISOFieldKeysToIgnore(CalendarId calendar,
result += monthOrMonthCode;
}
- // "era", "eraYear", and "year" are mutually exclusive in non-single era
- // calendar systems.
- if (CalendarEraRelevant(calendar) && !(keys & eraOrAnyYear).isEmpty()) {
+ // "era", "eraYear", and "year" are mutually exclusive when the calendar
+ // supports eras.
+ if (CalendarSupportsEra(calendar) && !(keys & eraOrAnyYear).isEmpty()) {
result += eraOrAnyYear;
}
diff --git a/js/src/builtin/temporal/Era.h b/js/src/builtin/temporal/Era.h
@@ -61,6 +61,28 @@ inline constexpr auto Empty = {
""sv,
};
+inline constexpr auto Buddhist = {
+ "be"sv,
+};
+
+inline constexpr auto Coptic = {
+ "am"sv,
+};
+
+inline constexpr auto EthiopianAmeteAlem = {
+ "aa"sv,
+};
+
+// "Intl era and monthCode" proposal follows CDLR which defines that Amete Alem
+// era is used for years before the incarnation. This may not match modern
+// usage, though. For the time being use a single era to check if we get any
+// user reports to clarify the situation.
+//
+// CLDR bug report: https://unicode-org.atlassian.net/browse/CLDR-18739
+inline constexpr auto Ethiopian = {
+ "am"sv,
+};
+
inline constexpr auto Gregorian = {
"ce"sv,
"ad"sv,
@@ -71,6 +93,14 @@ inline constexpr auto GregorianInverse = {
"bc"sv,
};
+inline constexpr auto Hebrew = {
+ "am"sv,
+};
+
+inline constexpr auto Indian = {
+ "shaka"sv,
+};
+
inline constexpr auto Islamic = {
"ah"sv,
};
@@ -99,6 +129,10 @@ inline constexpr auto JapaneseReiwa = {
"reiwa"sv,
};
+inline constexpr auto Persian = {
+ "ap"sv,
+};
+
inline constexpr auto ROC = {
"roc"sv,
"minguo"sv,
@@ -112,8 +146,8 @@ inline constexpr auto ROCInverse = {
} // namespace names
} // namespace eras
-constexpr auto& CalendarEras(CalendarId id) {
- switch (id) {
+constexpr auto& CalendarEras(CalendarId calendar) {
+ switch (calendar) {
case CalendarId::ISO8601:
case CalendarId::Buddhist:
case CalendarId::Chinese:
@@ -139,24 +173,78 @@ constexpr auto& CalendarEras(CalendarId id) {
MOZ_CRASH("invalid calendar id");
}
-constexpr bool CalendarEraRelevant(CalendarId calendar) {
+/**
+ * Return `true` iff the calendar has an inverse era.
+ */
+constexpr bool CalendarEraHasInverse(CalendarId calendar) {
+ // More than one era implies an inverse era is used.
return CalendarEras(calendar).size() > 1;
}
-constexpr auto& CalendarEraNames(CalendarId calendar, EraCode era) {
+/**
+ * CalendarSupportsEra ( calendar )
+ */
+constexpr bool CalendarSupportsEra(CalendarId calendar) {
switch (calendar) {
case CalendarId::ISO8601:
- case CalendarId::Buddhist:
case CalendarId::Chinese:
- case CalendarId::Coptic:
case CalendarId::Dangi:
+ return false;
+
+ case CalendarId::Buddhist:
+ case CalendarId::Coptic:
case CalendarId::Ethiopian:
case CalendarId::EthiopianAmeteAlem:
case CalendarId::Hebrew:
case CalendarId::Indian:
case CalendarId::Persian:
+ case CalendarId::Gregorian:
+ case CalendarId::IslamicCivil:
+ case CalendarId::IslamicTabular:
+ case CalendarId::IslamicUmmAlQura:
+ case CalendarId::ROC:
+ case CalendarId::Japanese:
+ return true;
+ }
+ MOZ_CRASH("invalid calendar id");
+}
+
+constexpr auto& CalendarEraNames(CalendarId calendar, EraCode era) {
+ switch (calendar) {
+ case CalendarId::ISO8601:
+ case CalendarId::Chinese:
+ case CalendarId::Dangi:
+ MOZ_ASSERT(era == EraCode::Standard);
return eras::names::Empty;
+ case CalendarId::Buddhist:
+ MOZ_ASSERT(era == EraCode::Standard);
+ return eras::names::Buddhist;
+
+ case CalendarId::Coptic:
+ MOZ_ASSERT(era == EraCode::Standard);
+ return eras::names::Coptic;
+
+ case CalendarId::Ethiopian:
+ MOZ_ASSERT(era == EraCode::Standard);
+ return eras::names::Ethiopian;
+
+ case CalendarId::EthiopianAmeteAlem:
+ MOZ_ASSERT(era == EraCode::Standard);
+ return eras::names::EthiopianAmeteAlem;
+
+ case CalendarId::Hebrew:
+ MOZ_ASSERT(era == EraCode::Standard);
+ return eras::names::Hebrew;
+
+ case CalendarId::Indian:
+ MOZ_ASSERT(era == EraCode::Standard);
+ return eras::names::Indian;
+
+ case CalendarId::Persian:
+ MOZ_ASSERT(era == EraCode::Standard);
+ return eras::names::Persian;
+
case CalendarId::Gregorian: {
MOZ_ASSERT(era == EraCode::Standard || era == EraCode::Inverse);
return era == EraCode::Standard ? eras::names::Gregorian
@@ -237,8 +325,8 @@ struct EraYear {
int32_t year = 0;
};
-constexpr EraYear CalendarEraYear(CalendarId id, int32_t year) {
- if (year > 0 || !CalendarEraRelevant(id)) {
+constexpr EraYear CalendarEraYear(CalendarId calendar, int32_t year) {
+ if (year > 0 || !CalendarEraHasInverse(calendar)) {
return EraYear{EraCode::Standard, year};
}
return EraYear{EraCode::Inverse, int32_t(mozilla::Abs(year) + 1)};