/* * Copyright (c) 2021, Idan Horowitz * Copyright (c) 2021-2023, Linus Groh * Copyright (c) 2023-2024, Shannon Booth * Copyright (c) 2024-2026, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace JS::Temporal { String ISO8601_CALENDAR = "iso8601"_string; enum class CalendarFieldConversion { ToIntegerWithTruncation, ToMonthCode, ToOffsetString, ToPositiveIntegerWithTruncation, ToString, ToTemporalTimeZoneIdentifier, }; // https://tc39.es/proposal-temporal/#table-temporal-calendar-fields-record-fields #define JS_ENUMERATE_CALENDAR_FIELDS \ __JS_ENUMERATE(CalendarField::Era, era, vm.names.era, CalendarFieldConversion::ToString) \ __JS_ENUMERATE(CalendarField::EraYear, era_year, vm.names.eraYear, CalendarFieldConversion::ToIntegerWithTruncation) \ __JS_ENUMERATE(CalendarField::Year, year, vm.names.year, CalendarFieldConversion::ToIntegerWithTruncation) \ __JS_ENUMERATE(CalendarField::Month, month, vm.names.month, CalendarFieldConversion::ToPositiveIntegerWithTruncation) \ __JS_ENUMERATE(CalendarField::MonthCode, month_code, vm.names.monthCode, CalendarFieldConversion::ToMonthCode) \ __JS_ENUMERATE(CalendarField::Day, day, vm.names.day, CalendarFieldConversion::ToPositiveIntegerWithTruncation) \ __JS_ENUMERATE(CalendarField::Hour, hour, vm.names.hour, CalendarFieldConversion::ToIntegerWithTruncation) \ __JS_ENUMERATE(CalendarField::Minute, minute, vm.names.minute, CalendarFieldConversion::ToIntegerWithTruncation) \ __JS_ENUMERATE(CalendarField::Second, second, vm.names.second, CalendarFieldConversion::ToIntegerWithTruncation) \ __JS_ENUMERATE(CalendarField::Millisecond, millisecond, vm.names.millisecond, CalendarFieldConversion::ToIntegerWithTruncation) \ __JS_ENUMERATE(CalendarField::Microsecond, microsecond, vm.names.microsecond, CalendarFieldConversion::ToIntegerWithTruncation) \ __JS_ENUMERATE(CalendarField::Nanosecond, nanosecond, vm.names.nanosecond, CalendarFieldConversion::ToIntegerWithTruncation) \ __JS_ENUMERATE(CalendarField::Offset, offset_string, vm.names.offset, CalendarFieldConversion::ToOffsetString) \ __JS_ENUMERATE(CalendarField::TimeZone, time_zone, vm.names.timeZone, CalendarFieldConversion::ToTemporalTimeZoneIdentifier) struct CalendarFieldData { CalendarField key; NonnullRawPtr property; CalendarFieldConversion conversion; }; static Vector sorted_calendar_fields(VM& vm, CalendarFieldList fields) { auto data_for_field = [&](auto field) -> CalendarFieldData { switch (field) { #define __JS_ENUMERATE(enumeration, field_name, property_key, conversion) \ case enumeration: \ return { enumeration, property_key, conversion }; JS_ENUMERATE_CALENDAR_FIELDS #undef __JS_ENUMERATE } VERIFY_NOT_REACHED(); }; Vector result; result.ensure_capacity(fields.size()); for (auto field : fields) result.unchecked_append(data_for_field(field)); quick_sort(result, [](auto const& lhs, auto const& rhs) { return lhs.property->as_string() < rhs.property->as_string(); }); return result; } template static void set_field_value(CalendarField field, CalendarFields& fields, T&& value) { switch (field) { #define __JS_ENUMERATE(enumeration, field_name, property_key, conversion) \ case enumeration: \ if constexpr (IsAssignable>) \ fields.field_name = value; \ return; JS_ENUMERATE_CALENDAR_FIELDS #undef __JS_ENUMERATE } VERIFY_NOT_REACHED(); } static void set_default_field_value(CalendarField field, CalendarFields& fields) { CalendarFields default_ {}; switch (field) { #define __JS_ENUMERATE(enumeration, field_name, property_key, conversion) \ case enumeration: \ fields.field_name = default_.field_name; \ return; JS_ENUMERATE_CALENDAR_FIELDS #undef __JS_ENUMERATE } VERIFY_NOT_REACHED(); } // Table 1: Calendar types described in CLDR, https://tc39.es/proposal-intl-era-monthcode/#table-calendar-types static constexpr auto CLDR_CALENDAR_TYPES = to_array({ "buddhist"sv, "chinese"sv, "coptic"sv, "dangi"sv, "ethioaa"sv, "ethiopic"sv, "ethiopic-amete-alem"sv, "gregory"sv, "hebrew"sv, "indian"sv, "islamic-civil"sv, "islamic-tbla"sv, "islamic-umalqura"sv, "islamicc"sv, "iso8601"sv, "japanese"sv, "persian"sv, "roc"sv, }); // Table 2: Eras, https://tc39.es/proposal-intl-era-monthcode/#table-eras struct CalendarEraData { enum class Kind : u8 { Epoch, Offset, Negative, }; StringView calendar; StringView era; StringView alias; Optional minimum_era_year; Optional maximum_era_year; Kind kind; Optional offset; // NB: This column is not in the spec table, but is needed to handle calendars with mid-year era transitions. Optional iso_era_start; }; static constexpr auto CALENDAR_ERA_DATA = to_array({ // clang-format off { "buddhist"sv, "be"sv, {}, {}, {}, CalendarEraData::Kind::Epoch, {}, {} }, { "coptic"sv, "am"sv, {}, {}, {}, CalendarEraData::Kind::Epoch, {}, {} }, { "ethioaa"sv, "aa"sv, {}, {}, {}, CalendarEraData::Kind::Epoch, {}, {} }, { "ethiopic"sv, "am"sv, {}, 1, {}, CalendarEraData::Kind::Epoch, {}, {} }, { "ethiopic"sv, "aa"sv, {}, {}, 5500, CalendarEraData::Kind::Offset, -5499, {} }, { "gregory"sv, "ce"sv, "ad"sv, 1, {}, CalendarEraData::Kind::Epoch, {}, {} }, { "gregory"sv, "bce"sv, "bc"sv, 1, {}, CalendarEraData::Kind::Negative, {}, {} }, { "hebrew"sv, "am"sv, {}, {}, {}, CalendarEraData::Kind::Epoch, {}, {} }, { "indian"sv, "shaka"sv, {}, {}, {}, CalendarEraData::Kind::Epoch, {}, {} }, { "islamic-civil"sv, "ah"sv, {}, 1, {}, CalendarEraData::Kind::Epoch, {}, {} }, { "islamic-civil"sv, "bh"sv, {}, 1, {}, CalendarEraData::Kind::Negative, {}, {} }, { "islamic-tbla"sv, "ah"sv, {}, 1, {}, CalendarEraData::Kind::Epoch, {}, {} }, { "islamic-tbla"sv, "bh"sv, {}, 1, {}, CalendarEraData::Kind::Negative, {}, {} }, { "islamic-umalqura"sv, "ah"sv, {}, 1, {}, CalendarEraData::Kind::Epoch, {}, {} }, { "islamic-umalqura"sv, "bh"sv, {}, 1, {}, CalendarEraData::Kind::Negative, {}, {} }, { "japanese"sv, "reiwa"sv, {}, 1, {}, CalendarEraData::Kind::Offset, 2019, { { 2019, 5, 1 } } }, { "japanese"sv, "heisei"sv, {}, 1, 31, CalendarEraData::Kind::Offset, 1989, { { 1989, 1, 8 } } }, { "japanese"sv, "showa"sv, {}, 1, 64, CalendarEraData::Kind::Offset, 1926, { { 1926, 12, 25 } } }, { "japanese"sv, "taisho"sv, {}, 1, 15, CalendarEraData::Kind::Offset, 1912, { { 1912, 7, 30 } } }, { "japanese"sv, "meiji"sv, {}, 1, 45, CalendarEraData::Kind::Offset, 1868, { { 1873, 1, 1 } } }, { "japanese"sv, "ce"sv, "ad"sv, 1, 1872, CalendarEraData::Kind::Epoch, {}, {} }, { "japanese"sv, "bce"sv, "bc"sv, 1, {}, CalendarEraData::Kind::Negative, {}, {} }, { "persian"sv, "ap"sv, {}, {}, {}, CalendarEraData::Kind::Epoch, {}, {} }, { "roc"sv, "roc"sv, {}, 1, {}, CalendarEraData::Kind::Epoch, {}, {} }, { "roc"sv, "broc"sv, {}, 1, {}, CalendarEraData::Kind::Negative, {}, {} }, // clang-format on }); // Table 3: Additional Month Codes in Calendars, https://tc39.es/proposal-intl-era-monthcode/#table-additional-month-codes struct AdditionalMonthCodes { enum class Leap : u8 { SkipBackward, SkipForward, }; StringView calendar; ReadonlySpan additional_month_codes; Optional leap_to_common_month_transformation; }; static constexpr auto ALL_LEAP_MONTH_CODES = to_array({ "M01L"sv, "M02L"sv, "M03L"sv, "M04L"sv, "M05L"sv, "M06L"sv, "M07L"sv, "M08L"sv, "M09L"sv, "M10L"sv, "M11L"sv, "M12L"sv }); static constexpr auto THIRTEENTH_MONTH_CODES = to_array({ "M13"sv }); static constexpr auto HEBREW_ADAR_I_MONTH_CODES = to_array({ "M05L"sv }); static constexpr auto ADDITIONAL_MONTH_CODES = to_array({ { "chinese"sv, ALL_LEAP_MONTH_CODES, AdditionalMonthCodes::Leap::SkipBackward }, { "coptic"sv, THIRTEENTH_MONTH_CODES, {} }, { "dangi"sv, ALL_LEAP_MONTH_CODES, AdditionalMonthCodes::Leap::SkipBackward }, { "ethioaa"sv, THIRTEENTH_MONTH_CODES, {} }, { "ethiopic"sv, THIRTEENTH_MONTH_CODES, {} }, { "hebrew"sv, HEBREW_ADAR_I_MONTH_CODES, AdditionalMonthCodes::Leap::SkipForward }, }); // Table 6: "chinese" and "dangi" Calendars ISO Reference Years, https://tc39.es/proposal-intl-era-monthcode/#chinese-dangi-iso-reference-years struct ISOReferenceYears { StringView month_code; Optional days_1_to_29; Optional day_30; }; static constexpr auto CHINESE_AND_DANGI_ISO_REFERENCE_YEARS = to_array({ { "M01"sv, 1972, 1970 }, { "M01L"sv, {}, {} }, { "M02"sv, 1972, 1972 }, { "M02L"sv, 1947, {} }, { "M03"sv, 1972, 0 }, // Day=30 depends on the calendar and is handled below. { "M03L"sv, 1966, 1955 }, { "M04"sv, 1972, 1970 }, { "M04L"sv, 1963, 1944 }, { "M05"sv, 1972, 1972 }, { "M05L"sv, 1971, 1952 }, { "M06"sv, 1972, 1971 }, { "M06L"sv, 1960, 1941 }, { "M07"sv, 1972, 1972 }, { "M07L"sv, 1968, 1938 }, { "M08"sv, 1972, 1971 }, { "M08L"sv, 1957, {} }, { "M09"sv, 1972, 1972 }, { "M09L"sv, 2014, {} }, { "M10"sv, 1972, 1972 }, { "M10L"sv, 1984, {} }, { "M11"sv, 1972, 1970 }, { "M11L"sv, 0, {} }, // The reference year for days 1-10 and days 11-29 differ and is handled below. { "M12"sv, 1972, 1972 }, { "M12L"sv, {}, {} }, }); static Optional chinese_or_dangi_reference_year(String const& calendar, StringView month_code, u8 day) { auto row = find_value(CHINESE_AND_DANGI_ISO_REFERENCE_YEARS, [&](auto const& row) { return row.month_code == month_code; }); VERIFY(row.has_value()); if (day >= 1 && day < 30) { if (month_code == "M11L"sv) return day <= 10 ? 2033 : 2034; return row->days_1_to_29; } if (day == 30) { if (month_code == "M03"sv) return calendar == "chinese"sv ? 1966 : 1968; return row->day_30; } return {}; } // 12.1.1 CanonicalizeCalendar ( id ), https://tc39.es/proposal-temporal/#sec-temporal-canonicalizecalendar ThrowCompletionOr canonicalize_calendar(VM& vm, StringView id) { // 1. Let calendars be AvailableCalendars(). auto const& calendars = available_calendars(); // 2. If calendars does not contain the ASCII-lowercase of id, throw a RangeError exception. for (auto const& calendar : calendars) { if (calendar.equals_ignoring_ascii_case(id)) { // 3. Return CanonicalizeUValue("ca", id). return Unicode::canonicalize_unicode_extension_values("ca"sv, id); } } return vm.throw_completion(ErrorType::TemporalInvalidCalendarIdentifier, id); } // 12.1.2 AvailableCalendars ( ), https://tc39.es/proposal-temporal/#sec-availablecalendars // 1.1.1 AvailableCalendars ( ), https://tc39.es/proposal-intl-era-monthcode/#sup-availablecalendars Vector const& available_calendars() { // The implementation-defined abstract operation AvailableCalendars takes no arguments and returns a List of calendar // types. The returned List is sorted according to lexicographic code unit order, and contains unique calendar types // in canonical form (12.1) identifying the calendars for which the implementation provides the functionality of // Intl.DateTimeFormat objects, including their aliases (e.g., both "islamicc" and "islamic-civil"). The List must // consist of the "Calendar Type" value of every row of Table 1, except the header row. static auto calendars = []() { auto calendars = Unicode::available_calendars(); for (auto calendar : CLDR_CALENDAR_TYPES) { if (!calendars.contains_slow(calendar)) calendars.append(String::from_utf8_without_validation(calendar.bytes())); } quick_sort(calendars); return calendars; }(); return calendars; } // 12.2.1 ParseMonthCode ( argument ), https://tc39.es/proposal-temporal/#sec-temporal-parsemonthcode ThrowCompletionOr parse_month_code(VM& vm, Value argument) { // 1. Let monthCode be ? ToPrimitive(argument, STRING). auto month_code = TRY(argument.to_primitive(vm, Value::PreferredType::String)); // 2. If monthCode is not a String, throw a TypeError exception. if (!month_code.is_string()) return vm.throw_completion(ErrorType::NotAString, month_code); return parse_month_code(vm, month_code.as_string().utf8_string_view()); } // 12.2.1 ParseMonthCode ( argument ), https://tc39.es/proposal-temporal/#sec-temporal-parsemonthcode ThrowCompletionOr parse_month_code(VM& vm, StringView month_code) { // 3. If ParseText(StringToCodePoints(monthCode), MonthCode) is a List of errors, throw a RangeError exception. if (auto result = Unicode::parse_month_code(month_code); result.has_value()) return result.release_value(); return vm.throw_completion(ErrorType::TemporalInvalidMonthCode); } // 12.3.3 PrepareCalendarFields ( calendar, fields, calendarFieldNames, nonCalendarFieldNames, requiredFieldNames ), https://tc39.es/proposal-temporal/#sec-temporal-preparecalendarfields ThrowCompletionOr prepare_calendar_fields(VM& vm, String const& calendar, Object const& fields, CalendarFieldList calendar_field_names, CalendarFieldList non_calendar_field_names, CalendarFieldListOrPartial required_field_names) { // 1. Assert: If requiredFieldNames is a List, requiredFieldNames contains zero or one of each of the elements of // calendarFieldNames and nonCalendarFieldNames. // 2. Let fieldNames be the list-concatenation of calendarFieldNames and nonCalendarFieldNames. Vector field_names; field_names.append(calendar_field_names.data(), calendar_field_names.size()); field_names.append(non_calendar_field_names.data(), non_calendar_field_names.size()); // 3. Let extraFieldNames be CalendarExtraFields(calendar, calendarFieldNames). auto extra_field_names = calendar_extra_fields(calendar, calendar_field_names); // 4. Set fieldNames to the list-concatenation of fieldNames and extraFieldNames. field_names.extend(move(extra_field_names)); // 5. Assert: fieldNames contains no duplicate elements. // 6. Let result be a Calendar Fields Record with all fields equal to UNSET. auto result = CalendarFields::unset(); // 7. Let any be false. auto any = false; // 8. Let sortedPropertyNames be a List whose elements are the values in the Property Key column of Table 19 // corresponding to the elements of fieldNames, sorted according to lexicographic code unit order. auto sorted_property_names = sorted_calendar_fields(vm, field_names); // 9. For each property name property of sortedPropertyNames, do for (auto const& [key, property, conversion] : sorted_property_names) { // a. Let key be the value in the Enumeration Key column of Table 19 corresponding to the row whose Property Key value is property. // b. Let value be ? Get(fields, property). auto value = TRY(fields.get(property)); // c. If value is not undefined, then if (!value.is_undefined()) { // i. Set any to true. any = true; // ii. Let Conversion be the Conversion value of the same row. switch (conversion) { // iii. If Conversion is TO-INTEGER-WITH-TRUNCATION, then case CalendarFieldConversion::ToIntegerWithTruncation: // 1. Set value to ? ToIntegerWithTruncation(value). // 2. Set value to 𝔽(value). set_field_value(key, result, TRY(to_integer_with_truncation(vm, value, ErrorType::TemporalInvalidCalendarFieldName, *property))); break; // iv. Else if Conversion is TO-POSITIVE-INTEGER-WITH-TRUNCATION, then case CalendarFieldConversion::ToPositiveIntegerWithTruncation: // 1. Set value to ? ToPositiveIntegerWithTruncation(value). // 2. Set value to 𝔽(value). set_field_value(key, result, TRY(to_positive_integer_with_truncation(vm, value, ErrorType::TemporalInvalidCalendarFieldName, *property))); break; // v. Else if Conversion is TO-STRING, then case CalendarFieldConversion::ToString: // 1. Set value to ? ToString(value). set_field_value(key, result, TRY(value.to_string(vm))); break; // vi. Else if Conversion is TO-TEMPORAL-TIME-ZONE-IDENTIFIER, then case CalendarFieldConversion::ToTemporalTimeZoneIdentifier: // 1. Set value to ? ToTemporalTimeZoneIdentifier(value). set_field_value(key, result, TRY(to_temporal_time_zone_identifier(vm, value))); break; // vii. Else if Conversion is TO-MONTH-CODE, then case CalendarFieldConversion::ToMonthCode: { // 1. Let parsed be ? ParseMonthCode(value). auto parsed = TRY(parse_month_code(vm, value)); // 2. Set value to CreateMonthCode(parsed.[[MonthNumber]], parsed.[[IsLeapMonth]]). set_field_value(key, result, Unicode::create_month_code(parsed.month_number, parsed.is_leap_month)); break; } // viii. Else, case CalendarFieldConversion::ToOffsetString: // 1. Assert: Conversion is TO-OFFSET-STRING. // 2. Set value to ? ToOffsetString(value). set_field_value(key, result, TRY(to_offset_string(vm, value))); break; } // ix. Set result's field whose name is given in the Field Name column of the same row to value. } // d. Else if requiredFieldNames is a List, then else if (auto const* required = required_field_names.get_pointer()) { // i. If requiredFieldNames contains key, throw a TypeError exception. if (required->contains_slow(key)) return vm.throw_completion(ErrorType::MissingRequiredProperty, *property); // ii. Set result's field whose name is given in the Field Name column of the same row to the corresponding // Default value of the same row. set_default_field_value(key, result); } } // 10. If requiredFieldNames is PARTIAL and any is false, throw a TypeError exception. if (required_field_names.has() && !any) return vm.throw_completion(ErrorType::TemporalObjectMustBePartialTemporalObject); // 11. Return result. return result; } // 12.3.4 CalendarFieldKeysPresent ( fields ), https://tc39.es/proposal-temporal/#sec-temporal-calendarfieldkeyspresent Vector calendar_field_keys_present(CalendarFields const& fields) { // 1. Let list be « ». Vector list; auto handle_field = [&](auto enumeration_key, auto const& value) { // a. Let value be fields' field whose name is given in the Field Name column of the row. // b. Let enumerationKey be the value in the Enumeration Key column of the row. // c. If value is not unset, append enumerationKey to list. if (value.has_value()) list.append(enumeration_key); }; // 2. For each row of Table 19, except the header row, do #define __JS_ENUMERATE(enumeration, field_name, property_key, conversion) \ handle_field(enumeration, fields.field_name); JS_ENUMERATE_CALENDAR_FIELDS #undef __JS_ENUMERATE // 3. Return list. return list; } // 12.3.5 CalendarMergeFields ( calendar, fields, additionalFields ), https://tc39.es/proposal-temporal/#sec-temporal-calendarmergefields CalendarFields calendar_merge_fields(String const& calendar, CalendarFields const& fields, CalendarFields const& additional_fields) { // 1. Let additionalKeys be CalendarFieldKeysPresent(additionalFields). auto additional_keys = calendar_field_keys_present(additional_fields); // 2. Let overriddenKeys be CalendarFieldKeysToIgnore(calendar, additionalKeys). auto overridden_keys = calendar_field_keys_to_ignore(calendar, additional_keys); // 3. Let merged be a Calendar Fields Record with all fields set to unset. auto merged = CalendarFields::unset(); // 4. Let fieldsKeys be CalendarFieldKeysPresent(fields). auto fields_keys = calendar_field_keys_present(fields); auto merge_field = [&](auto key, auto& merged_field, auto const& fields_field, auto const& additional_fields_field) { // a. Let key be the value in the Enumeration Key column of the row. // b. If fieldsKeys contains key and overriddenKeys does not contain key, then if (fields_keys.contains_slow(key) && !overridden_keys.contains_slow(key)) { // i. Let propValue be fields' field whose name is given in the Field Name column of the row. // ii. Set merged's field whose name is given in the Field Name column of the row to propValue. merged_field = fields_field; } // c. If additionalKeys contains key, then if (additional_keys.contains_slow(key)) { // i. Let propValue be additionalFields' field whose name is given in the Field Name column of the row. // ii. Set merged's field whose name is given in the Field Name column of the row to propValue. merged_field = additional_fields_field; } }; // 5. For each row of Table 19, except the header row, do #define __JS_ENUMERATE(enumeration, field_name, property_key, conversion) \ merge_field(enumeration, merged.field_name, fields.field_name, additional_fields.field_name); JS_ENUMERATE_CALENDAR_FIELDS #undef __JS_ENUMERATE // 6. Return merged. return merged; } // 12.3.6 NonISODateAdd ( calendar, isoDate, duration, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-nonisodateadd // 4.1.18 NonISODateAdd ( calendar, isoDate, duration, overflow ), https://tc39.es/proposal-intl-era-monthcode/#sup-temporal-nonisodateadd ThrowCompletionOr non_iso_date_add(VM& vm, String const& calendar, ISODate iso_date, DateDuration const& duration, Overflow overflow) { // 1. Let parts be CalendarISOToDate(calendar, isoDate). auto parts = non_iso_calendar_iso_to_date(calendar, iso_date); // 2. Let y0 be parts.[[Year]] + duration.[[Years]]. auto y0 = parts.year + static_cast(duration.years); // 3. Let m0 be MonthCodeToOrdinal(calendar, y0, ? ConstrainMonthCode(calendar, y0, parts.[[MonthCode]], overflow)). auto m0 = month_code_to_ordinal(calendar, y0, TRY(constrain_month_code(vm, calendar, y0, parts.month_code, overflow))); // 4. Let endOfMonth be BalanceNonISODate(calendar, y0, m0 + duration.[[Months]] + 1, 0). auto end_of_month = balance_non_iso_date(calendar, y0, static_cast(m0 + duration.months + 1), 0); // 5. Let baseDay be parts.[[Day]]. auto base_day = parts.day; u8 regulated_day = 0; // 6. If baseDay ≤ endOfMonth.[[Day]], then if (base_day <= end_of_month.day) { // a. Let regulatedDay be baseDay. regulated_day = base_day; } // 7. Else, else { // a. If overflow is REJECT, throw a RangeError exception. if (overflow == Overflow::Reject) return vm.throw_completion(ErrorType::TemporalInvalidISODate); // b. Let regulatedDay be endOfMonth.[[Day]]. regulated_day = end_of_month.day; } // 8. Let balancedDate be BalanceNonISODate(calendar, endOfMonth.[[Year]], endOfMonth.[[Month]], regulatedDay + 7 * duration.[[Weeks]] + duration.[[Days]]). auto balanced_date = balance_non_iso_date(calendar, end_of_month.year, end_of_month.month, static_cast(regulated_day + (7 * duration.weeks) + duration.days)); // 9. Let result be ? CalendarIntegersToISO(calendar, balancedDate.[[Year]], balancedDate.[[Month]], balancedDate.[[Day]]). auto result = TRY(calendar_integers_to_iso(vm, calendar, balanced_date.year, balanced_date.month, balanced_date.day)); // 10. If ISODateWithinLimits(result) is false, throw a RangeError exception. // NB: This is handled by the caller, CalendarDateAdd. // 11. Return result. return result; } // 12.3.7 CalendarDateAdd ( calendar, isoDate, duration, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendardateadd ThrowCompletionOr calendar_date_add(VM& vm, String const& calendar, ISODate iso_date, DateDuration const& duration, Overflow overflow) { ISODate result; // 1. If calendar is "iso8601", then if (calendar == ISO8601_CALENDAR) { // a. Let intermediate be BalanceISOYearMonth(isoDate.[[Year]] + duration.[[Years]], isoDate.[[Month]] + duration.[[Months]]). auto intermediate = balance_iso_year_month(static_cast(iso_date.year) + duration.years, static_cast(iso_date.month) + duration.months); // b. Set intermediate to ? RegulateISODate(intermediate.[[Year]], intermediate.[[Month]], isoDate.[[Day]], overflow). auto intermediate_date = TRY(regulate_iso_date(vm, intermediate.year, intermediate.month, iso_date.day, overflow)); // c. Let days be duration.[[Days]] + 7 × duration.[[Weeks]]. auto days = duration.days + (7 * duration.weeks); // d. Let result be AddDaysToISODate(intermediate, days). result = add_days_to_iso_date(intermediate_date, days); } // 2. Else, else { // a. Let result be ? NonISODateAdd(calendar, isoDate, duration, overflow). result = TRY(non_iso_date_add(vm, calendar, iso_date, duration, overflow)); } // 3. If ISODateWithinLimits(result) is false, throw a RangeError exception. if (!iso_date_within_limits(result)) return vm.throw_completion(ErrorType::TemporalInvalidISODate); // 4. Return result. return result; } // 12.3.8 NonISODateUntil ( calendar, one, two, largestUnit ), https://tc39.es/proposal-temporal/#sec-temporal-nonisodateuntil // 4.1.19 NonISODateUntil ( calendar, one, two, largestUnit ), https://tc39.es/proposal-intl-era-monthcode/#sup-temporal-nonisodateuntil DateDuration non_iso_date_until(VM& vm, String const& calendar, ISODate one, ISODate two, Unit largest_unit) { // 1. Let sign be -1 × CompareISODate(one, two). auto sign = compare_iso_date(one, two); sign *= -1; // 2. If sign = 0, return ZeroDateDuration(). if (sign == 0) return zero_date_duration(vm); // OPTIMIZATION: For DAY and WEEK largest units, calendar days equal ISO days. We can compute the difference // directly from ISO epoch days without any calendar arithmetic. if (largest_unit == Unit::Day || largest_unit == Unit::Week) { auto days = iso_date_to_epoch_days(two.year, two.month - 1, two.day) - iso_date_to_epoch_days(one.year, one.month - 1, one.day); double weeks = 0; if (largest_unit == Unit::Week) { weeks = trunc(days / 7.0); days = fmod(days, 7.0); } return MUST(create_date_duration_record(vm, 0, 0, weeks, days)); } // OPTIMIZATION: Pre-compute calendar dates for `from` and `to` to avoid expensive redundant conversions. auto calendar_one = non_iso_calendar_iso_to_date(calendar, one); auto calendar_two = non_iso_calendar_iso_to_date(calendar, two); // 3. Let years be 0. double years = 0; // OPTIMIZATION: If the largestUnit is MONTH, we want to skip ahead to the correct year. If implemented in exact // accordance with the spec, we could enter the second NonISODateSurpasses loop below with a very // large number of months to traverse. // 4. If largestUnit is YEAR, then if (largest_unit == Unit::Year) { // OPTIMIZATION: Skip ahead by estimating the year difference from calendar dates to avoid a large number of // iterations in the NonISODateSurpasses loop below. auto estimated_years = calendar_two.year - calendar_one.year; if (estimated_years != 0) estimated_years -= sign; // a. Let candidateYears be sign. double candidate_years = estimated_years ? estimated_years : static_cast(sign); // b. Repeat, while NonISODateSurpasses(calendar, sign, one, two, candidateYears, 0, 0, 0) is false, while (!non_iso_date_surpasses(vm, calendar, sign, calendar_one, calendar_two, candidate_years, 0, 0, 0)) { // i. Set years to candidateYears. years = candidate_years; // ii. Set candidateYears to candidateYears + sign. candidate_years += sign; } } // 5. Let months be 0. double months = 0; // 6. If largestUnit is YEAR or largestUnit is MONTH, then if (largest_unit == Unit::Year || largest_unit == Unit::Month) { // a. Let candidateMonths be sign. double candidate_months = sign; // OPTIMIZATION: Skip ahead by estimating the month difference from calendar dates to avoid a large number of // iterations in the NonISODateSurpasses loop below. if (largest_unit == Unit::Month) { auto estimated_months = ((calendar_two.year - calendar_one.year) * 12) + (calendar_two.month - calendar_one.month); if (estimated_months != 0) estimated_months -= sign; if (estimated_months != 0) candidate_months = estimated_months; } // b. Repeat, while NonISODateSurpasses(calendar, sign, one, two, years, candidateMonths, 0, 0) is false, while (!non_iso_date_surpasses(vm, calendar, sign, calendar_one, calendar_two, years, candidate_months, 0, 0)) { // i. Set months to candidateMonths. months = candidate_months; // ii. Set candidateMonths to candidateMonths + sign. candidate_months += sign; } } // 9. Let days be 0. double days = 0; // 10. Let candidateDays be sign. double candidate_days = sign; // 11. Repeat, while NonISODateSurpasses(calendar, sign, one, two, years, months, weeks, candidateDays) is false, while (!non_iso_date_surpasses(vm, calendar, sign, calendar_one, calendar_two, years, months, 0, candidate_days)) { // a. Set days to candidateDays. days = candidate_days; // b. Set candidateDays to candidateDays + sign. candidate_days += sign; } // 12. Return ! CreateDateDurationRecord(years, months, weeks, days). return MUST(create_date_duration_record(vm, years, months, 0, days)); } // 12.3.9 CalendarDateUntil ( calendar, one, two, largestUnit ), https://tc39.es/proposal-temporal/#sec-temporal-calendardateuntil DateDuration calendar_date_until(VM& vm, String const& calendar, ISODate one, ISODate two, Unit largest_unit) { // 1. Let sign be CompareISODate(one, two). auto sign = compare_iso_date(one, two); // 2. If sign = 0, return ZeroDateDuration(). if (sign == 0) return zero_date_duration(vm); // 3. If calendar is "iso8601", then if (calendar == ISO8601_CALENDAR) { // a. Set sign to -sign. sign *= -1; // b. Let years be 0. double years = 0; // d. Let months be 0. double months = 0; // OPTIMIZATION: If the largestUnit is MONTH, we want to skip ahead to the correct year. If implemented in exact // accordance with the spec, we could enter the second ISODateSurpasses loop below with a very large // number of months to traverse. // c. If largestUnit is YEAR, then // e. If largestUnit is either YEAR or MONTH, then if (largest_unit == Unit::Year || largest_unit == Unit::Month) { // c.i. Let candidateYears be sign. auto candidate_years = two.year - one.year; if (candidate_years != 0) candidate_years -= sign; // c.ii. Repeat, while ISODateSurpasses(sign, one, two, candidateYears, 0, 0, 0) is false, while (!iso_date_surpasses(vm, sign, one, two, candidate_years, 0, 0, 0)) { // 1. Set years to candidateYears. years = candidate_years; // 2. Set candidateYears to candidateYears + sign. candidate_years += sign; } // e.i. Let candidateMonths be sign. double candidate_months = sign; // e.ii. Repeat, while ISODateSurpasses(sign, one, two, years, candidateMonths, 0, 0) is false, while (!iso_date_surpasses(vm, sign, one, two, years, candidate_months, 0, 0)) { // 1. Set months to candidateMonths. months = candidate_months; // 2. Set candidateMonths to candidateMonths + sign. candidate_months += sign; } if (largest_unit == Unit::Month) { months += years * 12.0; years = 0.0; } } // f. Let weeks be 0. double weeks = 0; // OPTIMIZATION: If the largestUnit is DAY, we do not want to enter an ISODateSurpasses loop. The loop would have // us increment the intermediate ISOYearMonth one day at time, which will take an extremely long // time if the difference is a large number of years. Instead, we can compute the day difference, // and convert to weeks if needed. auto year_month = balance_iso_year_month(static_cast(one.year) + years, static_cast(one.month) + months); auto regulated_date = MUST(regulate_iso_date(vm, year_month.year, year_month.month, one.day, Overflow::Constrain)); auto days = iso_date_to_epoch_days(two.year, two.month - 1, two.day) - iso_date_to_epoch_days(regulated_date.year, regulated_date.month - 1, regulated_date.day); if (largest_unit == Unit::Week) { weeks = trunc(days / 7.0); days = fmod(days, 7.0); } // k. Return ! CreateDateDurationRecord(years, months, weeks, days). return MUST(create_date_duration_record(vm, years, months, weeks, days)); } // 4. Return NonISODateUntil(calendar, one, two, largestUnit). return non_iso_date_until(vm, calendar, one, two, largest_unit); } // 12.3.10 ToTemporalCalendarIdentifier ( temporalCalendarLike ), https://tc39.es/proposal-temporal/#sec-temporal-totemporalcalendaridentifier ThrowCompletionOr to_temporal_calendar_identifier(VM& vm, Value temporal_calendar_like) { // 1. If temporalCalendarLike is an Object and temporalCalendarLike has an [[InitializedTemporalDate]], // [[InitializedTemporalDateTime]], [[InitializedTemporalMonthDay]], [[InitializedTemporalYearMonth]], or // [[InitializedTemporalZonedDateTime]] internal slot, return temporalCalendarLike.[[Calendar]]. if (auto plain_date = temporal_calendar_like.as_if()) return plain_date->calendar(); if (auto plain_date_time = temporal_calendar_like.as_if()) return plain_date_time->calendar(); if (auto plain_month_day = temporal_calendar_like.as_if()) return plain_month_day->calendar(); if (auto plain_year_month = temporal_calendar_like.as_if()) return plain_year_month->calendar(); if (auto zoned_date_time = temporal_calendar_like.as_if()) return zoned_date_time->calendar(); // 2. If temporalCalendarLike is not a String, throw a TypeError exception. if (!temporal_calendar_like.is_string()) return vm.throw_completion(ErrorType::TemporalInvalidCalendar); // 3. Let identifier be ? ParseTemporalCalendarString(temporalCalendarLike). auto identifier = TRY(parse_temporal_calendar_string(vm, temporal_calendar_like.as_string().utf8_string())); // 4. Return ? CanonicalizeCalendar(identifier). return TRY(canonicalize_calendar(vm, identifier)); } // 12.3.11 GetTemporalCalendarIdentifierWithISODefault ( item ), https://tc39.es/proposal-temporal/#sec-temporal-gettemporalcalendarslotvaluewithisodefault ThrowCompletionOr get_temporal_calendar_identifier_with_iso_default(VM& vm, Object const& item) { // 1. If item has an [[InitializedTemporalDate]], [[InitializedTemporalDateTime]], [[InitializedTemporalMonthDay]], // [[InitializedTemporalYearMonth]], or [[InitializedTemporalZonedDateTime]] internal slot, then // a. Return item.[[Calendar]]. if (auto const* plain_date = as_if(item)) return plain_date->calendar(); if (auto const* plain_date_time = as_if(item)) return plain_date_time->calendar(); if (auto const* plain_month_day = as_if(item)) return plain_month_day->calendar(); if (auto const* plain_year_month = as_if(item)) return plain_year_month->calendar(); if (auto const* zoned_date_time = as_if(item)) return zoned_date_time->calendar(); // 2. Let calendarLike be ? Get(item, "calendar"). auto calendar_like = TRY(item.get(vm.names.calendar)); // 3. If calendarLike is undefined, return "iso8601". if (calendar_like.is_undefined()) return "iso8601"_string; // 4. Return ? ToTemporalCalendarIdentifier(calendarLike). return TRY(to_temporal_calendar_identifier(vm, calendar_like)); } // 12.3.12 CalendarDateFromFields ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendardatefromfields ThrowCompletionOr calendar_date_from_fields(VM& vm, String const& calendar, CalendarFields& fields, Overflow overflow) { // 1. Perform ? CalendarResolveFields(calendar, fields, DATE). TRY(calendar_resolve_fields(vm, calendar, fields, DateType::Date)); // 2. Let result be ? CalendarDateToISO(calendar, fields, overflow). auto result = TRY(calendar_date_to_iso(vm, calendar, fields, overflow)); // 3. If ISODateWithinLimits(result) is false, throw a RangeError exception. if (!iso_date_within_limits(result)) return vm.throw_completion(ErrorType::TemporalInvalidISODate); // 4. Return result. return result; } // 12.3.13 CalendarYearMonthFromFields ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendaryearmonthfromfields ThrowCompletionOr calendar_year_month_from_fields(VM& vm, String const& calendar, CalendarFields& fields, Overflow overflow) { // 1. Set fields.[[Day]] to 1. fields.day = 1; // 2. Perform ? CalendarResolveFields(calendar, fields, YEAR-MONTH). TRY(calendar_resolve_fields(vm, calendar, fields, DateType::YearMonth)); // 3. Let result be ? CalendarDateToISO(calendar, fields, overflow). auto result = TRY(calendar_date_to_iso(vm, calendar, fields, overflow)); // 4. If ISOYearMonthWithinLimits(result) is false, throw a RangeError exception. if (!iso_year_month_within_limits(result)) return vm.throw_completion(ErrorType::TemporalInvalidISODate); // 5. Return result. return result; } // 12.3.14 CalendarMonthDayFromFields ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendarmonthdayfromfields ThrowCompletionOr calendar_month_day_from_fields(VM& vm, String const& calendar, CalendarFields& fields, Overflow overflow) { // 1. Perform ? CalendarResolveFields(calendar, fields, MONTH-DAY). TRY(calendar_resolve_fields(vm, calendar, fields, DateType::MonthDay)); // 2. Let result be ? CalendarMonthDayToISOReferenceDate(calendar, fields, overflow). auto result = TRY(calendar_month_day_to_iso_reference_date(vm, calendar, fields, overflow)); // 3. Assert: ISODateWithinLimits(result) is true. VERIFY(iso_date_within_limits(result)); // 4. Return result. return result; } // 12.3.15 FormatCalendarAnnotation ( id, showCalendar ), https://tc39.es/proposal-temporal/#sec-temporal-formatcalendarannotation String format_calendar_annotation(StringView id, ShowCalendar show_calendar) { // 1. If showCalendar is NEVER, return the empty String. if (show_calendar == ShowCalendar::Never) return String {}; // 2. If showCalendar is AUTO and id is "iso8601", return the empty String. if (show_calendar == ShowCalendar::Auto && id == ISO8601_CALENDAR) return String {}; // 3. If showCalendar is CRITICAL, let flag be "!"; else, let flag be the empty String. auto flag = show_calendar == ShowCalendar::Critical ? "!"sv : ""sv; // 4. Return the string-concatenation of "[", flag, "u-ca=", id, and "]". return MUST(String::formatted("[{}u-ca={}]", flag, id)); } // 12.3.16 CalendarEquals ( one, two ), https://tc39.es/proposal-temporal/#sec-temporal-calendarequals bool calendar_equals(StringView one, StringView two) { // 1. If CanonicalizeUValue("ca", one) is CanonicalizeUValue("ca", two), return true. // 2. Return false. return Unicode::canonicalize_unicode_extension_values("ca"sv, one) == Unicode::canonicalize_unicode_extension_values("ca"sv, two); } // 12.3.17 ISODaysInMonth ( year, month ), https://tc39.es/proposal-temporal/#sec-temporal-isodaysinmonth u8 iso_days_in_month(double year, double month) { // 1. If month is one of 1, 3, 5, 7, 8, 10, or 12, return 31. if (first_is_one_of(month, 1, 3, 5, 7, 8, 10, 12)) return 31; // 2. If month is one of 4, 6, 9, or 11, return 30. if (first_is_one_of(month, 4, 6, 9, 11)) return 30; // 3. Assert: month is 2. VERIFY(month == 2); // 4. Return 28 + MathematicalInLeapYear(EpochTimeForYear(year)). return 28 + mathematical_in_leap_year(epoch_time_for_year(year)); } // 12.3.18 ISOWeekOfYear ( isoDate ), https://tc39.es/proposal-temporal/#sec-temporal-isoweekofyear YearWeek iso_week_of_year(ISODate iso_date) { // 1. Let year be isoDate.[[Year]]. auto year = iso_date.year; // 2. Let wednesday be 3. static constexpr auto wednesday = 3; // 3. Let thursday be 4. static constexpr auto thursday = 4; // 4. Let friday be 5. static constexpr auto friday = 5; // 5. Let saturday be 6. static constexpr auto saturday = 6; // 6. Let daysInWeek be 7. static constexpr auto days_in_week = 7; // 7. Let maxWeekNumber be 53. static constexpr auto max_week_number = 53; // 8. Let dayOfYear be ISODayOfYear(isoDate). auto day_of_year = iso_day_of_year(iso_date); // 9. Let dayOfWeek be ISODayOfWeek(isoDate). auto day_of_week = iso_day_of_week(iso_date); // 10. Let week be floor((dayOfYear + daysInWeek - dayOfWeek + wednesday) / daysInWeek). auto week = floor(static_cast(day_of_year + days_in_week - day_of_week + wednesday) / static_cast(days_in_week)); // 11. If week < 1, then if (week < 1) { // a. NOTE: This is the last week of the previous year. // b. Let jan1st be CreateISODateRecord(year, 1, 1). auto jan1st = create_iso_date_record(year, 1, 1); // c. Let dayOfJan1st be ISODayOfWeek(jan1st). auto day_of_jan1st = iso_day_of_week(jan1st); // d. If dayOfJan1st = friday, then if (day_of_jan1st == friday) { // i. Return Year-Week Record { [[Week]]: maxWeekNumber, [[Year]]: year - 1 }. return { .week = max_week_number, .year = year - 1 }; } // e. If dayOfJan1st = saturday, and MathematicalInLeapYear(EpochTimeForYear(year - 1)) = 1, then if (day_of_jan1st == saturday && mathematical_in_leap_year(epoch_time_for_year(year - 1)) == 1) { // i. Return Year-Week Record { [[Week]]: maxWeekNumber. [[Year]]: year - 1 }. return { .week = max_week_number, .year = year - 1 }; } // f. Return Year-Week Record { [[Week]]: maxWeekNumber - 1, [[Year]]: year - 1 }. return { .week = max_week_number - 1, .year = year - 1 }; } // 12. If week = maxWeekNumber, then if (week == max_week_number) { // a. Let daysInYear be MathematicalDaysInYear(year). auto days_in_year = mathematical_days_in_year(year); // b. Let daysLaterInYear be daysInYear - dayOfYear. auto days_later_in_year = days_in_year - day_of_year; // c. Let daysAfterThursday be thursday - dayOfWeek. auto days_after_thursday = thursday - day_of_week; // d. If daysLaterInYear < daysAfterThursday, then if (days_later_in_year < days_after_thursday) { // i. Return Year-Week Record { [[Week]]: 1, [[Year]]: year + 1 }. return { .week = 1, .year = year + 1 }; } } // 13. Return Year-Week Record { [[Week]]: week, [[Year]]: year }. return { .week = week, .year = year }; } // 12.3.19 ISODayOfYear ( isoDate ), https://tc39.es/proposal-temporal/#sec-temporal-isodayofyear u16 iso_day_of_year(ISODate iso_date) { // 1. Let epochDays be ISODateToEpochDays(isoDate.[[Year]], isoDate.[[Month]] - 1, isoDate.[[Day]]). auto epoch_days = iso_date_to_epoch_days(iso_date.year, iso_date.month - 1, iso_date.day); // 2. Return EpochTimeToDayInYear(EpochDaysToEpochMs(epochDays, 0)) + 1. return epoch_time_to_day_in_year(epoch_days_to_epoch_ms(epoch_days, 0)) + 1; } // 12.3.20 ISODayOfWeek ( isoDate ), https://tc39.es/proposal-temporal/#sec-temporal-isodayofweek u8 iso_day_of_week(ISODate iso_date) { // 1. Let epochDays be ISODateToEpochDays(isoDate.[[Year]], isoDate.[[Month]] - 1, isoDate.[[Day]]). auto epoch_days = iso_date_to_epoch_days(iso_date.year, iso_date.month - 1, iso_date.day); // 2. Let dayOfWeek be EpochTimeToWeekDay(EpochDaysToEpochMs(epochDays, 0)). auto day_of_week = epoch_time_to_week_day(epoch_days_to_epoch_ms(epoch_days, 0)); // 3. If dayOfWeek = 0, return 7. if (day_of_week == 0) return 7; // 4. Return dayOfWeek. return day_of_week; } // 12.3.21 NonISOCalendarDateToISO ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-nonisocalendardatetoiso // 4.1.20 NonISOCalendarDateToISO ( calendar, fields, overflow ), https://tc39.es/proposal-intl-era-monthcode/#sup-temporal-nonisocalendardatetoiso ThrowCompletionOr non_iso_calendar_date_to_iso(VM& vm, String const& calendar, CalendarFields const& fields, Overflow overflow) { // 1. Assert: fields.[[Year]], fields.[[Month]], and fields.[[Day]] are not UNSET. VERIFY(fields.year.has_value()); VERIFY(fields.month.has_value()); VERIFY(fields.day.has_value()); // 2. If fields.[[MonthCode]] is not UNSET, then if (fields.month_code.has_value()) { // a. Perform ? ConstrainMonthCode(calendar, fields.[[Year]], fields.[[MonthCode]], overflow). (void)TRY(constrain_month_code(vm, calendar, *fields.year, *fields.month_code, overflow)); } // 3. Let monthsInYear be CalendarMonthsInYear(calendar, fields.[[Year]]). auto months_in_year = calendar_months_in_year(calendar, *fields.year); u8 month = 0; u8 day = 0; // 4. If fields.[[Month]] > monthsInYear, then if (*fields.month > months_in_year) { // a. If overflow is REJECT, throw a RangeError exception. if (overflow == Overflow::Reject) return vm.throw_completion(ErrorType::TemporalInvalidCalendarFieldName, "month"sv); // b. Let month be monthsInYear. month = months_in_year; } // 5. Else, else { // a. Let month be fields.[[Month]]. month = static_cast(*fields.month); } // 6. Let daysInMonth be CalendarDaysInMonth(calendar, fields.[[Year]], fields.[[Month]]). // FIXME: Spec issue: We should use the `month` value that we just constrained. auto days_in_month = calendar_days_in_month(calendar, *fields.year, month); // 7. If fields.[[Day]] > daysInMonth, then if (*fields.day > days_in_month) { // a. If overflow is REJECT, throw a RangeError exception. if (overflow == Overflow::Reject) return vm.throw_completion(ErrorType::TemporalInvalidCalendarFieldName, "day"sv); // b. Let day be daysInMonth. day = days_in_month; } // 8. Else, else { // a. Let day be fields.[[Day]]. day = static_cast(*fields.day); } // 9. Return ? CalendarIntegersToISO(calendar, fields.[[Year]], month, day). return TRY(calendar_integers_to_iso(vm, calendar, *fields.year, month, day)); } // 12.3.22 CalendarDateToISO ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendardatetoiso ThrowCompletionOr calendar_date_to_iso(VM& vm, String const& calendar, CalendarFields const& fields, Overflow overflow) { // 1. If calendar is "iso8601", then if (calendar == ISO8601_CALENDAR) { // a. Assert: fields.[[Year]], fields.[[Month]], and fields.[[Day]] are not UNSET. VERIFY(fields.year.has_value()); VERIFY(fields.month.has_value()); VERIFY(fields.day.has_value()); // b. Return ? RegulateISODate(fields.[[Year]], fields.[[Month]], fields.[[Day]], overflow). return TRY(regulate_iso_date(vm, *fields.year, *fields.month, *fields.day, overflow)); } // 2. Return ? NonISOCalendarDateToISO(calendar, fields, overflow). return non_iso_calendar_date_to_iso(vm, calendar, fields, overflow); } // 12.3.23 NonISOMonthDayToISOReferenceDate ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-nonisomonthdaytoisoreferencedate // 4.1.21 NonISOMonthDayToISOReferenceDate ( calendar, fields, overflow ), https://tc39.es/proposal-intl-era-monthcode/#sup-temporal-nonisomonthdaytoisoreferencedate ThrowCompletionOr non_iso_month_day_to_iso_reference_date(VM& vm, String const& calendar, CalendarFields const& fields, Overflow overflow) { // 1. Assert: fields.[[Day]] is not UNSET. VERIFY(fields.day.has_value()); u8 day = 0; u8 month = 0; u8 days_in_month = 0; String month_code; // 2. If fields.[[Year]] is not UNSET, then if (fields.year.has_value()) { // a. Assert: fields.[[Month]] is not UNSET. VERIFY(fields.month.has_value()); // b. If there exists no combination of inputs such that ! CalendarIntegersToISO(calendar, fields.[[Year]], ..., ...) // would return an ISO Date Record isoDate for which ISODateWithinLimits(isoDate) is true, throw a RangeError exception. // c. NOTE: The above step exists so as not to require calculating whether the month and day described in fields // exist in user-provided years arbitrarily far in the future or past. if (auto iso_date = calendar_integers_to_iso(vm, calendar, *fields.year, 1, 1); iso_date.is_error() || !iso_date_within_limits(iso_date.value())) return vm.throw_completion(ErrorType::TemporalInvalidISODate); // d. Let monthsInYear be CalendarMonthsInYear(calendar, fields.[[Year]]). auto months_in_year = calendar_months_in_year(calendar, *fields.year); // e. If fields.[[Month]] > monthsInYear, then if (*fields.month > months_in_year) { // i. If overflow is REJECT, throw a RangeError exception. if (overflow == Overflow::Reject) return vm.throw_completion(ErrorType::TemporalInvalidCalendarFieldName, "month"sv); // ii. Let month be monthsInYear. month = months_in_year; } // f. Else, else { // i. Let month be fields.[[Month]]. month = static_cast(*fields.month); } // g. If fields.[[MonthCode]] is UNSET, then if (!fields.month_code.has_value()) { // i. Let fieldsISODate be ! CalendarIntegersToISO(calendar, fields.[[Year]], month, 1). auto fields_iso_date = MUST(calendar_integers_to_iso(vm, calendar, *fields.year, month, 1)); // ii. Let monthCode be NonISOCalendarISOToDate(calendar, fieldsISODate).[[MonthCode]]. month_code = non_iso_calendar_iso_to_date(calendar, fields_iso_date).month_code; } // h. Else, else { // i. Let monthCode be ? ConstrainMonthCode(calendar, fields.[[Year]], fields.[[MonthCode]], overflow). month_code = TRY(constrain_month_code(vm, calendar, *fields.year, *fields.month_code, overflow)); } // i. Let daysInMonth be CalendarDaysInMonth(calendar, fields.[[Year]], month). days_in_month = calendar_days_in_month(calendar, *fields.year, month); } // 3. Else, else { // a. Assert: fields.[[MonthCode]] is not UNSET. VERIFY(fields.month_code.has_value()); // b. Let monthCode be fields.[[MonthCode]]. month_code = *fields.month_code; // c. If calendar is "chinese" or "dangi", let daysInMonth be 30; else, let daysInMonth be the maximum number of // days in the month described by monthCode in any year. days_in_month = calendar.is_one_of("chinese"sv, "dangi"sv) ? 30 : Unicode::calendar_max_days_in_month_code(calendar, month_code); } // 4. If fields.[[Day]] > daysInMonth, then if (*fields.day > days_in_month) { // a. If overflow is REJECT, throw a RangeError exception. if (overflow == Overflow::Reject) return vm.throw_completion(ErrorType::TemporalInvalidCalendarFieldName, "day"sv); // b. Let day be daysInMonth. day = days_in_month; } // 5. Else, else { // a. Let day be fields.[[Day]]. day = static_cast(*fields.day); } auto is_chinese_or_dangi = calendar.is_one_of("chinese"sv, "dangi"sv); // 6. If calendar is "chinese" or "dangi", then if (is_chinese_or_dangi) { // a. NOTE: This special case handles combinations of month and day that theoretically could occur but are not // known to have occurred historically and cannot be accurately calculated to occur in the future, even if it // may be possible to construct a PlainDate with such combinations due to inaccurate approximations. This is // explicitly mentioned here because as time goes on, these dates may become known to have occurred // historically, or may be more accurately calculated to occur in the future. // b. Let row be the row in Table 6 with a value in the "Month Code" column matching monthCode. auto reference_year = chinese_or_dangi_reference_year(calendar, month_code, day); // c. If the "Reference Year (Days 1-29)" column of row is "—", or day = 30 and the "Reference Year (Day 30)" // column of row is "—", then if (!reference_year.has_value()) { // i. If overflow is REJECT, throw a RangeError exception. if (overflow == Overflow::Reject) return vm.throw_completion(ErrorType::TemporalInvalidCalendarFieldName, "monthCode"sv); // ii. Set monthCode to CreateMonthCode(! ParseMonthCode(monthCode).[[MonthNumber]], false). if (month_code.ends_with('L')) month_code = MUST(month_code.trim("L"sv, TrimMode::Right)); } } // 7. Let referenceYear be the ISO reference year for monthCode and day as described above. If calendar is "chinese" // or "dangi", the reference years in Table 6 are to be used. // 8. Return the latest possible ISO Date Record isoDate such that isoDate.[[Year]] = referenceYear and // NonISOCalendarISOToDate(calendar, isoDate) returns a Calendar Date Record whose [[MonthCode]] and [[Day]] // field values respectively equal monthCode and day. auto result = [&]() -> Optional { if (is_chinese_or_dangi) { auto reference_year = chinese_or_dangi_reference_year(calendar, month_code, day); return Unicode::iso_year_and_month_code_to_iso_date(calendar, *reference_year, month_code, day); } for (i32 iso_year = 1972; iso_year >= 1900; --iso_year) { if (auto result = Unicode::iso_year_and_month_code_to_iso_date(calendar, iso_year, month_code, day); result.has_value()) return result; } for (i32 iso_year = 1973; iso_year <= 2035; ++iso_year) { if (auto result = Unicode::iso_year_and_month_code_to_iso_date(calendar, iso_year, month_code, day); result.has_value()) return result; } return {}; }(); VERIFY(result.has_value()); return *result; } // 12.3.24 CalendarMonthDayToISOReferenceDate ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendarmonthdaytoisoreferencedate ThrowCompletionOr calendar_month_day_to_iso_reference_date(VM& vm, String const& calendar, CalendarFields const& fields, Overflow overflow) { // 1. If calendar is "iso8601", then if (calendar == ISO8601_CALENDAR) { // a. Assert: fields.[[Month]] and fields.[[Day]] are not UNSET. VERIFY(fields.month.has_value()); VERIFY(fields.day.has_value()); // b. Let referenceISOYear be 1972 (the first ISO 8601 leap year after the epoch). static constexpr i32 reference_iso_year = 1972; // c. If fields.[[Year]] is UNSET, let year be referenceISOYear; else let year be fields.[[Year]]. auto year = !fields.year.has_value() ? reference_iso_year : *fields.year; // d. Let result be ? RegulateISODate(year, fields.[[Month]], fields.[[Day]], overflow). auto result = TRY(regulate_iso_date(vm, year, *fields.month, *fields.day, overflow)); // e. Return CreateISODateRecord(referenceISOYear, result.[[Month]], result.[[Day]]). return create_iso_date_record(reference_iso_year, result.month, result.day); } // 2. Return ? NonISOMonthDayToISOReferenceDate(calendar, fields, overflow). return non_iso_month_day_to_iso_reference_date(vm, calendar, fields, overflow); } // 12.3.25 NonISOCalendarISOToDate ( calendar, isoDate ), https://tc39.es/proposal-temporal/#sec-temporal-nonisocalendarisotodate CalendarDate non_iso_calendar_iso_to_date(String const& calendar, ISODate iso_date) { auto result = Unicode::iso_date_to_calendar_date(calendar, iso_date); for (auto const& row : CALENDAR_ERA_DATA) { if (row.calendar != calendar) continue; i32 era_year = 0; switch (row.kind) { case CalendarEraData::Kind::Epoch: era_year = result.year; break; case CalendarEraData::Kind::Negative: era_year = 1 - result.year; break; case CalendarEraData::Kind::Offset: era_year = result.year - *row.offset + 1; break; } if (row.minimum_era_year.has_value() && era_year < *row.minimum_era_year) continue; if (row.maximum_era_year.has_value() && era_year > *row.maximum_era_year) continue; if (row.iso_era_start.has_value() && compare_iso_date(iso_date, *row.iso_era_start) < 0) continue; result.era = String::from_utf8_without_validation(row.era.bytes()); result.era_year = era_year; break; } return result; } // 12.3.26 CalendarISOToDate ( calendar, isoDate ), https://tc39.es/proposal-temporal/#sec-temporal-calendarisotodate CalendarDate calendar_iso_to_date(String const& calendar, ISODate iso_date) { // 1. If calendar is "iso8601", then if (calendar == ISO8601_CALENDAR) { // a. If MathematicalInLeapYear(EpochTimeForYear(isoDate.[[Year]])) = 1, let inLeapYear be true; else let inLeapYear be false. auto in_leap_year = mathematical_in_leap_year(epoch_time_for_year(iso_date.year)) == 1; // b. Return Calendar Date Record { [[Era]]: undefined, [[EraYear]]: undefined, [[Year]]: isoDate.[[Year]], // [[Month]]: isoDate.[[Month]], [[MonthCode]]: CreateMonthCode(isoDate.[[Month]], false), [[Day]]: isoDate.[[Day]], // [[DayOfWeek]]: ISODayOfWeek(isoDate), [[DayOfYear]]: ISODayOfYear(isoDate), [[WeekOfYear]]: ISOWeekOfYear(isoDate), // [[DaysInWeek]]: 7, [[DaysInMonth]]: ISODaysInMonth(isoDate.[[Year]], isoDate.[[Month]]), // [[DaysInYear]]: MathematicalDaysInYear(isoDate.[[Year]]), [[MonthsInYear]]: 12, [[InLeapYear]]: inLeapYear }. return CalendarDate { .era = {}, .era_year = {}, .year = iso_date.year, .month = iso_date.month, .month_code = Unicode::create_month_code(iso_date.month, false), .day = iso_date.day, .day_of_week = iso_day_of_week(iso_date), .day_of_year = iso_day_of_year(iso_date), .week_of_year = iso_week_of_year(iso_date), .days_in_week = 7, .days_in_month = iso_days_in_month(iso_date.year, iso_date.month), .days_in_year = mathematical_days_in_year(iso_date.year), .months_in_year = 12, .in_leap_year = in_leap_year, }; } // 2. Return NonISOCalendarISOToDate(calendar, isoDate). return non_iso_calendar_iso_to_date(calendar, iso_date); } // 12.3.27 CalendarExtraFields ( calendar, fields ), https://tc39.es/proposal-temporal/#sec-temporal-calendarextrafields // 4.1.22 CalendarExtraFields ( calendar, fields ), https://tc39.es/proposal-intl-era-monthcode/#sup-temporal-calendarextrafields Vector calendar_extra_fields(String const& calendar, CalendarFieldList fields) { // 1. If fields contains an element equal to YEAR and CalendarSupportsEra(calendar) is true, then if (fields.contains_slow(CalendarField::Year) && calendar_supports_era(calendar)) { // a. Return « ERA, ERA-YEAR ». return { CalendarField::Era, CalendarField::EraYear }; } // 2. Return an empty List. return {}; } // 12.3.28 NonISOFieldKeysToIgnore ( calendar, keys ), https://tc39.es/proposal-temporal/#sec-temporal-nonisofieldkeystoignore // 4.1.23 NonISOFieldKeysToIgnore ( calendar, keys ), https://tc39.es/proposal-intl-era-monthcode/#sup-temporal-nonisofieldkeystoignore Vector non_iso_field_keys_to_ignore(String const& calendar, ReadonlySpan keys) { // 1. Let ignoredKeys be a copy of keys. Vector ignored_keys { keys }; // 2. For each element key of keys, do for (auto key : keys) { // a. If key is MONTH, append MONTH-CODE to ignoredKeys. if (key == CalendarField::Month) ignored_keys.append(CalendarField::MonthCode); // b. If key is MONTH-CODE, append month. if (key == CalendarField::MonthCode) ignored_keys.append(CalendarField::Month); // c. If key is one of ERA, ERA-YEAR, or YEAR and CalendarSupportsEra(calendar) is true, then if (first_is_one_of(key, CalendarField::Era, CalendarField::EraYear, CalendarField::Year) && calendar_supports_era(calendar)) { // i. Append ERA, ERA-YEAR, and YEAR to ignoredKeys. ignored_keys.append(CalendarField::Era); ignored_keys.append(CalendarField::EraYear); ignored_keys.append(CalendarField::Year); } // d. If key is one of DAY, MONTH, or MONTH-CODE and CalendarHasMidYearEras(calendar) is true, then if (first_is_one_of(key, CalendarField::Day, CalendarField::Month, CalendarField::MonthCode) && calendar_has_mid_year_eras(calendar)) { // i. Append ERA and ERA-YEAR to ignoredKeys. ignored_keys.append(CalendarField::Era); ignored_keys.append(CalendarField::EraYear); } } // 3. NOTE: While ignoredKeys can have duplicate elements, this is not intended to be meaningful. This specification // only checks whether particular keys are or are not members of the list. // 4. Return ignoredKeys. return ignored_keys; } // 12.3.29 CalendarFieldKeysToIgnore ( calendar, keys ), https://tc39.es/proposal-temporal/#sec-temporal-calendarfieldkeystoignore Vector calendar_field_keys_to_ignore(String const& calendar, ReadonlySpan keys) { // 1. If calendar is "iso8601", then if (calendar == ISO8601_CALENDAR) { // a. Let ignoredKeys be a new empty List. Vector ignored_keys; // b. For each element key of keys, do for (auto key : keys) { // i. Append key to ignoredKeys. ignored_keys.append(key); // ii. If key is MONTH, append MONTH-CODE to ignoredKeys. if (key == CalendarField::Month) ignored_keys.append(CalendarField::MonthCode); // iii. Else if key is MONTH-CODE, append MONTH to ignoredKeys. else if (key == CalendarField::MonthCode) ignored_keys.append(CalendarField::Month); } // c. NOTE: While ignoredKeys can have duplicate elements, this is not intended to be meaningful. This specification // only checks whether particular keys are or are not members of the list. // d. Return ignoredKeys. return ignored_keys; } // 2. Return NonISOFieldKeysToIgnore(calendar, keys). return non_iso_field_keys_to_ignore(calendar, keys); } // 12.3.30 NonISOResolveFields ( calendar, fields, type ), https://tc39.es/proposal-temporal/#sec-temporal-nonisoresolvefields // 4.1.24 NonISOResolveFields ( calendar, fields, type ), https://tc39.es/proposal-intl-era-monthcode/#sup-temporal-nonisoresolvefields ThrowCompletionOr non_iso_resolve_fields(VM& vm, String const& calendar, CalendarFields& fields, DateType type) { // 1. Let needsYear be false. // 2. If type is DATE or type is YEAR-MONTH, set needsYear to true. // 3. If fields.[[MonthCode]] is UNSET, set needsYear to true. // 4. If fields.[[Month]] is not UNSET, set needsYear to true. auto needs_year = type == DateType::Date || type == DateType::YearMonth || !fields.month_code.has_value() || fields.month.has_value(); // 5. Let needsOrdinalMonth be false. // 6. If fields.[[Year]] is not UNSET, set needsOrdinalMonth to true. // 7. If fields.[[EraYear]] is not UNSET, set needsOrdinalMonth to true. auto needs_ordinal_month = fields.year.has_value() || fields.era_year.has_value(); // 8. Let needsDay be false. // 9. If type is DATE or type is MONTH-DAY, set needsDay to true. auto needs_day = type == DateType::Date || type == DateType::MonthDay; // 10. If needsYear is true, then if (needs_year) { // a. If fields.[[Year]] is UNSET, then if (!fields.year.has_value()) { // i. If CalendarSupportsEra(calendar) is false, throw a TypeError exception. if (!calendar_supports_era(calendar)) return vm.throw_completion(ErrorType::MissingRequiredProperty, "year"sv); // ii. If fields.[[Era]] is UNSET or fields.[[EraYear]] is UNSET, throw a TypeError exception. if (!fields.era.has_value() || !fields.era_year.has_value()) return vm.throw_completion(ErrorType::MissingRequiredProperty, "era"sv); } } // 11. If CalendarSupportsEra(calendar) is true, then if (calendar_supports_era(calendar)) { // a. If fields.[[Era]] is not UNSET and fields.[[EraYear]] is UNSET, throw a TypeError exception. if (fields.era.has_value() && !fields.era_year.has_value()) return vm.throw_completion(ErrorType::MissingRequiredProperty, "eraYear"sv); // b. If fields.[[EraYear]] is not UNSET and fields.[[Era]] is UNSET, throw a TypeError exception. if (fields.era_year.has_value() && !fields.era.has_value()) return vm.throw_completion(ErrorType::MissingRequiredProperty, "era"sv); } // 12. If needsDay is true and fields.[[Day]] is UNSET, throw a TypeError exception. if (needs_day && !fields.day.has_value()) return vm.throw_completion(ErrorType::MissingRequiredProperty, "day"sv); // 13. If fields.[[Month]] is UNSET and fields.[[MonthCode]] is UNSET, throw a TypeError exception. if (!fields.month.has_value() && !fields.month_code.has_value()) return vm.throw_completion(ErrorType::MissingRequiredProperty, "month"sv); // 14. If CalendarSupportsEra(calendar) is true and fields.[[EraYear]] is not UNSET, then if (calendar_supports_era(calendar) && fields.era_year.has_value()) { // a. Let canonicalEra be CanonicalizeEraInCalendar(calendar, fields.[[Era]]). auto canonical_era = canonicalize_era_in_calendar(calendar, *fields.era); // b. If canonicalEra is undefined, throw a RangeError exception. if (!canonical_era.has_value()) return vm.throw_completion(ErrorType::TemporalInvalidCalendarFieldName, "era"sv); // c. Let arithmeticYear be CalendarDateArithmeticYearForEraYear(calendar, canonicalEra, fields.[[EraYear]]). auto arithmetic_year = calendar_date_arithmetic_year_for_era_year(calendar, *canonical_era, *fields.era_year); // d. If fields.[[Year]] is not UNSET, and fields.[[Year]] ≠ arithmeticYear, throw a RangeError exception. if (fields.year.has_value() && *fields.year != arithmetic_year) return vm.throw_completion(ErrorType::TemporalInvalidCalendarFieldName, "year"sv); // e. Set fields.[[Year]] to arithmeticYear. fields.year = arithmetic_year; } // 15. Set fields.[[Era]] to UNSET. fields.era = {}; // 16. Set fields.[[EraYear]] to UNSET. fields.era_year = {}; // 17. NOTE: fields.[[Era]] and fields.[[EraYear]] are erased in order to allow a lenient interpretation of // out-of-bounds values, which is particularly useful for consistent interpretation of dates in calendars with // regnal eras. // 18. If fields.[[MonthCode]] is not UNSET, then if (fields.month_code.has_value()) { // a. If IsValidMonthCodeForCalendar(calendar, fields.[[MonthCode]]) is false, throw a RangeError exception. if (!is_valid_month_code_for_calendar(calendar, *fields.month_code)) return vm.throw_completion(ErrorType::TemporalInvalidCalendarFieldName, "monthCode"sv); // b. If fields.[[Year]] is not UNSET, then if (fields.year.has_value()) { // i. If YearContainsMonthCode(calendar, fields.[[Year]], fields.[[MonthCode]]) is true, let constrainedMonthCode be fields.[[MonthCode]]; // else let constrainedMonthCode be ! ConstrainMonthCode(calendar, fields.[[Year]], fields.[[MonthCode]], CONSTRAIN). auto constrained_month_code = year_contains_month_code(calendar, *fields.year, *fields.month_code) ? *fields.month_code : MUST(constrain_month_code(vm, calendar, *fields.year, *fields.month_code, Overflow::Constrain)); // ii. Let month be MonthCodeToOrdinal(calendar, fields.[[Year]], constrainedMonthCode). auto month = month_code_to_ordinal(calendar, *fields.year, constrained_month_code); // iii. If fields.[[Month]] is not UNSET and fields.[[Month]] ≠ month, throw a RangeError exception. if (fields.month.has_value() && *fields.month != month) return vm.throw_completion(ErrorType::TemporalInvalidCalendarFieldName, "month"sv); // iv. Set fields.[[Month]] to month. fields.month = month; // v. NOTE: fields.[[MonthCode]] is intentionally not overwritten with constrainedMonthCode. Pending the // "overflow" parameter in CalendarDateToISO or CalendarMonthDayToISOReferenceDate, a month code not // occurring in fields.[[Year]] may cause that operation to throw. However, if fields.[[Month]] is // present, it must agree with the constrained month code. } } // 19. Assert: fields.[[Era]] and fields.[[EraYear]] are UNSET. VERIFY(!fields.era.has_value()); VERIFY(!fields.era_year.has_value()); // 20. Assert: If needsYear is true, fields.[[Year]] is not UNSET. if (needs_year) VERIFY(fields.year.has_value()); // 21. Assert: If needsOrdinalMonth is true, fields.[[Month]] is not UNSET. if (needs_ordinal_month) VERIFY(fields.month.has_value()); // 22. Assert: If needsDay is true, fields.[[Day]] is not UNSET. if (needs_day) VERIFY(fields.day.has_value()); // 23. Return unused. return {}; } // 12.3.31 CalendarResolveFields ( calendar, fields, type ), https://tc39.es/proposal-temporal/#sec-temporal-calendarresolvefields ThrowCompletionOr calendar_resolve_fields(VM& vm, String const& calendar, CalendarFields& fields, DateType type) { // 1. If calendar is "iso8601", then if (calendar == ISO8601_CALENDAR) { // a. Let needsYear be false. // b. If type is either DATE or YEAR-MONTH, set needsYear to true. auto needs_year = type == DateType::Date || type == DateType::YearMonth; // c. Let needsDay be false. // d. If type is either DATE or MONTH-DAY, set needsDay to true. auto needs_day = type == DateType::Date || type == DateType::MonthDay; // e. If needsYear is true and fields.[[Year]] is UNSET, throw a TypeError exception. if (needs_year && !fields.year.has_value()) return vm.throw_completion(ErrorType::MissingRequiredProperty, "year"sv); // f. If needsDay is true and fields.[[Day]] is UNSET, throw a TypeError exception. if (needs_day && !fields.day.has_value()) return vm.throw_completion(ErrorType::MissingRequiredProperty, "day"sv); // g. If fields.[[Month]] is UNSET and fields.[[MonthCode]] is UNSET, throw a TypeError exception. if (!fields.month.has_value() && !fields.month_code.has_value()) return vm.throw_completion(ErrorType::MissingRequiredProperty, "month"sv); // h. If fields.[[MonthCode]] is not UNSET, then if (fields.month_code.has_value()) { // i. Let parsedMonthCode be ! ParseMonthCode(fields.[[MonthCode]]). auto parsed_month_code = Unicode::parse_month_code(*fields.month_code).release_value(); // ii. If parsedMonthCode.[[IsLeapMonth]] is true, throw a RangeError exception. if (parsed_month_code.is_leap_month) return vm.throw_completion(ErrorType::TemporalInvalidCalendarFieldName, "monthCode"sv); // iii. Let month be parsedMonthCode.[[MonthNumber]]. auto month = parsed_month_code.month_number; // iv. If month > 12, throw a RangeError exception. if (month > 12) return vm.throw_completion(ErrorType::TemporalInvalidCalendarFieldName, "monthCode"sv); // v. If fields.[[Month]] is not UNSET and fields.[[Month]] ≠ month, throw a RangeError exception. if (fields.month.has_value() && fields.month != month) return vm.throw_completion(ErrorType::TemporalInvalidCalendarFieldName, "month"sv); // vi. Set fields.[[Month]] to month. fields.month = month; } } // 2. Else, else { // a. Perform ? NonISOResolveFields(calendar, fields, type). return TRY(non_iso_resolve_fields(vm, calendar, fields, type)); } // 3. Return UNUSED. return {}; } // 4.1.1 CalendarSupportsEra ( calendar ), https://tc39.es/proposal-intl-era-monthcode/#sec-temporal-calendarsupportsera bool calendar_supports_era(String const& calendar) { // 1. If calendar is listed in the "Calendar" column of Table 2, return true. // 2. If calendar is listed in the "Calendar Type" column of Table 1, return false. // 3. Return an implementation-defined value. return find_value(CALENDAR_ERA_DATA, [&](auto const& row) { return row.calendar == calendar; }).has_value(); } // 4.1.2 CanonicalizeEraInCalendar ( calendar, era ), https://tc39.es/proposal-intl-era-monthcode/#sec-temporal-canonicalizeeraincalendar Optional canonicalize_era_in_calendar(String const& calendar, StringView era) { // 1. For each row of Table 2, except the header row, do for (auto const& row : CALENDAR_ERA_DATA) { // a. Let cal be the Calendar value of the current row. // b. If cal is equal to calendar, then if (row.calendar == calendar) { // i. Let canonicalName be the Era value of the current row. auto canonical_name = row.era; // ii. If canonicalName is equal to era, return canonicalName. if (canonical_name == era) return canonical_name; // iii. Let aliases be a List whose elements are the strings given in the "Aliases" column of the row. // iv. If aliases contains era, return canonicalName. if (row.alias == era) return canonical_name; } } // 2. If calendar is listed in the "Calendar Type" column of Table 1, return undefined. // 3. Return an implementation-defined value. return {}; } // 4.1.3 CalendarHasMidYearEras ( calendar ), https://tc39.es/proposal-intl-era-monthcode/#sec-temporal-calendarhasmidyeareras bool calendar_has_mid_year_eras(String const& calendar) { // 1. If calendar is "japanese", return true. // 2. If calendar is listed in the "Calendar Type" column of Table 1, return false. // 3. Return an implementation-defined value. return calendar == "japanese"sv; } // 4.1.4 IsValidMonthCodeForCalendar ( calendar, monthCode ), https://tc39.es/proposal-intl-era-monthcode/#sec-temporal-isvalidmonthcodeforcalendar bool is_valid_month_code_for_calendar(String const& calendar, StringView month_code) { // 1. Let commonMonthCodes be « "M01", "M02", "M03", "M04", "M05", "M06", "M07", "M08", "M09", "M10", "M11", "M12" ». // 2. If commonMonthCodes contains monthCode, return true. if (month_code.is_one_of("M01"sv, "M02"sv, "M03"sv, "M04"sv, "M05"sv, "M06"sv, "M07"sv, "M08"sv, "M09"sv, "M10"sv, "M11"sv, "M12"sv)) return true; // 3. If calendar is listed in the "Calendar" column of Table 3, then if (auto row = find_value(ADDITIONAL_MONTH_CODES, [&](auto const& row) { return row.calendar == calendar; }); row.has_value()) { // a. Let r be the row in Table 3 with a value in the Calendar column matching calendar. // b. Let specialMonthCodes be a List whose elements are the strings given in the "Additional Month Codes" column of r. // c. If specialMonthCodes contains monthCode, return true. // d. Return false. return row->additional_month_codes.contains_slow(month_code); } // 4. If calendar is listed in the "Calendar Type" column of Table 1, return false. // 5. Return an implementation-defined value. return false; } // 4.1.5 YearContainsMonthCode ( calendar, arithmeticYear, monthCode ), https://tc39.es/proposal-intl-era-monthcode/#sec-temporal-yearcontainsmonthcode bool year_contains_month_code(String const& calendar, i32 arithmetic_year, StringView month_code) { // 1. Assert: IsValidMonthCodeForCalendar(calendar, monthCode) is true. VERIFY(is_valid_month_code_for_calendar(calendar, month_code)); // 2. If ! ParseMonthCode(monthCode).[[IsLeap]] is false, return true. if (!Unicode::parse_month_code(month_code)->is_leap_month) return true; // 3. Return whether the leap month indicated by monthCode exists in the year arithmeticYear in calendar, using // calendar-dependent behaviour. return Unicode::calendar_year_contains_month_code(calendar, arithmetic_year, month_code); } // 4.1.6 ConstrainMonthCode ( calendar, arithmeticYear, monthCode, overflow ), https://tc39.es/proposal-intl-era-monthcode/#sec-temporal-constrainmonthcode ThrowCompletionOr constrain_month_code(VM& vm, String const& calendar, i32 arithmetic_year, String const& month_code, Overflow overflow) { // 1. Assert: IsValidMonthCodeForCalendar(calendar, monthCode) is true. VERIFY(is_valid_month_code_for_calendar(calendar, month_code)); // 2. If YearContainsMonthCode(calendar, arithmeticYear, monthCode) is true, return monthCode. if (year_contains_month_code(calendar, arithmetic_year, month_code)) return month_code; // 3. If overflow is REJECT, throw a RangeError exception. if (overflow == Overflow::Reject) return vm.throw_completion(ErrorType::TemporalInvalidMonthCode); // 4. Assert: calendar is listed in the "Calendar" column of Table 3. // 5. Let r be the row in Table 3 with a value in the Calendar column matching calendar. auto row = find_value(ADDITIONAL_MONTH_CODES, [&](auto const& row) { return row.calendar == calendar; }); VERIFY(row.has_value()); // 6. Let shiftType be the value given in the "Leap to Common Month Transformation" column of r. // 7. If shiftType is SKIP-BACKWARD, then if (row->leap_to_common_month_transformation == AdditionalMonthCodes::Leap::SkipBackward) { // a. Return CreateMonthCode(! ParseMonthCode(monthCode).[[MonthNumber]], false). return MUST(month_code.trim("L"sv, TrimMode::Right)); } // 8. Else, // a. Assert: monthCode is "M05L". VERIFY(month_code == "M05L"); // b. Return "M06". return "M06"_string; } // 4.1.7 MonthCodeToOrdinal ( calendar, arithmeticYear, monthCode ), https://tc39.es/proposal-intl-era-monthcode/#sec-temporal-monthcodetoordinal u8 month_code_to_ordinal(String const& calendar, i32 arithmetic_year, StringView month_code) { // 1. Assert: YearContainsMonthCode(calendar, arithmeticYear, monthCode) is true. VERIFY(year_contains_month_code(calendar, arithmetic_year, month_code)); // 2. Let monthsBefore be 0. auto months_before = 0; // 3. Let number be 1. auto number = 1; // 4. Let isLeap be false. auto is_leap = false; // 5. Let r be the row in Table 3 which the calendar is in the Calendar column. auto row = find_value(ADDITIONAL_MONTH_CODES, [&](auto const& row) { return row.calendar == calendar; }); // 6. If the "Leap to Common Month Transformation" column of r is empty, then if (!row.has_value() || !row->leap_to_common_month_transformation.has_value()) { // a. Return ! ParseMonthCode(monthCode).[[MonthNumber]]. return Unicode::parse_month_code(month_code)->month_number; } // 7. Assert: The "Additional Month Codes" column of r does not contain "M00L" or "M13". VERIFY(!row->additional_month_codes.contains_slow("M00L"sv)); VERIFY(!row->additional_month_codes.contains_slow("M13"sv)); // 8. Assert: This algorithm will return before the following loop terminates by failing its condition. // 9. Repeat, while number ≤ 12, while (number <= 12) { // a. Let currentMonthCode be CreateMonthCode(number, isLeap). auto current_month_code = Unicode::create_month_code(number, is_leap); // b. If IsValidMonthCodeForCalendar(calendar, currentMonthCode) is true and YearContainsMonthCode(calendar, arithmeticYear, currentMonthCode) is true, then if (is_valid_month_code_for_calendar(calendar, current_month_code) && year_contains_month_code(calendar, arithmetic_year, current_month_code)) { // i. Set monthsBefore to monthsBefore + 1. ++months_before; } // c. If currentMonthCode is monthCode, then if (current_month_code == month_code) { // i. Return monthsBefore. return months_before; } // d. If isLeap is false, then // i. Set isLeap to true. // e. Else, // i. Set isLeap to false. // ii. Set number to number + 1. if (exchange(is_leap, !is_leap)) ++number; } VERIFY_NOT_REACHED(); } // 4.1.8 CalendarDaysInMonth ( calendar, arithmeticYear, ordinalMonth ), https://tc39.es/proposal-intl-era-monthcode/#sec-temporal-calendardaysinmonth u8 calendar_days_in_month(String const& calendar, i32 arithmetic_year, u8 ordinal_month) { // 1. Let isoDate be ! CalendarIntegersToISO(calendar, arithmeticYear, ordinalMonth, 1). // 2. Return CalendarISOToDate(calendar, isoDate).[[DaysInMonth]]. return Unicode::calendar_days_in_month(calendar, arithmetic_year, ordinal_month); } // 4.1.12 CalendarDateArithmeticYearForEraYear ( calendar, era, eraYear ), https://tc39.es/proposal-intl-era-monthcode/#sec-temporal-calendardatearithmeticyearforerayear i32 calendar_date_arithmetic_year_for_era_year(String const& calendar, StringView era, i32 era_year) { // 1. Let era be CanonicalizeEraInCalendar(calendar, era). // 2. Assert: era is not undefined. era = canonicalize_era_in_calendar(calendar, era).release_value(); // 3. If calendar is not listed in the "Calendar Type" column of Table 1, return an implementation-defined value. // 4. Let r be the row in Table 2 with a value in the Calendar column matching calendar and a value in the Era // column matching era. auto row = find_value(CALENDAR_ERA_DATA, [&](auto const& row) { return row.calendar == calendar && row.era == era; }); if (!row.has_value()) return era_year; // 5. Let eraKind be the value given in the "Era Kind" column of r. auto era_kind = row->kind; // 6. Let offset be the value given in the "Offset" column of r. auto offset = row->offset; switch (era_kind) { // 7. If eraKind is EPOCH, return eraYear. case CalendarEraData::Kind::Epoch: return era_year; // 8. If eraKind is NEGATIVE, return 1 - eraYear. case CalendarEraData::Kind::Negative: return 1 - era_year; // 9. Assert: eraKind is OFFSET. case CalendarEraData::Kind::Offset: // 10. Assert: offset is not undefined. VERIFY(offset.has_value()); // 11. Return offset + eraYear - 1. return *offset + era_year - 1; } VERIFY_NOT_REACHED(); } // 4.1.13 CalendarIntegersToISO ( calendar, arithmeticYear, ordinalMonth, day ), https://tc39.es/proposal-intl-era-monthcode/#sec-temporal-calendarintegerstoiso ThrowCompletionOr calendar_integers_to_iso(VM& vm, String const& calendar, i32 arithmetic_year, u8 ordinal_month, u8 day) { // 1. If arithmeticYear, ordinalMonth, and day do not form a valid date in calendar, throw a RangeError exception. // 2. Let isoDate be an ISO Date Record such that CalendarISOToDate(calendar, isoDate) returns a Calendar Date Record // whose [[Year]], [[Month]], and [[Day]] field values respectively equal arithmeticYear, ordinalMonth, and day. // 3. NOTE: No known calendars have repeated dates that would cause isoDate to be ambiguous between two ISO Date Records. // 4. Return isoDate. if (auto iso_date = Unicode::calendar_date_to_iso_date(calendar, arithmetic_year, ordinal_month, day); iso_date.has_value()) return create_iso_date_record(iso_date->year, iso_date->month, iso_date->day); return vm.throw_completion(ErrorType::TemporalInvalidISODate); } // 4.1.15 CalendarMonthsInYear ( calendar, arithmeticYear ), https://tc39.es/proposal-intl-era-monthcode/#sec-temporal-calendarmonthsinyear u8 calendar_months_in_year(String const& calendar, i32 arithmetic_year) { // 1. Let isoDate be ! CalendarIntegersToISO(calendar, arithmeticYear, 1, 1). // 2. Return CalendarISOToDate(calendar, isoDate).[[MonthsInYear]]. return Unicode::calendar_months_in_year(calendar, arithmetic_year); } // 4.1.16 BalanceNonISODate ( calendar, arithmeticYear, ordinalMonth, day ), https://tc39.es/proposal-intl-era-monthcode/#sec-temporal-balancenonisodate BalancedDate balance_non_iso_date(String const& calendar, i32 arithmetic_year, i32 ordinal_month, i32 day) { // 1. Let resolvedYear be arithmeticYear. auto resolved_year = arithmetic_year; // 2. Let resolvedMonth be ordinalMonth. auto resolved_month = ordinal_month; // 3. Let monthsInYear be CalendarMonthsInYear(calendar, resolvedYear). auto months_in_year = calendar_months_in_year(calendar, resolved_year); // 4. Repeat, while resolvedMonth ≤ 0, while (resolved_month <= 0) { // a. Set resolvedYear to resolvedYear - 1. --resolved_year; // b. Set monthsInYear to CalendarMonthsInYear(calendar, resolvedYear). months_in_year = calendar_months_in_year(calendar, resolved_year); // c. Set resolvedMonth to resolvedMonth + monthsInYear. resolved_month += months_in_year; } // 5. Repeat, while resolvedMonth > monthsInYear, while (resolved_month > months_in_year) { // a. Set resolvedMonth to resolvedMonth - monthsInYear. resolved_month -= months_in_year; // b. Set resolvedYear to resolvedYear + 1. ++resolved_year; // c. Set monthsInYear to CalendarMonthsInYear(calendar, resolvedYear). months_in_year = calendar_months_in_year(calendar, resolved_year); } // 6. Let resolvedDay be day. auto resolved_day = day; // 7. Let daysInMonth be CalendarDaysInMonth(calendar, resolvedYear, resolvedMonth). auto days_in_month = calendar_days_in_month(calendar, resolved_year, resolved_month); // 8. Repeat, while resolvedDay ≤ 0, while (resolved_day <= 0) { // a. Set resolvedMonth to resolvedMonth - 1. --resolved_month; // b. If resolvedMonth is 0, then if (resolved_month == 0) { // i. Set resolvedYear to resolvedYear - 1. --resolved_year; // ii. Set monthsInYear to CalendarMonthsInYear(calendar, resolvedYear). months_in_year = calendar_months_in_year(calendar, resolved_year); // iii. Set resolvedMonth to monthsInYear. resolved_month = months_in_year; } // c. Set daysInMonth to CalendarDaysInMonth(calendar, resolvedYear, resolvedMonth). days_in_month = calendar_days_in_month(calendar, resolved_year, resolved_month); // d. Set resolvedDay to resolvedDay + daysInMonth. resolved_day += days_in_month; } // 9. Repeat, while resolvedDay > daysInMonth, while (resolved_day > days_in_month) { // a. Set resolvedDay to resolvedDay - daysInMonth. resolved_day -= days_in_month; // b. Set resolvedMonth to resolvedMonth + 1. ++resolved_month; // c. If resolvedMonth > monthsInYear, then if (resolved_month > months_in_year) { // i. Set resolvedYear to resolvedYear + 1. ++resolved_year; // ii. Set monthsInYear to CalendarMonthsInYear(calendar, resolvedYear). months_in_year = calendar_months_in_year(calendar, resolved_year); // iii. Set resolvedMonth to 1. resolved_month = 1; } // d. Set daysInMonth to CalendarDaysInMonth(calendar, resolvedYear, resolvedMonth). days_in_month = calendar_days_in_month(calendar, resolved_year, resolved_month); } // 10. Return the Record { [[Year]]: resolvedYear, [[Month]]: resolvedMonth, [[Day]]: resolvedDay }. return { .year = resolved_year, .month = static_cast(resolved_month), .day = static_cast(resolved_day) }; } // 4.1.17 NonISODateSurpasses ( calendar, sign, fromIsoDate, toIsoDate, years, months, weeks, days ), https://tc39.es/proposal-intl-era-monthcode/#sec-temporal-nonisodatesurpasses // NB: The only caller to this function is NonISODateUntil, which precomputes the calendar dates. bool non_iso_date_surpasses(VM& vm, String const& calendar, i8 sign, CalendarDate const& from_calendar_date, CalendarDate const& to_calendar_date, double years, double months, double weeks, double days) { // 1. Let parts be CalendarISOToDate(calendar, fromIsoDate). auto const& parts = from_calendar_date; // 2. Let calDate2 be CalendarISOToDate(calendar, toIsoDate). auto const& calendar_date_2 = to_calendar_date; // 3. Let y0 be parts.[[Year]] + years. auto y0 = parts.year + static_cast(years); // 4. If CompareSurpasses(sign, y0, parts.[[MonthCode]], parts.[[Day]], calDate2) is true, return true. if (compare_surpasses(sign, y0, parts.month_code, parts.day, calendar_date_2)) return true; // 5. Let m0 be MonthCodeToOrdinal(calendar, y0, ! ConstrainMonthCode(calendar, y0, parts.[[MonthCode]], CONSTRAIN)). auto m0 = month_code_to_ordinal(calendar, y0, MUST(constrain_month_code(vm, calendar, y0, parts.month_code, Overflow::Constrain))); // 6. Let monthsAdded be BalanceNonISODate(calendar, y0, m0 + months, 1). auto months_added = balance_non_iso_date(calendar, y0, m0 + static_cast(months), 1); // 7. If CompareSurpasses(sign, monthsAdded.[[Year]], monthsAdded.[[Month]], parts.[[Day]], calDate2) is true, return true. if (compare_surpasses(sign, months_added.year, months_added.month, parts.day, calendar_date_2)) return true; // 8. If weeks = 0 and days = 0, return false. if (weeks == 0 && days == 0) return false; // 9. Let endOfMonth be BalanceNonISODate(calendar, monthsAdded.[[Year]], monthsAdded.[[Month]] + 1, 0). auto end_of_month = balance_non_iso_date(calendar, months_added.year, months_added.month + 1, 0); // 10. Let baseDay be parts.[[Day]]. auto base_day = parts.day; // 11. If baseDay ≤ endOfMonth.[[Day]], then // a. Let regulatedDay be baseDay. // 12. Else, // a. Let regulatedDay be endOfMonth.[[Day]]. auto regulated_day = base_day <= end_of_month.day ? base_day : end_of_month.day; // 13. Let daysInWeek be 7 (the number of days in a week for all supported calendars). static constexpr auto days_in_week = 7; // 14. Let balancedDate be BalanceNonISODate(calendar, endOfMonth.[[Year]], endOfMonth.[[Month]], regulatedDay + daysInWeek * weeks + days). auto balanced_date = balance_non_iso_date(calendar, end_of_month.year, end_of_month.month, static_cast(regulated_day + (days_in_week * weeks) + days)); // 15. Return CompareSurpasses(sign, balancedDate.[[Year]], balancedDate.[[Month]], balancedDate.[[Day]], calDate2). return compare_surpasses(sign, balanced_date.year, balanced_date.month, balanced_date.day, calendar_date_2); } }