From 1a386e78c3bc962883f28d3c4aba58f4dffbbfeb Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Wed, 20 Nov 2024 12:59:15 -0500 Subject: [PATCH] LibJS: Implement the Temporal.PlainMonthDay constructor And the simple Temporal.PlainMonthDay.prototype getters, so that the constructed Temporal.PlainMonthDay may actually be validated. --- Libraries/LibJS/CMakeLists.txt | 9 + Libraries/LibJS/Forward.h | 17 +- Libraries/LibJS/Print.cpp | 12 + Libraries/LibJS/Runtime/ErrorTypes.h | 2 + Libraries/LibJS/Runtime/Intrinsics.cpp | 2 + .../Runtime/Temporal/AbstractOperations.cpp | 479 +++++++++++++- .../Runtime/Temporal/AbstractOperations.h | 72 ++- Libraries/LibJS/Runtime/Temporal/Calendar.cpp | 586 ++++++++++++++++++ Libraries/LibJS/Runtime/Temporal/Calendar.h | 114 ++++ .../LibJS/Runtime/Temporal/DateEquations.cpp | 89 +++ .../LibJS/Runtime/Temporal/DateEquations.h | 24 + .../LibJS/Runtime/Temporal/PlainDate.cpp | 99 +++ Libraries/LibJS/Runtime/Temporal/PlainDate.h | 30 + .../LibJS/Runtime/Temporal/PlainDateTime.cpp | 55 ++ .../LibJS/Runtime/Temporal/PlainDateTime.h | 25 + .../LibJS/Runtime/Temporal/PlainMonthDay.cpp | 144 +++++ .../LibJS/Runtime/Temporal/PlainMonthDay.h | 37 ++ .../Temporal/PlainMonthDayConstructor.cpp | 102 +++ .../Temporal/PlainMonthDayConstructor.h | 33 + .../Temporal/PlainMonthDayPrototype.cpp | 70 +++ .../Runtime/Temporal/PlainMonthDayPrototype.h | 31 + .../LibJS/Runtime/Temporal/PlainTime.cpp | 83 +++ Libraries/LibJS/Runtime/Temporal/PlainTime.h | 30 + Libraries/LibJS/Runtime/Temporal/Temporal.cpp | 2 + Libraries/LibJS/Runtime/Temporal/TimeZone.cpp | 132 ++++ Libraries/LibJS/Runtime/Temporal/TimeZone.h | 28 + .../PlainMonthDay/PlainMonthDay.from.js | 104 ++++ .../Temporal/PlainMonthDay/PlainMonthDay.js | 66 ++ .../PlainMonthDay.prototype.calendarId.js | 14 + .../PlainMonthDay.prototype.day.js | 14 + .../PlainMonthDay.prototype.monthCode.js | 14 + 31 files changed, 2515 insertions(+), 4 deletions(-) create mode 100644 Libraries/LibJS/Runtime/Temporal/Calendar.cpp create mode 100644 Libraries/LibJS/Runtime/Temporal/Calendar.h create mode 100644 Libraries/LibJS/Runtime/Temporal/DateEquations.cpp create mode 100644 Libraries/LibJS/Runtime/Temporal/DateEquations.h create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainDate.cpp create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainDate.h create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainDateTime.cpp create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainDateTime.h create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainMonthDay.cpp create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainMonthDay.h create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainMonthDayConstructor.cpp create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainMonthDayConstructor.h create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainTime.cpp create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainTime.h create mode 100644 Libraries/LibJS/Runtime/Temporal/TimeZone.cpp create mode 100644 Libraries/LibJS/Runtime/Temporal/TimeZone.h create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.from.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.calendarId.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.day.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.monthCode.js diff --git a/Libraries/LibJS/CMakeLists.txt b/Libraries/LibJS/CMakeLists.txt index ae7f6450493..c010b88da9b 100644 --- a/Libraries/LibJS/CMakeLists.txt +++ b/Libraries/LibJS/CMakeLists.txt @@ -205,12 +205,21 @@ set(SOURCES Runtime/SymbolObject.cpp Runtime/SymbolPrototype.cpp Runtime/Temporal/AbstractOperations.cpp + Runtime/Temporal/Calendar.cpp + Runtime/Temporal/DateEquations.cpp Runtime/Temporal/Duration.cpp Runtime/Temporal/DurationConstructor.cpp Runtime/Temporal/DurationPrototype.cpp Runtime/Temporal/Instant.cpp Runtime/Temporal/ISO8601.cpp + Runtime/Temporal/PlainDate.cpp + Runtime/Temporal/PlainDateTime.cpp + Runtime/Temporal/PlainMonthDay.cpp + Runtime/Temporal/PlainMonthDayConstructor.cpp + Runtime/Temporal/PlainMonthDayPrototype.cpp + Runtime/Temporal/PlainTime.cpp Runtime/Temporal/Temporal.cpp + Runtime/Temporal/TimeZone.cpp Runtime/TypedArray.cpp Runtime/TypedArrayConstructor.cpp Runtime/TypedArrayPrototype.cpp diff --git a/Libraries/LibJS/Forward.h b/Libraries/LibJS/Forward.h index a09c4e16cd6..e73e9051da4 100644 --- a/Libraries/LibJS/Forward.h +++ b/Libraries/LibJS/Forward.h @@ -87,8 +87,9 @@ __JS_ENUMERATE(RelativeTimeFormat, relative_time_format, RelativeTimeFormatPrototype, RelativeTimeFormatConstructor) \ __JS_ENUMERATE(Segmenter, segmenter, SegmenterPrototype, SegmenterConstructor) -#define JS_ENUMERATE_TEMPORAL_OBJECTS \ - __JS_ENUMERATE(Duration, duration, DurationPrototype, DurationConstructor) +#define JS_ENUMERATE_TEMPORAL_OBJECTS \ + __JS_ENUMERATE(Duration, duration, DurationPrototype, DurationConstructor) \ + __JS_ENUMERATE(PlainMonthDay, plain_month_day, PlainMonthDayPrototype, PlainMonthDayConstructor) #define JS_ENUMERATE_BUILTIN_NAMESPACE_OBJECTS \ __JS_ENUMERATE(AtomicsObject, atomics) \ @@ -277,6 +278,18 @@ JS_ENUMERATE_TEMPORAL_OBJECTS #undef __JS_ENUMERATE class Temporal; + +struct CalendarDate; +struct CalendarFields; +struct DateDuration; +struct InternalDuration; +struct ISODate; +struct ISODateTime; +struct ParseResult; +struct PartialDuration; +struct Time; +struct TimeZone; +struct TimeZoneOffset; }; template diff --git a/Libraries/LibJS/Print.cpp b/Libraries/LibJS/Print.cpp index bfda483c4d9..6a894526049 100644 --- a/Libraries/LibJS/Print.cpp +++ b/Libraries/LibJS/Print.cpp @@ -48,6 +48,7 @@ #include #include #include +#include #include #include #include @@ -835,6 +836,15 @@ ErrorOr print_temporal_duration(JS::PrintContext& print_context, JS::Tempo return {}; } +ErrorOr print_temporal_plain_month_day(JS::PrintContext& print_context, JS::Temporal::PlainMonthDay const& plain_month_day, HashTable& seen_objects) +{ + TRY(print_type(print_context, "Temporal.PlainMonthDay"sv)); + TRY(js_out(print_context, " \033[34;1m{:02}-{:02}\033[0m", plain_month_day.iso_date().month, plain_month_day.iso_date().day)); + TRY(js_out(print_context, "\n calendar: ")); + TRY(print_value(print_context, JS::PrimitiveString::create(plain_month_day.vm(), plain_month_day.calendar()), seen_objects)); + return {}; +} + ErrorOr print_boolean_object(JS::PrintContext& print_context, JS::BooleanObject const& boolean_object, HashTable& seen_objects) { TRY(print_type(print_context, "Boolean"sv)); @@ -952,6 +962,8 @@ ErrorOr print_value(JS::PrintContext& print_context, JS::Value value, Hash return print_intl_duration_format(print_context, static_cast(object), seen_objects); if (is(object)) return print_temporal_duration(print_context, static_cast(object), seen_objects); + if (is(object)) + return print_temporal_plain_month_day(print_context, static_cast(object), seen_objects); return print_object(print_context, object, seen_objects); } diff --git a/Libraries/LibJS/Runtime/ErrorTypes.h b/Libraries/LibJS/Runtime/ErrorTypes.h index 83442c3a602..e5301f763f1 100644 --- a/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Libraries/LibJS/Runtime/ErrorTypes.h @@ -249,6 +249,7 @@ M(TemporalInvalidCalendarFunctionResult, "Invalid calendar, {}() function returned {}") \ M(TemporalInvalidCalendarIdentifier, "Invalid calendar identifier '{}'") \ M(TemporalInvalidCalendarString, "Invalid calendar string '{}'") \ + M(TemporalInvalidCriticalAnnotation, "Invalid critical annotation: '{}={}'") \ M(TemporalInvalidDateTimeString, "Invalid date time string '{}'") \ M(TemporalInvalidDateTimeStringUTCDesignator, "Invalid date time string '{}': must not contain a UTC designator") \ M(TemporalInvalidDuration, "Invalid duration") \ @@ -295,6 +296,7 @@ "nanoseconds with the opposite sign") \ M(TemporalNanosecondsConvertedToRemainderOfNanosecondsLongerThanDayLength, "Time zone or calendar ended up with a remainder of " \ "nanoseconds longer than the day length") \ + M(TemporalObjectMustBePartialTemporalObject, "Object must be a partial Temporal object") \ M(TemporalObjectMustHaveOneOf, "Object must have at least one of the following properties: {}") \ M(TemporalObjectMustNotHave, "Object must not have a defined {} property") \ M(TemporalPropertyMustBeFinite, "Property must not be Infinity") \ diff --git a/Libraries/LibJS/Runtime/Intrinsics.cpp b/Libraries/LibJS/Runtime/Intrinsics.cpp index 9d929d3f494..f511939245c 100644 --- a/Libraries/LibJS/Runtime/Intrinsics.cpp +++ b/Libraries/LibJS/Runtime/Intrinsics.cpp @@ -101,6 +101,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp index 47f32ab13cb..f84c1c9fa76 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp @@ -9,11 +9,15 @@ #include #include +#include #include #include +#include #include -#include #include +#include +#include +#include namespace JS::Temporal { @@ -43,6 +47,43 @@ StringView temporal_unit_to_string(Unit unit) return temporal_units[to_underlying(unit)].singular_property_name; } +// 13.1 ISODateToEpochDays ( year, month, date ), https://tc39.es/proposal-temporal/#sec-isodatetoepochdays +double iso_date_to_epoch_days(double year, double month, double date) +{ + // 1. Let resolvedYear be year + floor(month / 12). + // 2. Let resolvedMonth be month modulo 12. + // 3. Find a time t such that EpochTimeToEpochYear(t) = resolvedYear, EpochTimeToMonthInYear(t) = resolvedMonth, and EpochTimeToDate(t) = 1. + // 4. Return EpochTimeToDayNumber(t) + date - 1. + + // EDITOR'S NOTE: This operation corresponds to ECMA-262 operation MakeDay(year, month, date). It calculates the + // result in mathematical values instead of Number values. These two operations would be unified when + // https://github.com/tc39/ecma262/issues/1087 is fixed. + + // Since we don't have a real MV type to work with, let's defer to MakeDay. + return JS::make_day(year, month, date); +} + +// 13.2 EpochDaysToEpochMs ( day, time ), https://tc39.es/proposal-temporal/#sec-epochdaystoepochms +double epoch_days_to_epoch_ms(double day, double time) +{ + // 1. Return day × ℝ(msPerDay) + time. + return day * JS::ms_per_day + time; +} + +// 13.6 GetTemporalOverflowOption ( options ), https://tc39.es/proposal-temporal/#sec-temporal-gettemporaloverflowoption +ThrowCompletionOr get_temporal_overflow_option(VM& vm, Object const& options) +{ + // 1. Let stringValue be ? GetOption(options, "overflow", STRING, « "constrain", "reject" », "constrain"). + auto string_value = TRY(get_option(vm, options, vm.names.overflow, OptionType::String, { "constrain"sv, "reject"sv }, "constrain"sv)); + + // 2. If stringValue is "constrain", return CONSTRAIN. + if (string_value.as_string().utf8_string() == "constrain"sv) + return Overflow::Constrain; + + // 3. Return REJECT. + return Overflow::Reject; +} + // 13.14 ValidateTemporalRoundingIncrement ( increment, dividend, inclusive ), https://tc39.es/proposal-temporal/#sec-validatetemporalroundingincrement ThrowCompletionOr validate_temporal_rounding_increment(VM& vm, u64 increment, u64 dividend, bool inclusive) { @@ -422,6 +463,27 @@ String format_fractional_seconds(u64 sub_second_nanoseconds, Precision precision return MUST(String::formatted(".{}", fraction_string)); } +// 13.25 FormatTimeString ( hour, minute, second, subSecondNanoseconds, precision [ , style ] ), https://tc39.es/proposal-temporal/#sec-temporal-formattimestring +String format_time_string(u8 hour, u8 minute, u8 second, u16 sub_second_nanoseconds, SecondsStringPrecision::Precision precision, Optional style) +{ + // 1. If style is present and style is UNSEPARATED, let separator be the empty String; otherwise, let separator be ":". + auto separator = style == TimeStyle::Unseparated ? ""sv : ":"sv; + + // 2. Let hh be ToZeroPaddedDecimalString(hour, 2). + // 3. Let mm be ToZeroPaddedDecimalString(minute, 2). + + // 4. If precision is minute, return the string-concatenation of hh, separator, and mm. + if (precision.has()) + return MUST(String::formatted("{:02}{}{:02}", hour, separator, minute)); + + // 5. Let ss be ToZeroPaddedDecimalString(second, 2). + // 6. Let subSecondsPart be FormatFractionalSeconds(subSecondNanoseconds, precision). + auto sub_seconds_part = format_fractional_seconds(sub_second_nanoseconds, precision.downcast()); + + // 7. Return the string-concatenation of hh, separator, mm, separator, ss, and subSecondsPart. + return MUST(String::formatted("{:02}{}{:02}{}{:02}{}", hour, separator, minute, separator, second, sub_seconds_part)); +} + // 13.26 GetUnsignedRoundingMode ( roundingMode, sign ), https://tc39.es/proposal-temporal/#sec-getunsignedroundingmode UnsignedRoundingMode get_unsigned_rounding_mode(RoundingMode rounding_mode, Sign sign) { @@ -664,6 +726,260 @@ Crypto::SignedBigInteger round_number_to_increment(Crypto::SignedBigInteger cons return rounded.multiplied_by(increment); } +// 13.33 ParseISODateTime ( isoString, allowedFormats ), https://tc39.es/proposal-temporal/#sec-temporal-parseisodatetime +ThrowCompletionOr parse_iso_date_time(VM& vm, StringView iso_string, ReadonlySpan allowed_formats) +{ + // 1. Let parseResult be EMPTY. + Optional parse_result; + + // 2. Let calendar be EMPTY. + Optional calendar; + + // 3. Let yearAbsent be false. + auto year_absent = false; + + // 4. For each nonterminal goal of allowedFormats, do + for (auto goal : allowed_formats) { + // a. If parseResult is not a Parse Node, then + if (parse_result.has_value()) + break; + + // i. Set parseResult to ParseText(StringToCodePoints(isoString), goal). + parse_result = parse_iso8601(goal, iso_string); + + // ii. If parseResult is a Parse Node, then + if (parse_result.has_value()) { + // 1. Let calendarWasCritical be false. + auto calendar_was_critical = false; + + // 2. For each Annotation Parse Node annotation contained within parseResult, do + for (auto const& annotation : parse_result->annotations) { + // a. Let key be the source text matched by the AnnotationKey Parse Node contained within annotation. + auto const& key = annotation.key; + + // b. Let value be the source text matched by the AnnotationValue Parse Node contained within annotation. + auto const& value = annotation.value; + + // c. If CodePointsToString(key) is "u-ca", then + if (key == "u-ca"sv) { + // i. If calendar is EMPTY, then + if (!calendar.has_value()) { + // i. Set calendar to CodePointsToString(value). + calendar = String::from_utf8_without_validation(value.bytes()); + + // ii. If annotation contains an AnnotationCriticalFlag Parse Node, set calendarWasCritical to true. + if (annotation.critical) + calendar_was_critical = true; + } + // ii. Else, + else { + // i. If annotation contains an AnnotationCriticalFlag Parse Node, or calendarWasCritical is true, + // throw a RangeError exception. + if (annotation.critical || calendar_was_critical) + return vm.throw_completion(ErrorType::TemporalInvalidCriticalAnnotation, key, value); + } + } + // d. Else, + else { + // i. If annotation contains an AnnotationCriticalFlag Parse Node, throw a RangeError exception. + if (annotation.critical) + return vm.throw_completion(ErrorType::TemporalInvalidCriticalAnnotation, key, value); + } + } + + // 3. If goal is TemporalMonthDayString or TemporalYearMonthString, calendar is not EMPTY, and the + // ASCII-lowercase of calendar is not "iso8601", throw a RangeError exception. + if (goal == Production::TemporalMonthDayString || goal == Production::TemporalYearMonthString) { + if (calendar.has_value() && !calendar->equals_ignoring_ascii_case("iso8601"sv)) + return vm.throw_completion(ErrorType::TemporalInvalidCalendarIdentifier, *calendar); + } + + // 4. If goal is TemporalMonthDayString and parseResult does not contain a DateYear Parse Node, then + if (goal == Production::TemporalMonthDayString && !parse_result->date_year.has_value()) { + // a. Assert: goal is the last element of allowedFormats. + VERIFY(goal == allowed_formats.last()); + + // b. Set yearAbsent to true. + year_absent = true; + } + } + } + + // 5. If parseResult is not a Parse Node, throw a RangeError exception. + if (!parse_result.has_value()) + return vm.throw_completion(ErrorType::TemporalInvalidISODateTime); + + // 6. NOTE: Applications of StringToNumber below do not lose precision, since each of the parsed values is guaranteed + // to be a sufficiently short string of decimal digits. + + // 7. Let each of year, month, day, hour, minute, second, and fSeconds be the source text matched by the respective + // DateYear, DateMonth, DateDay, the first Hour, the first MinuteSecond, TimeSecond, and the first + // TemporalDecimalFraction Parse Node contained within parseResult, or an empty sequence of code points if not present. + auto year = parse_result->date_year.value_or({}); + auto month = parse_result->date_month.value_or({}); + auto day = parse_result->date_day.value_or({}); + auto hour = parse_result->time_hour.value_or({}); + auto minute = parse_result->time_minute.value_or({}); + auto second = parse_result->time_second.value_or({}); + auto fractional_seconds = parse_result->time_fraction.value_or({}); + + // 8. Let yearMV be ℝ(StringToNumber(CodePointsToString(year))). + auto year_value = string_to_number(year); + + // 9. If month is empty, then + // a. Let monthMV be 1. + // 10. Else, + // a. Let monthMV be ℝ(StringToNumber(CodePointsToString(month))). + auto month_value = month.is_empty() ? 1 : string_to_number(month); + + // 11. If day is empty, then + // a. Let dayMV be 1. + // 12. Else, + // a. Let dayMV be ℝ(StringToNumber(CodePointsToString(day))). + auto day_value = day.is_empty() ? 1 : string_to_number(day); + + // 13. If hour is empty, then + // a. Let hourMV be 0. + // 14. Else, + // a. Let hourMV be ℝ(StringToNumber(CodePointsToString(hour))). + auto hour_value = hour.is_empty() ? 0 : string_to_number(hour); + + // 15. If minute is empty, then + // a. Let minuteMV be 0. + // 16. Else, + // a. Let minuteMV be ℝ(StringToNumber(CodePointsToString(minute))). + auto minute_value = minute.is_empty() ? 0 : string_to_number(minute); + + // 17. If second is empty, then + // a. Let secondMV be 0. + // 18. Else, + // a. Let secondMV be ℝ(StringToNumber(CodePointsToString(second))). + // b. If secondMV = 60, then + // i. Set secondMV to 59. + auto second_value = second.is_empty() ? 0 : min(string_to_number(second), 59.0); + + double millisecond_value = 0; + double microsecond_value = 0; + double nanosecond_value = 0; + + // 19. If fSeconds is not empty, then + if (!fractional_seconds.is_empty()) { + // a. Let fSecondsDigits be the substring of CodePointsToString(fSeconds) from 1. + auto fractional_seconds_digits = fractional_seconds.substring_view(1); + + // b. Let fSecondsDigitsExtended be the string-concatenation of fSecondsDigits and "000000000". + auto fractional_seconds_extended = MUST(String::formatted("{}000000000", fractional_seconds_digits)); + + // c. Let millisecond be the substring of fSecondsDigitsExtended from 0 to 3. + auto millisecond = fractional_seconds_extended.bytes_as_string_view().substring_view(0, 3); + + // d. Let microsecond be the substring of fSecondsDigitsExtended from 3 to 6. + auto microsecond = fractional_seconds_extended.bytes_as_string_view().substring_view(3, 3); + + // e. Let nanosecond be the substring of fSecondsDigitsExtended from 6 to 9. + auto nanosecond = fractional_seconds_extended.bytes_as_string_view().substring_view(6, 3); + + // f. Let millisecondMV be ℝ(StringToNumber(millisecond)). + millisecond_value = string_to_number(millisecond); + + // g. Let microsecondMV be ℝ(StringToNumber(microsecond)). + microsecond_value = string_to_number(microsecond); + + // h. Let nanosecondMV be ℝ(StringToNumber(nanosecond)). + nanosecond_value = string_to_number(nanosecond); + } + // 20. Else, + else { + // a. Let millisecondMV be 0. + // b. Let microsecondMV be 0. + // c. Let nanosecondMV be 0. + } + + // 21. Assert: IsValidISODate(yearMV, monthMV, dayMV) is true. + VERIFY(is_valid_iso_date(year_value, month_value, day_value)); + + Variant time { ParsedISODateTime::StartOfDay {} }; + + // 22. If hour is empty, then + if (hour.is_empty()) { + // a. Let time be START-OF-DAY. + } + // 23. Else, + else { + // a. Let time be CreateTimeRecord(hourMV, minuteMV, secondMV, millisecondMV, microsecondMV, nanosecondMV). + time = create_time_record(hour_value, minute_value, second_value, millisecond_value, microsecond_value, nanosecond_value); + } + + // 24. Let timeZoneResult be ISO String Time Zone Parse Record { [[Z]]: false, [[OffsetString]]: EMPTY, [[TimeZoneAnnotation]]: EMPTY }. + ParsedISOTimeZone time_zone_result; + + // 25. If parseResult contains a TimeZoneIdentifier Parse Node, then + if (parse_result->time_zone_identifier.has_value()) { + // a. Let identifier be the source text matched by the TimeZoneIdentifier Parse Node contained within parseResult. + // b. Set timeZoneResult.[[TimeZoneAnnotation]] to CodePointsToString(identifier). + time_zone_result.time_zone_annotation = String::from_utf8_without_validation(parse_result->time_zone_identifier->bytes()); + } + + // 26. If parseResult contains a UTCDesignator Parse Node, then + if (parse_result->utc_designator.has_value()) { + // a. Set timeZoneResult.[[Z]] to true. + time_zone_result.z_designator = true; + } + // 27. Else if parseResult contains a UTCOffset[+SubMinutePrecision] Parse Node, then + else if (parse_result->date_time_offset.has_value()) { + // a. Let offset be the source text matched by the UTCOffset[+SubMinutePrecision] Parse Node contained within parseResult. + // b. Set timeZoneResult.[[OffsetString]] to CodePointsToString(offset). + time_zone_result.offset_string = String::from_utf8_without_validation(parse_result->date_time_offset->source_text.bytes()); + } + + // 28. If yearAbsent is true, let yearReturn be EMPTY; else let yearReturn be yearMV. + Optional year_return; + if (!year_absent) + year_return = static_cast(year_value); + + // 29. Return ISO Date-Time Parse Record { [[Year]]: yearReturn, [[Month]]: monthMV, [[Day]]: dayMV, [[Time]]: time, [[TimeZone]]: timeZoneResult, [[Calendar]]: calendar }. + return ParsedISODateTime { .year = year_return, .month = static_cast(month_value), .day = static_cast(day_value), .time = move(time), .time_zone = move(time_zone_result), .calendar = move(calendar) }; +} + +// 13.34 ParseTemporalCalendarString ( string ), https://tc39.es/proposal-temporal/#sec-temporal-parsetemporalcalendarstring +ThrowCompletionOr parse_temporal_calendar_string(VM& vm, String const& string) +{ + // 1. Let parseResult be Completion(ParseISODateTime(string, « TemporalDateTimeString[+Zoned], TemporalDateTimeString[~Zoned], + // TemporalInstantString, TemporalTimeString, TemporalMonthDayString, TemporalYearMonthString »)). + static constexpr auto productions = to_array({ + Production::TemporalZonedDateTimeString, + Production::TemporalDateTimeString, + Production::TemporalInstantString, + Production::TemporalTimeString, + Production::TemporalMonthDayString, + Production::TemporalYearMonthString, + }); + + auto parse_result = parse_iso_date_time(vm, string, productions); + + // 2. If parseResult is a normal completion, then + if (!parse_result.is_error()) { + // a. Let calendar be parseResult.[[Value]].[[Calendar]]. + auto calendar = parse_result.value().calendar; + + // b. If calendar is empty, return "iso8601". + // c. Else, return calendar. + return calendar.value_or("iso8601"_string); + } + // 3. Else, + else { + // a. Set parseResult to ParseText(StringToCodePoints(string), AnnotationValue). + auto annotation_parse_result = parse_iso8601(Production::AnnotationValue, string); + + // b. If parseResult is a List of errors, throw a RangeError exception. + if (!annotation_parse_result.has_value()) + return vm.throw_completion(ErrorType::TemporalInvalidCalendarString, string); + + // c. Else, return string. + return string; + } +} + // 13.35 ParseTemporalDurationString ( isoString ), https://tc39.es/proposal-temporal/#sec-temporal-parsetemporaldurationstring ThrowCompletionOr> parse_temporal_duration_string(VM& vm, StringView iso_string) { @@ -884,6 +1200,152 @@ ThrowCompletionOr> parse_temporal_duration_string(VM& vm, Stri return TRY(create_temporal_duration(vm, years_value, months_value, weeks_value, days_value, hours_value, factored_minutes_value, factored_seconds_value, factored_milliseconds_value, factored_microseconds_value, factored_nanoseconds_value)); } +// 13.36 ParseTemporalTimeZoneString ( timeZoneString ), https://tc39.es/proposal-temporal/#sec-temporal-parsetemporaltimezonestring +ThrowCompletionOr parse_temporal_time_zone_string(VM& vm, StringView time_zone_string) +{ + // 1. Let parseResult be ParseText(StringToCodePoints(timeZoneString), TimeZoneIdentifier). + auto parse_result = parse_iso8601(Production::TimeZoneIdentifier, time_zone_string); + + // 2. If parseResult is a Parse Node, then + if (parse_result.has_value()) { + // a. Return ! ParseTimeZoneIdentifier(timeZoneString). + return parse_time_zone_identifier(parse_result.release_value()); + } + + // 3. Let result be ? ParseISODateTime(timeZoneString, « TemporalDateTimeString[+Zoned], TemporalDateTimeString[~Zoned], + // TemporalInstantString, TemporalTimeString, TemporalMonthDayString, TemporalYearMonthString »). + static constexpr auto productions = to_array({ + Production::TemporalZonedDateTimeString, + Production::TemporalDateTimeString, + Production::TemporalInstantString, + Production::TemporalTimeString, + Production::TemporalMonthDayString, + Production::TemporalYearMonthString, + }); + + auto result = TRY(parse_iso_date_time(vm, time_zone_string, productions)); + + // 4. Let timeZoneResult be result.[[TimeZone]]. + auto time_zone_result = move(result.time_zone); + + // 5. If timeZoneResult.[[TimeZoneAnnotation]] is not empty, then + if (time_zone_result.time_zone_annotation.has_value()) { + // a. Return ! ParseTimeZoneIdentifier(timeZoneResult.[[TimeZoneAnnotation]]). + return MUST(parse_time_zone_identifier(vm, *time_zone_result.time_zone_annotation)); + } + + // 6. If timeZoneResult.[[Z]] is true, then + if (time_zone_result.z_designator) { + // a. Return ! ParseTimeZoneIdentifier("UTC"). + return MUST(parse_time_zone_identifier(vm, "UTC"sv)); + } + + // 7. If timeZoneResult.[[OffsetString]] is not empty, then + if (time_zone_result.offset_string.has_value()) { + // a. Return ? ParseTimeZoneIdentifier(timeZoneResult.[[OffsetString]]). + return TRY(parse_time_zone_identifier(vm, *time_zone_result.offset_string)); + } + + // 8. Throw a RangeError exception. + return vm.throw_completion(ErrorType::TemporalInvalidTimeZoneString, time_zone_string); +} + +// 13.40 ToMonthCode ( argument ), https://tc39.es/proposal-temporal/#sec-temporal-tomonthcode +ThrowCompletionOr to_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::TemporalInvalidMonthCode); + auto month_code_string = month_code.as_string().utf8_string_view(); + + // 3. If the length of monthCode is not 3 or 4, throw a RangeError exception. + if (month_code_string.length() != 3 && month_code_string.length() != 4) + return vm.throw_completion(ErrorType::TemporalInvalidMonthCode); + + // 4. If the first code unit of monthCode is not 0x004D (LATIN CAPITAL LETTER M), throw a RangeError exception. + if (month_code_string[0] != 'M') + return vm.throw_completion(ErrorType::TemporalInvalidMonthCode); + + // 5. If the second code unit of monthCode is not in the inclusive interval from 0x0030 (DIGIT ZERO) to 0x0039 (DIGIT NINE), + // throw a RangeError exception. + if (!is_ascii_digit(month_code_string[1]) || parse_ascii_digit(month_code_string[1]) > 9) + return vm.throw_completion(ErrorType::TemporalInvalidMonthCode); + + // 6. If the third code unit of monthCode is not in the inclusive interval from 0x0030 (DIGIT ZERO) to 0x0039 (DIGIT NINE), + // throw a RangeError exception. + if (!is_ascii_digit(month_code_string[2]) || parse_ascii_digit(month_code_string[2]) > 9) + return vm.throw_completion(ErrorType::TemporalInvalidMonthCode); + + // 7. If the length of monthCode is 4 and the fourth code unit of monthCode is not 0x004C (LATIN CAPITAL LETTER L), + // throw a RangeError exception. + if (month_code_string.length() == 4 && month_code_string[3] != 'L') + return vm.throw_completion(ErrorType::TemporalInvalidMonthCode); + + // 8. Let monthCodeDigits be the substring of monthCode from 1 to 3. + auto month_code_digits = month_code_string.substring_view(1, 2); + + // 9. Let monthCodeInteger be ℝ(StringToNumber(monthCodeDigits)). + auto month_code_integer = month_code_digits.to_number().value(); + + // 10. If monthCodeInteger is 0 and the length of monthCode is not 4, throw a RangeError exception. + if (month_code_integer == 0 && month_code_string.length() != 4) + return vm.throw_completion(ErrorType::TemporalInvalidMonthCode); + + // 11. Return monthCode. + return month_code.as_string().utf8_string(); +} + +// 13.41 ToOffsetString ( argument ), https://tc39.es/proposal-temporal/#sec-temporal-tooffsetstring +ThrowCompletionOr to_offset_string(VM& vm, Value argument) +{ + // 1. Let offset be ? ToPrimitive(argument, STRING). + auto offset = TRY(argument.to_primitive(vm, Value::PreferredType::String)); + + // 2. If offset is not a String, throw a TypeError exception. + if (!offset.is_string()) + return vm.throw_completion(ErrorType::TemporalInvalidTimeZoneString, offset); + + // 3. Perform ? ParseDateTimeUTCOffset(offset). + // FIXME: ParseTimeZoneOffsetString should be renamed to ParseDateTimeUTCOffset and updated for Temporal. For now, we + // can just check with the ISO8601 parser directly. + if (!parse_utc_offset(argument.as_string().utf8_string_view(), SubMinutePrecision::Yes).has_value()) + return vm.throw_completion(ErrorType::TemporalInvalidTimeZoneString, offset); + + // 4. Return offset. + return offset.as_string().utf8_string(); +} + +// 13.42 ISODateToFields ( calendar, isoDate, type ), https://tc39.es/proposal-temporal/#sec-temporal-isodatetofields +CalendarFields iso_date_to_fields(StringView calendar, ISODate const& iso_date, DateType type) +{ + // 1. Let fields be an empty Calendar Fields Record with all fields set to unset. + auto fields = CalendarFields::unset(); + + // 2. Let calendarDate be CalendarISOToDate(calendar, isoDate). + auto calendar_date = calendar_iso_to_date(calendar, iso_date); + + // 3. Set fields.[[MonthCode]] to calendarDate.[[MonthCode]]. + fields.month_code = calendar_date.month_code; + + // 4. If type is MONTH-DAY or DATE, then + if (type == DateType::MonthDay || type == DateType::Date) { + // a. Set fields.[[Day]] to calendarDate.[[Day]]. + fields.day = calendar_date.day; + } + + // 5. If type is YEAR-MONTH or DATE, then + if (type == DateType::YearMonth || type == DateType::Date) { + // a. Set fields.[[Year]] to calendarDate.[[Year]]. + fields.year = calendar_date.year; + } + + // 6. Return fields. + return fields; +} + // 14.4.1.1 GetOptionsObject ( options ), https://tc39.es/proposal-temporal/#sec-getoptionsobject ThrowCompletionOr> get_options_object(VM& vm, Value options) { @@ -992,4 +1454,19 @@ ThrowCompletionOr get_rounding_increment_option(VM& vm, Object const& optio return static_cast(integer_increment); } +// 14.5.1 GetUTCEpochNanoseconds ( isoDateTime ), https://tc39.es/proposal-temporal/#sec-getutcepochnanoseconds +Crypto::SignedBigInteger get_utc_epoch_nanoseconds(ISODateTime const& iso_date_time) +{ + return JS::get_utc_epoch_nanoseconds( + iso_date_time.iso_date.year, + iso_date_time.iso_date.month, + iso_date_time.iso_date.day, + iso_date_time.time.hour, + iso_date_time.time.minute, + iso_date_time.time.second, + iso_date_time.time.millisecond, + iso_date_time.time.microsecond, + iso_date_time.time.nanosecond); +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h index 7e4fa8ab79e..101f757206c 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h @@ -14,6 +14,8 @@ #include #include #include +#include +#include #include #include #include @@ -25,6 +27,22 @@ enum class ArithmeticOperation { Subtract, }; +enum class DateType { + Date, + MonthDay, + YearMonth, +}; + +enum class Overflow { + Constrain, + Reject, +}; + +enum class TimeStyle { + Separated, + Unseparated, +}; + // https://tc39.es/proposal-temporal/#sec-temporal-units enum class Unit { Year, @@ -91,8 +109,9 @@ using UnitValue = Variant; struct SecondsStringPrecision { struct Minute { }; + using Precision = Variant; - Variant precision; + Precision precision; Unit unit; u8 increment { 0 }; }; @@ -103,6 +122,28 @@ struct RelativeTo { GC::Ptr zoned_relative_to; // [[ZonedRelativeTo]] }; +// 13.31 ISO String Time Zone Parse Records, https://tc39.es/proposal-temporal/#sec-temporal-iso-string-time-zone-parse-records +struct ParsedISOTimeZone { + bool z_designator { false }; + Optional offset_string; + Optional time_zone_annotation; +}; + +// 13.32 ISO Date-Time Parse Records, https://tc39.es/proposal-temporal/#sec-temporal-iso-date-time-parse-records +struct ParsedISODateTime { + struct StartOfDay { }; + + Optional year { 0 }; + u8 month { 0 }; + u8 day { 0 }; + Variant time; + ParsedISOTimeZone time_zone; + Optional calendar; +}; + +double iso_date_to_epoch_days(double year, double month, double date); +double epoch_days_to_epoch_ms(double day, double time); +ThrowCompletionOr get_temporal_overflow_option(VM&, Object const& options); ThrowCompletionOr validate_temporal_rounding_increment(VM&, u64 increment, u64 dividend, bool inclusive); ThrowCompletionOr get_temporal_fractional_second_digits_option(VM&, Object const& options); SecondsStringPrecision to_seconds_string_precision_record(UnitValue, Precision); @@ -114,12 +155,19 @@ UnitCategory temporal_unit_category(Unit); RoundingIncrement maximum_temporal_duration_rounding_increment(Unit); Crypto::UnsignedBigInteger const& temporal_unit_length_in_nanoseconds(Unit); String format_fractional_seconds(u64, Precision); +String format_time_string(u8 hour, u8 minute, u8 second, u16 sub_second_nanoseconds, SecondsStringPrecision::Precision, Optional = {}); UnsignedRoundingMode get_unsigned_rounding_mode(RoundingMode, Sign); double apply_unsigned_rounding_mode(double, double r1, double r2, UnsignedRoundingMode); Crypto::SignedBigInteger apply_unsigned_rounding_mode(Crypto::SignedDivisionResult const&, Crypto::SignedBigInteger const& r1, Crypto::SignedBigInteger const& r2, UnsignedRoundingMode, Crypto::UnsignedBigInteger const& increment); double round_number_to_increment(double, u64 increment, RoundingMode); Crypto::SignedBigInteger round_number_to_increment(Crypto::SignedBigInteger const&, Crypto::UnsignedBigInteger const& increment, RoundingMode); +ThrowCompletionOr parse_iso_date_time(VM&, StringView iso_string, ReadonlySpan allowed_formats); +ThrowCompletionOr parse_temporal_calendar_string(VM&, String const&); ThrowCompletionOr> parse_temporal_duration_string(VM&, StringView iso_string); +ThrowCompletionOr parse_temporal_time_zone_string(VM& vm, StringView time_zone_string); +ThrowCompletionOr to_month_code(VM&, Value argument); +ThrowCompletionOr to_offset_string(VM&, Value argument); +CalendarFields iso_date_to_fields(StringView calendar, ISODate const&, DateType); // 13.38 ToIntegerWithTruncation ( argument ), https://tc39.es/proposal-temporal/#sec-tointegerwithtruncation template @@ -153,6 +201,21 @@ ThrowCompletionOr to_integer_with_truncation(VM& vm, StringView argument return trunc(number); } +// 13.37 ToPositiveIntegerWithTruncation ( argument ), https://tc39.es/proposal-temporal/#sec-topositiveintegerwithtruncation +template +ThrowCompletionOr to_positive_integer_with_truncation(VM& vm, Value argument, ErrorType error_type, Args&&... args) +{ + // 1. Let integer be ? ToIntegerWithTruncation(argument). + auto integer = TRY(to_integer_with_truncation(vm, argument, error_type, args...)); + + // 2. If integer ≤ 0, throw a RangeError exception. + if (integer <= 0) + return vm.throw_completion(error_type, args...); + + // 3. Return integer. + return integer; +} + // 13.39 ToIntegerIfIntegral ( argument ), https://tc39.es/proposal-temporal/#sec-tointegerifintegral template ThrowCompletionOr to_integer_if_integral(VM& vm, Value argument, ErrorType error_type, Args&&... args) @@ -168,6 +231,12 @@ ThrowCompletionOr to_integer_if_integral(VM& vm, Value argument, ErrorTy return number.as_double(); } +// 14.2 The Year-Week Record Specification Type, https://tc39.es/proposal-temporal/#sec-year-week-record-specification-type +struct YearWeek { + Optional week; + Optional year; +}; + enum class OptionType { Boolean, String, @@ -186,5 +255,6 @@ ThrowCompletionOr get_option(VM& vm, Object const& options, PropertyKey c ThrowCompletionOr get_rounding_mode_option(VM&, Object const& options, RoundingMode fallback); ThrowCompletionOr get_rounding_increment_option(VM&, Object const& options); +Crypto::SignedBigInteger get_utc_epoch_nanoseconds(ISODateTime const&); } diff --git a/Libraries/LibJS/Runtime/Temporal/Calendar.cpp b/Libraries/LibJS/Runtime/Temporal/Calendar.cpp new file mode 100644 index 00000000000..93fb7f8a96e --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/Calendar.cpp @@ -0,0 +1,586 @@ +/* + * Copyright (c) 2021, Idan Horowitz + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2023-2024, Shannon Booth + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace JS::Temporal { + +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, 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 StringView { lhs.property->as_string() } < StringView { 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(); +} + +// 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 +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., either both or neither of "islamicc" and + // "islamic-civil"). The List must include "iso8601". + return Unicode::available_calendars(); +} + +// 12.2.3 PrepareCalendarFields ( calendar, fields, calendarFieldNames, nonCalendarFieldNames, requiredFieldNames ), https://tc39.es/proposal-temporal/#sec-temporal-preparecalendarfields +ThrowCompletionOr prepare_calendar_fields(VM& vm, StringView 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. Set value to ? ToMonthCode(value). + set_field_value(key, result, TRY(to_month_code(vm, value))); + 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, then + if (required->contains_slow(key)) { + // 1. Throw a TypeError exception. + 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, then + if (required_field_names.has() && !any) { + // a. Throw a TypeError exception. + return vm.throw_completion(ErrorType::TemporalObjectMustBePartialTemporalObject); + } + + // 11. Return result. + return result; +} + +// 12.2.8 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, then + if (temporal_calendar_like.is_object()) { + auto const& temporal_calendar_object = temporal_calendar_like.as_object(); + + // a. If temporalCalendarLike has an [[InitializedTemporalDate]], [[InitializedTemporalDateTime]], + // [[InitializedTemporalMonthDay]], [[InitializedTemporalYearMonth]], or [[InitializedTemporalZonedDateTime]] + // internal slot, then + // i. Return temporalCalendarLike.[[Calendar]]. + // FIXME: Add the other calendar-holding types as we define them. + if (is(temporal_calendar_object)) + return static_cast(temporal_calendar_object).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.2.9 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]]. + // FIXME: Add the other calendar-holding types as we define them. + if (is(item)) + return static_cast(item).calendar(); + + // 2. Let calendarLike be ? Get(item, "calendar"). + auto calendar_like = TRY(item.get(vm.names.calendar)); + + // 3. If calendarLike is undefined, then + if (calendar_like.is_undefined()) { + // a. Return "iso8601". + return "iso8601"_string; + } + + // 4. Return ? ToTemporalCalendarIdentifier(calendarLike). + return TRY(to_temporal_calendar_identifier(vm, calendar_like)); +} + +// 12.2.12 CalendarMonthDayFromFields ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendarmonthdayfromfields +ThrowCompletionOr calendar_month_day_from_fields(VM& vm, StringView 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. 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.2.15 ISODaysInMonth ( year, month ), https://tc39.es/proposal-temporal/#sec-temporal-isodaysinmonth +u8 iso_days_in_month(double year, double month) +{ + // 1. If month is 1, 3, 5, 7, 8, 10, or 12, return 31. + if (month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12) + return 31; + + // 2. If month is 4, 6, 9, or 11, return 30. + if (month == 4 || month == 6 || month == 9 || month == 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.2.16 ISOWeekOfYear ( isoDate ), https://tc39.es/proposal-temporal/#sec-temporal-isoweekofyear +YearWeek iso_week_of_year(ISODate const& 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.2.17 ISODayOfYear ( isoDate ), https://tc39.es/proposal-temporal/#sec-temporal-isodayofyear +u16 iso_day_of_year(ISODate const& 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.2.18 ISODayOfWeek ( isoDate ), https://tc39.es/proposal-temporal/#sec-temporal-isodayofweek +u8 iso_day_of_week(ISODate const& 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.2.20 CalendarMonthDayToISOReferenceDate ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendarmonthdaytoisoreferencedate +ThrowCompletionOr calendar_month_day_to_iso_reference_date(VM& vm, StringView calendar, CalendarFields const& fields, Overflow overflow) +{ + // 1. If calendar is "iso8601", then + if (calendar == "iso8601"sv) { + // 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 an implementation-defined ISO Date Record, or throw a RangeError exception, as described below. + // FIXME: Create an ISODateRecord based on an ISO8601 calendar for now. See also: CalendarResolveFields. + return calendar_month_day_to_iso_reference_date(vm, "iso8601"sv, fields, overflow); +} + +// 12.2.21 CalendarISOToDate ( calendar, isoDate ), https://tc39.es/proposal-temporal/#sec-temporal-calendarisotodate +CalendarDate calendar_iso_to_date(StringView calendar, ISODate const& iso_date) +{ + // 1. If calendar is "iso8601", then + if (calendar == "iso8601"sv) { + // a. Let monthNumberPart be ToZeroPaddedDecimalString(isoDate.[[Month]], 2). + // b. Let monthCode be the string-concatenation of "M" and monthNumberPart. + auto month_code = MUST(String::formatted("M{:02}", iso_date.month)); + + // c. 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; + + // d. Return Calendar Date Record { [[Era]]: undefined, [[EraYear]]: undefined, [[Year]]: isoDate.[[Year]], + // [[Month]]: isoDate.[[Month]], [[MonthCode]]: monthCode, [[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 = move(month_code), + .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 an implementation-defined Calendar Date Record with fields as described in Table 18. + // FIXME: Return an ISO8601 calendar date for now. + return calendar_iso_to_date("iso8601"sv, iso_date); +} + +// 12.2.22 CalendarExtraFields ( calendar, fields ), https://tc39.es/proposal-temporal/#sec-temporal-calendarextrafields +Vector calendar_extra_fields(StringView calendar, CalendarFieldList) +{ + // 1. If calendar is "iso8601", return an empty List. + if (calendar == "iso8601"sv) + return {}; + + // FIXME: 2. Return an implementation-defined List as described above. + return {}; +} + +// 12.2.24 CalendarResolveFields ( calendar, fields, type ), https://tc39.es/proposal-temporal/#sec-temporal-calendarresolvefields +ThrowCompletionOr calendar_resolve_fields(VM& vm, StringView calendar, CalendarFields& fields, DateType type) +{ + // 1. If calendar is "iso8601", then + if (calendar == "iso8601"sv) { + // a. If type is DATE or YEAR-MONTH and fields.[[Year]] is UNSET, throw a TypeError exception. + if ((type == DateType::Date || type == DateType::YearMonth) && !fields.year.has_value()) + return vm.throw_completion(ErrorType::MissingRequiredProperty, "year"sv); + + // b. If type is DATE or MONTH-DAY and fields.[[Day]] is UNSET, throw a TypeError exception. + if ((type == DateType::Date || type == DateType::MonthDay) && !fields.day.has_value()) + return vm.throw_completion(ErrorType::MissingRequiredProperty, "day"sv); + + // c. Let month be fields.[[Month]]. + auto const& month = fields.month; + + // d. Let monthCode be fields.[[MonthCode]]. + auto const& month_code = fields.month_code; + + // e. If monthCode is UNSET, then + if (!month_code.has_value()) { + // i. If month is UNSET, throw a TypeError exception. + if (!month.has_value()) + return vm.throw_completion(ErrorType::MissingRequiredProperty, "month"sv); + + // ii. Return UNUSED. + return {}; + } + + // f. Assert: monthCode is a String. + VERIFY(month_code.has_value()); + + // g. NOTE: The ISO 8601 calendar does not include leap months. + // h. If the length of monthCode is not 3, throw a RangeError exception. + if (month_code->byte_count() != 3) + return vm.throw_completion(ErrorType::TemporalInvalidCalendarFieldName, "monthCode"sv); + + // i. If the first code unit of monthCode is not 0x004D (LATIN CAPITAL LETTER M), throw a RangeError exception. + if (month_code->bytes_as_string_view()[0] != 'M') + return vm.throw_completion(ErrorType::TemporalInvalidCalendarFieldName, "monthCode"sv); + + // j. Let monthCodeDigits be the substring of monthCode from 1. + auto month_code_digits = month_code->bytes_as_string_view().substring_view(1); + + // k. If ParseText(StringToCodePoints(monthCodeDigits), DateMonth) is a List of errors, throw a RangeError exception. + if (!parse_iso8601(Production::DateMonth, month_code_digits).has_value()) + return vm.throw_completion(ErrorType::TemporalInvalidCalendarFieldName, "monthCode"sv); + + // l. Let monthCodeInteger be ℝ(StringToNumber(monthCodeDigits)). + auto month_code_integer = month_code_digits.to_number().value(); + + // m. If month is not UNSET and month ≠ monthCodeInteger, throw a RangeError exception. + if (month.has_value() && month != month_code_integer) + return vm.throw_completion(ErrorType::TemporalInvalidCalendarFieldName, "month"sv); + + // n. Set fields.[[Month]] to monthCodeInteger. + fields.month = month_code_integer; + } + // 2. Else, + else { + // a. Perform implementation-defined processing to mutate fields, or throw a TypeError or RangeError exception, as described below. + // FIXME: Resolve fields as an ISO8601 calendar for now. See also: CalendarMonthDayToISOReferenceDate. + TRY(calendar_resolve_fields(vm, "iso8601"sv, fields, type)); + } + + // 3. Return UNUSED. + return {}; +} + +} diff --git a/Libraries/LibJS/Runtime/Temporal/Calendar.h b/Libraries/LibJS/Runtime/Temporal/Calendar.h new file mode 100644 index 00000000000..7289d1e3c78 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/Calendar.h @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2021, Idan Horowitz + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2023-2024, Shannon Booth + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace JS::Temporal { + +// 12.2.1 Calendar Date Records, https://tc39.es/proposal-temporal/#sec-temporal-calendar-date-records +struct CalendarDate { + Optional era; + Optional era_year; + i32 year { 0 }; + u8 month { 0 }; + String month_code; + u8 day { 0 }; + u8 day_of_week { 0 }; + u16 day_of_year { 0 }; + YearWeek week_of_year; + u8 days_in_week { 0 }; + u8 days_in_month { 0 }; + u16 days_in_year { 0 }; + u8 months_in_year { 0 }; + bool in_leap_year { false }; +}; + +// https://tc39.es/proposal-temporal/#table-temporal-calendar-fields-record-fields +enum class CalendarField { + Era, + EraYear, + Year, + Month, + MonthCode, + Day, + Hour, + Minute, + Second, + Millisecond, + Microsecond, + Nanosecond, + Offset, + TimeZone, +}; + +// https://tc39.es/proposal-temporal/#table-temporal-calendar-fields-record-fields +struct CalendarFields { + static CalendarFields unset() + { + return { + .era = {}, + .era_year = {}, + .year = {}, + .month = {}, + .month_code = {}, + .day = {}, + .hour = {}, + .minute = {}, + .second = {}, + .millisecond = {}, + .microsecond = {}, + .nanosecond = {}, + .offset = {}, + .time_zone = {}, + }; + } + + Optional era; + Optional era_year; + Optional year; + Optional month; + Optional month_code; + Optional day; + Optional hour { 0 }; + Optional minute { 0 }; + Optional second { 0 }; + Optional millisecond { 0 }; + Optional microsecond { 0 }; + Optional nanosecond { 0 }; + Optional offset; + Optional time_zone; +}; + +struct Partial { }; +using CalendarFieldList = ReadonlySpan; +using CalendarFieldListOrPartial = Variant; + +ThrowCompletionOr canonicalize_calendar(VM&, StringView id); +Vector const& available_calendars(); +ThrowCompletionOr prepare_calendar_fields(VM&, StringView calendar, Object const& fields, CalendarFieldList calendar_field_names, CalendarFieldList non_calendar_field_names, CalendarFieldListOrPartial required_field_names); +ThrowCompletionOr calendar_month_day_from_fields(VM&, StringView calendar, CalendarFields, Overflow); +u8 iso_days_in_month(double year, double month); +YearWeek iso_week_of_year(ISODate const&); +u16 iso_day_of_year(ISODate const&); +u8 iso_day_of_week(ISODate const&); +ThrowCompletionOr to_temporal_calendar_identifier(VM&, Value temporal_calendar_like); +ThrowCompletionOr get_temporal_calendar_identifier_with_iso_default(VM&, Object const& item); +ThrowCompletionOr calendar_month_day_to_iso_reference_date(VM&, StringView calendar, CalendarFields const&, Overflow); +CalendarDate calendar_iso_to_date(StringView calendar, ISODate const&); +Vector calendar_extra_fields(StringView calendar, CalendarFieldList); +ThrowCompletionOr calendar_resolve_fields(VM&, StringView calendar, CalendarFields&, DateType); + +} diff --git a/Libraries/LibJS/Runtime/Temporal/DateEquations.cpp b/Libraries/LibJS/Runtime/Temporal/DateEquations.cpp new file mode 100644 index 00000000000..fc499e183f0 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/DateEquations.cpp @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +namespace JS::Temporal { + +// https://tc39.es/proposal-temporal/#eqn-mathematicaldaysinyear +u16 mathematical_days_in_year(i32 year) +{ + // MathematicalDaysInYear(y) + // = 365 if ((y) modulo 4) ≠ 0 + // = 366 if ((y) modulo 4) = 0 and ((y) modulo 100) ≠ 0 + // = 365 if ((y) modulo 100) = 0 and ((y) modulo 400) ≠ 0 + // = 366 if ((y) modulo 400) = 0 + if (modulo(year, 4) != 0) + return 365; + if (modulo(year, 4) == 0 && modulo(year, 100) != 0) + return 366; + if (modulo(year, 100) == 0 && modulo(year, 400) != 0) + return 365; + if (modulo(year, 400) == 0) + return 366; + VERIFY_NOT_REACHED(); +} + +// https://tc39.es/proposal-temporal/#eqn-mathematicalinleapyear +u8 mathematical_in_leap_year(double time) +{ + // MathematicalInLeapYear(t) + // = 0 if MathematicalDaysInYear(EpochTimeToEpochYear(t)) = 365 + // = 1 if MathematicalDaysInYear(EpochTimeToEpochYear(t)) = 366 + auto days_in_year = mathematical_days_in_year(epoch_time_to_epoch_year(time)); + + if (days_in_year == 365) + return 0; + if (days_in_year == 366) + return 1; + VERIFY_NOT_REACHED(); +} + +// https://tc39.es/proposal-temporal/#eqn-EpochTimeToDayNumber +double epoch_time_to_day_number(double time) +{ + // EpochTimeToDayNumber(t) = floor(t / ℝ(msPerDay)) + return floor(time / JS::ms_per_day); +} + +// https://tc39.es/proposal-temporal/#eqn-epochdaynumberforyear +double epoch_day_number_for_year(double year) +{ + // EpochDayNumberForYear(y) = 365 × (y - 1970) + floor((y - 1969) / 4) - floor((y - 1901) / 100) + floor((y - 1601) / 400) + return 365.0 * (year - 1970.0) + floor((year - 1969.0) / 4.0) - floor((year - 1901.0) / 100.0) + floor((year - 1601.0) / 400.0); +} + +// https://tc39.es/proposal-temporal/#eqn-epochtimeforyear +double epoch_time_for_year(double year) +{ + // EpochTimeForYear(y) = ℝ(msPerDay) × EpochDayNumberForYear(y) + return ms_per_day * epoch_day_number_for_year(year); +} + +// https://tc39.es/proposal-temporal/#eqn-epochtimetoepochyear +i32 epoch_time_to_epoch_year(double time) +{ + // EpochTimeToEpochYear(t) = the largest integral Number y (closest to +∞) such that EpochTimeForYear(y) ≤ t + return JS::year_from_time(time); +} + +// https://tc39.es/proposal-temporal/#eqn-epochtimetodayinyear +u16 epoch_time_to_day_in_year(double time) +{ + // EpochTimeToDayInYear(t) = EpochTimeToDayNumber(t) - EpochDayNumberForYear(EpochTimeToEpochYear(t)) + return static_cast(epoch_time_to_day_number(time) - epoch_day_number_for_year(epoch_time_to_epoch_year(time))); +} + +// https://tc39.es/proposal-temporal/#eqn-epochtimetoweekday +u8 epoch_time_to_week_day(double time) +{ + // EpochTimeToWeekDay(t) = (EpochTimeToDayNumber(t) + 4) modulo 7 + return static_cast(modulo(epoch_time_to_day_number(time) + 4, 7.0)); +} + +} diff --git a/Libraries/LibJS/Runtime/Temporal/DateEquations.h b/Libraries/LibJS/Runtime/Temporal/DateEquations.h new file mode 100644 index 00000000000..0dcd6033014 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/DateEquations.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace JS::Temporal { + +// 13.3 Date Equations, https://tc39.es/proposal-temporal/#sec-date-equations + +u16 mathematical_days_in_year(i32 year); +u8 mathematical_in_leap_year(double time); +double epoch_time_to_day_number(double time); +double epoch_day_number_for_year(double year); +double epoch_time_for_year(double year); +i32 epoch_time_to_epoch_year(double time); +u16 epoch_time_to_day_in_year(double time); +u8 epoch_time_to_week_day(double time); + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp b/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp new file mode 100644 index 00000000000..330eb942b88 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2021, Idan Horowitz + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2024, Shannon Booth + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include + +namespace JS::Temporal { + +// 3.5.2 CreateISODateRecord ( year, month, day ), https://tc39.es/proposal-temporal/#sec-temporal-create-iso-date-record +ISODate create_iso_date_record(double year, double month, double day) +{ + // 1. Assert: IsValidISODate(year, month, day) is true. + VERIFY(is_valid_iso_date(year, month, day)); + + // 2. Return ISO Date Record { [[Year]]: year, [[Month]]: month, [[Day]]: day }. + return { .year = static_cast(year), .month = static_cast(month), .day = static_cast(day) }; +} + +// 3.5.6 RegulateISODate ( year, month, day, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-regulateisodate +ThrowCompletionOr regulate_iso_date(VM& vm, double year, double month, double day, Overflow overflow) +{ + switch (overflow) { + // 1. If overflow is CONSTRAIN, then + case Overflow::Constrain: + // a. Set month to the result of clamping month between 1 and 12. + month = clamp(month, 1, 12); + + // b. Let daysInMonth be ISODaysInMonth(year, month). + // c. Set day to the result of clamping day between 1 and daysInMonth. + day = clamp(day, 1, iso_days_in_month(year, month)); + + // AD-HOC: We further clamp the year to the range allowed by ISOYearMonthWithinLimits, to ensure we do not + // overflow when we store the year as an integer. + year = clamp(year, -271821, 275760); + + break; + + // 2. Else, + case Overflow::Reject: + // a. Assert: overflow is REJECT. + // b. If IsValidISODate(year, month, day) is false, throw a RangeError exception. + if (!is_valid_iso_date(year, month, day)) + return vm.throw_completion(ErrorType::TemporalInvalidISODate); + break; + } + + // 3. Return CreateISODateRecord(year, month, day). + return create_iso_date_record(year, month, day); +} + +// 3.5.7 IsValidISODate ( year, month, day ), https://tc39.es/proposal-temporal/#sec-temporal-isvalidisodate +bool is_valid_iso_date(double year, double month, double day) +{ + // AD-HOC: This is an optimization that allows us to treat these doubles as normal integers from this point onwards. + // This does not change the exposed behavior as the call to CreateISODateRecord will immediately check that + // these values are valid ISO values (years: [-271821, 275760], months: [1, 12], days: [1, 31]), all of + // which are subsets of this check. + if (!AK::is_within_range(year) || !AK::is_within_range(month) || !AK::is_within_range(day)) + return false; + + // 1. If month < 1 or month > 12, then + if (month < 1 || month > 12) { + // a. Return false. + return false; + } + + // 2. Let daysInMonth be ISODaysInMonth(year, month). + auto days_in_month = iso_days_in_month(year, month); + + // 3. If day < 1 or day > daysInMonth, then + if (day < 1 || day > days_in_month) { + // a. Return false. + return false; + } + + // 4. Return true. + return true; +} + +// 3.5.11 ISODateWithinLimits ( isoDate ), https://tc39.es/proposal-temporal/#sec-temporal-isodatewithinlimits +bool iso_date_within_limits(ISODate iso_date) +{ + // 1. Let isoDateTime be CombineISODateAndTimeRecord(isoDate, NoonTimeRecord()). + auto iso_date_time = combine_iso_date_and_time_record(iso_date, noon_time_record()); + + // 2. Return ISODateTimeWithinLimits(isoDateTime). + return iso_date_time_within_limits(iso_date_time); +} + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainDate.h b/Libraries/LibJS/Runtime/Temporal/PlainDate.h new file mode 100644 index 00000000000..67d38b33753 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainDate.h @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2021, Idan Horowitz + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2024, Shannon Booth + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace JS::Temporal { + +// 3.5.1 ISO Date Records, https://tc39.es/proposal-temporal/#sec-temporal-iso-date-records +struct ISODate { + i32 year { 0 }; + u8 month { 0 }; + u8 day { 0 }; +}; + +ISODate create_iso_date_record(double year, double month, double day); +ThrowCompletionOr regulate_iso_date(VM& vm, double year, double month, double day, Overflow overflow); +bool is_valid_iso_date(double year, double month, double day); +bool iso_date_within_limits(ISODate); + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainDateTime.cpp b/Libraries/LibJS/Runtime/Temporal/PlainDateTime.cpp new file mode 100644 index 00000000000..84c21661e02 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainDateTime.cpp @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2021, Idan Horowitz + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +namespace JS::Temporal { + +// 5.5.3 CombineISODateAndTimeRecord ( isoDate, time ), https://tc39.es/proposal-temporal/#sec-temporal-combineisodateandtimerecord +ISODateTime combine_iso_date_and_time_record(ISODate iso_date, Time time) +{ + // 1. NOTE: time.[[Days]] is ignored. + // 2. Return ISO Date-Time Record { [[ISODate]]: isoDate, [[Time]]: time }. + return { .iso_date = iso_date, .time = time }; +} + +// nsMinInstant - nsPerDay +static auto const DATETIME_NANOSECONDS_MIN = "-8640000086400000000000"_sbigint; + +// nsMaxInstant + nsPerDay +static auto const DATETIME_NANOSECONDS_MAX = "8640000086400000000000"_sbigint; + +// 5.5.4 ISODateTimeWithinLimits ( isoDateTime ), https://tc39.es/proposal-temporal/#sec-temporal-isodatetimewithinlimits +bool iso_date_time_within_limits(ISODateTime iso_date_time) +{ + // 1. If abs(ISODateToEpochDays(isoDateTime.[[ISODate]].[[Year]], isoDateTime.[[ISODate]].[[Month]] - 1, isoDateTime.[[ISODate]].[[Day]])) > 10**8 + 1, return false. + if (fabs(iso_date_to_epoch_days(iso_date_time.iso_date.year, iso_date_time.iso_date.month - 1, iso_date_time.iso_date.day)) > 100000001) + return false; + + // 2. Let ns be ℝ(GetUTCEpochNanoseconds(isoDateTime)). + auto nanoseconds = get_utc_epoch_nanoseconds(iso_date_time); + + // 3. If ns ≤ nsMinInstant - nsPerDay, then + if (nanoseconds <= DATETIME_NANOSECONDS_MIN) { + // a. Return false. + return false; + } + + // 4. If ns ≥ nsMaxInstant + nsPerDay, then + if (nanoseconds >= DATETIME_NANOSECONDS_MAX) { + // a. Return false. + return false; + } + + // 5. Return true. + return true; +} + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainDateTime.h b/Libraries/LibJS/Runtime/Temporal/PlainDateTime.h new file mode 100644 index 00000000000..31260cca3ae --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainDateTime.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2021, Idan Horowitz + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace JS::Temporal { + +// 5.5.1 ISO Date-Time Records, https://tc39.es/proposal-temporal/#sec-temporal-iso-date-time-records +struct ISODateTime { + ISODate iso_date; + Time time; +}; + +ISODateTime combine_iso_date_and_time_record(ISODate, Time); +bool iso_date_time_within_limits(ISODateTime); + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainMonthDay.cpp b/Libraries/LibJS/Runtime/Temporal/PlainMonthDay.cpp new file mode 100644 index 00000000000..e5efdc04c35 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainMonthDay.cpp @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2021, Luke Wilde + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace JS::Temporal { + +GC_DEFINE_ALLOCATOR(PlainMonthDay); + +// 10 Temporal.PlainMonthDay Objects, https://tc39.es/proposal-temporal/#sec-temporal-plainmonthday-objects +PlainMonthDay::PlainMonthDay(ISODate iso_date, String calendar, Object& prototype) + : Object(ConstructWithPrototypeTag::Tag, prototype) + , m_iso_date(iso_date) + , m_calendar(move(calendar)) +{ +} + +// 10.5.1 ToTemporalMonthDay ( item [ , options ] ), https://tc39.es/proposal-temporal/#sec-temporal-totemporalmonthday +ThrowCompletionOr> to_temporal_month_day(VM& vm, Value item, Value options) +{ + // 1. If options is not present, set options to undefined. + + // 2. If item is a Object, then + if (item.is_object()) { + auto const& object = item.as_object(); + + // a. If item has an [[InitializedTemporalMonthDay]] internal slot, then + if (is(object)) { + auto const& plain_month_day = static_cast(object); + + // i. Let resolvedOptions be ? GetOptionsObject(options). + auto resolved_options = TRY(get_options_object(vm, options)); + + // ii. Perform ? GetTemporalOverflowOption(resolvedOptions). + TRY(get_temporal_overflow_option(vm, resolved_options)); + + // iii. Return ! CreateTemporalMonthDay(item.[[ISODate]], item.[[Calendar]]). + return MUST(create_temporal_month_day(vm, plain_month_day.iso_date(), plain_month_day.calendar())); + } + + // b. Let calendar be ? GetTemporalCalendarIdentifierWithISODefault(item). + auto calendar = TRY(get_temporal_calendar_identifier_with_iso_default(vm, object)); + + // c. Let fields be ? PrepareCalendarFields(calendar, item, « YEAR, MONTH, MONTH-CODE, DAY », «», «»). + auto fields = TRY(prepare_calendar_fields(vm, calendar, object, { { CalendarField::Year, CalendarField::Month, CalendarField::MonthCode, CalendarField::Day } }, {}, CalendarFieldList {})); + + // d. Let resolvedOptions be ? GetOptionsObject(options). + auto resolved_options = TRY(get_options_object(vm, options)); + + // e. Let overflow be ? GetTemporalOverflowOption(resolvedOptions). + auto overflow = TRY(get_temporal_overflow_option(vm, resolved_options)); + + // f. Let isoDate be ? CalendarMonthDayFromFields(calendar, fields, overflow). + auto iso_date = TRY(calendar_month_day_from_fields(vm, calendar, move(fields), overflow)); + + // g. Return ! CreateTemporalMonthDay(isoDate, calendar). + return MUST(create_temporal_month_day(vm, iso_date, move(calendar))); + } + + // 3. If item is not a String, throw a TypeError exception. + if (!item.is_string()) + return vm.throw_completion(ErrorType::TemporalInvalidPlainMonthDay); + + // 4. Let result be ? ParseISODateTime(item, « TemporalMonthDayString »). + auto parse_result = TRY(parse_iso_date_time(vm, item.as_string().utf8_string_view(), { { Production::TemporalMonthDayString } })); + + // 5. Let calendar be result.[[Calendar]]. + // 6. If calendar is empty, set calendar to "iso8601". + auto calendar = parse_result.calendar.value_or("iso8601"_string); + + // 7. Set calendar to ? CanonicalizeCalendar(calendar). + calendar = TRY(canonicalize_calendar(vm, calendar)); + + // 8. Let resolvedOptions be ? GetOptionsObject(options). + auto resolved_options = TRY(get_options_object(vm, options)); + + // 9. Perform ? GetTemporalOverflowOption(resolvedOptions). + TRY(get_temporal_overflow_option(vm, resolved_options)); + + // 10. If result.[[Year]] is empty, then + if (!parse_result.year.has_value()) { + // a. Assert: calendar is "iso8601". + VERIFY(calendar == "iso8601"sv); + + // b. Let referenceISOYear be 1972 (the first ISO 8601 leap year after the epoch). + static constexpr i32 reference_iso_year = 1972; + + // c. Let isoDate be CreateISODateRecord(referenceISOYear, result.[[Month]], result.[[Day]]). + auto iso_date = create_iso_date_record(reference_iso_year, parse_result.month, parse_result.day); + + // d. Return ! CreateTemporalMonthDay(isoDate, calendar). + return MUST(create_temporal_month_day(vm, iso_date, move(calendar))); + } + + // 11. Let isoDate be CreateISODateRecord(result.[[Year]], result.[[Month]], result.[[Day]]). + auto iso_date = create_iso_date_record(*parse_result.year, parse_result.month, parse_result.day); + + // 12. Set result to ISODateToFields(calendar, isoDate, MONTH-DAY). + auto result = iso_date_to_fields(calendar, iso_date, DateType::MonthDay); + + // 13. NOTE: The following operation is called with CONSTRAIN regardless of the value of overflow, in order for the + // calendar to store a canonical value in the [[Year]] field of the [[ISODate]] internal slot of the result. + // 14. Set isoDate to ? CalendarMonthDayFromFields(calendar, result, CONSTRAIN). + iso_date = TRY(calendar_month_day_from_fields(vm, calendar, result, Overflow::Constrain)); + + // 15. Return ! CreateTemporalMonthDay(isoDate, calendar). + return MUST(create_temporal_month_day(vm, iso_date, move(calendar))); +} + +// 10.5.2 CreateTemporalMonthDay ( isoDate, calendar [ , newTarget ] ), https://tc39.es/proposal-temporal/#sec-temporal-createtemporalmonthday +ThrowCompletionOr> create_temporal_month_day(VM& vm, ISODate iso_date, String calendar, GC::Ptr new_target) +{ + auto& realm = *vm.current_realm(); + + // 1. If ISODateWithinLimits(isoDate) is false, throw a RangeError exception. + if (!iso_date_within_limits(iso_date)) + return vm.throw_completion(ErrorType::TemporalInvalidPlainMonthDay); + + // 2. If newTarget is not present, set newTarget to %Temporal.PlainMonthDay%. + if (!new_target) + new_target = realm.intrinsics().temporal_plain_month_day_constructor(); + + // 3. Let object be ? OrdinaryCreateFromConstructor(newTarget, "%Temporal.PlainMonthDay.prototype%", « [[InitializedTemporalMonthDay]], [[ISODate]], [[Calendar]] »). + // 4. Set object.[[ISODate]] to isoDate. + // 5. Set object.[[Calendar]] to calendar. + auto object = TRY(ordinary_create_from_constructor(vm, *new_target, &Intrinsics::temporal_plain_month_day_prototype, iso_date, move(calendar))); + + // 6. Return object. + return object; +} + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainMonthDay.h b/Libraries/LibJS/Runtime/Temporal/PlainMonthDay.h new file mode 100644 index 00000000000..3a6eeb6ab80 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainMonthDay.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include + +namespace JS::Temporal { + +class PlainMonthDay final : public Object { + JS_OBJECT(PlainMonthDay, Object); + GC_DECLARE_ALLOCATOR(PlainMonthDay); + +public: + virtual ~PlainMonthDay() override = default; + + [[nodiscard]] ISODate iso_date() const { return m_iso_date; } + [[nodiscard]] String const& calendar() const { return m_calendar; } + +private: + PlainMonthDay(ISODate, String calendar, Object& prototype); + + ISODate m_iso_date; // [[ISODate]] + String m_calendar; // [[Calendar]] +}; + +ThrowCompletionOr> to_temporal_month_day(VM&, Value item, Value options = js_undefined()); +ThrowCompletionOr> create_temporal_month_day(VM&, ISODate, String calendar, GC::Ptr new_target = {}); + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainMonthDayConstructor.cpp b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayConstructor.cpp new file mode 100644 index 00000000000..f5634aa4019 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayConstructor.cpp @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include + +namespace JS::Temporal { + +GC_DEFINE_ALLOCATOR(PlainMonthDayConstructor); + +// 10.1 The Temporal.PlainMonthDay Constructor, https://tc39.es/proposal-temporal/#sec-temporal-plainmonthday-constructor +PlainMonthDayConstructor::PlainMonthDayConstructor(Realm& realm) + : NativeFunction(realm.vm().names.PlainMonthDay.as_string(), realm.intrinsics().function_prototype()) +{ +} + +void PlainMonthDayConstructor::initialize(Realm& realm) +{ + Base::initialize(realm); + + auto& vm = this->vm(); + + // 10.2.1 Temporal.PlainMonthDay.prototype, https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.prototype + define_direct_property(vm.names.prototype, realm.intrinsics().temporal_plain_month_day_prototype(), 0); + + define_direct_property(vm.names.length, Value(2), Attribute::Configurable); + + u8 attr = Attribute::Writable | Attribute::Configurable; + define_native_function(realm, vm.names.from, from, 1, attr); +} + +// 10.1.1 Temporal.PlainMonthDay ( isoMonth, isoDay [ , calendar [ , referenceISOYear ] ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday +ThrowCompletionOr PlainMonthDayConstructor::call() +{ + auto& vm = this->vm(); + + // 1. If NewTarget is undefined, throw a TypeError exception. + return vm.throw_completion(ErrorType::ConstructorWithoutNew, "Temporal.PlainMonthDay"); +} + +// 10.1.1 Temporal.PlainMonthDay ( isoMonth, isoDay [ , calendar [ , referenceISOYear ] ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday +ThrowCompletionOr> PlainMonthDayConstructor::construct(FunctionObject& new_target) +{ + auto& vm = this->vm(); + + auto iso_month = vm.argument(0); + auto iso_day = vm.argument(1); + auto calendar_value = vm.argument(2); + auto reference_iso_year = vm.argument(3); + + // 2. If referenceISOYear is undefined, then + if (reference_iso_year.is_undefined()) { + // a. Set referenceISOYear to 1972𝔽 (the first ISO 8601 leap year after the epoch). + reference_iso_year = Value { 1972 }; + } + + // 3. Let m be ? ToIntegerWithTruncation(isoMonth). + auto month = TRY(to_integer_with_truncation(vm, iso_month, ErrorType::TemporalInvalidPlainMonthDay)); + + // 4. Let d be ? ToIntegerWithTruncation(isoDay). + auto day = TRY(to_integer_with_truncation(vm, iso_day, ErrorType::TemporalInvalidPlainMonthDay)); + + // 5. If calendar is undefined, set calendar to "iso8601". + if (calendar_value.is_undefined()) + calendar_value = PrimitiveString::create(vm, "iso8601"_string); + + // 6. If calendar is not a String, throw a TypeError exception. + if (!calendar_value.is_string()) + return vm.throw_completion(ErrorType::NotAString, calendar_value); + + // 7. Set calendar to ? CanonicalizeCalendar(calendar). + auto calendar = TRY(canonicalize_calendar(vm, calendar_value.as_string().utf8_string_view())); + + // 8. Let y be ? ToIntegerWithTruncation(referenceISOYear). + auto year = TRY(to_integer_with_truncation(vm, reference_iso_year, ErrorType::TemporalInvalidPlainMonthDay)); + + // 9. If IsValidISODate(y, m, d) is false, throw a RangeError exception. + if (!is_valid_iso_date(year, month, day)) + return vm.throw_completion(ErrorType::TemporalInvalidPlainMonthDay); + + // 10. Let isoDate be CreateISODateRecord(y, m, d). + auto iso_date = create_iso_date_record(year, month, day); + + // 11. Return ? CreateTemporalMonthDay(isoDate, calendar, NewTarget). + return TRY(create_temporal_month_day(vm, iso_date, move(calendar), &new_target)); +} + +// 10.2.2 Temporal.PlainMonthDay.from ( item [ , options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.from +JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayConstructor::from) +{ + // 1. Return ? ToTemporalMonthDay(item, options). + return TRY(to_temporal_month_day(vm, vm.argument(0), vm.argument(1))); +} + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainMonthDayConstructor.h b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayConstructor.h new file mode 100644 index 00000000000..03b81c0ceb2 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayConstructor.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2021-2022, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace JS::Temporal { + +class PlainMonthDayConstructor final : public NativeFunction { + JS_OBJECT(PlainMonthDayConstructor, NativeFunction); + GC_DECLARE_ALLOCATOR(PlainMonthDayConstructor); + +public: + virtual void initialize(Realm&) override; + virtual ~PlainMonthDayConstructor() override = default; + + virtual ThrowCompletionOr call() override; + virtual ThrowCompletionOr> construct(FunctionObject& new_target) override; + +private: + explicit PlainMonthDayConstructor(Realm&); + + virtual bool has_constructor() const override { return true; } + + JS_DECLARE_NATIVE_FUNCTION(from); +}; + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp new file mode 100644 index 00000000000..4987eb706af --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include + +namespace JS::Temporal { + +GC_DEFINE_ALLOCATOR(PlainMonthDayPrototype); + +// 10.3 Properties of the Temporal.PlainMonthDay Prototype Object, https://tc39.es/proposal-temporal/#sec-properties-of-the-temporal-plainmonthday-prototype-object +PlainMonthDayPrototype::PlainMonthDayPrototype(Realm& realm) + : PrototypeObject(realm.intrinsics().object_prototype()) +{ +} + +void PlainMonthDayPrototype::initialize(Realm& realm) +{ + Base::initialize(realm); + + auto& vm = this->vm(); + + // 10.3.2 Temporal.PlainMonthDay.prototype[ %Symbol.toStringTag% ], https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.prototype-%symbol.tostringtag% + define_direct_property(vm.well_known_symbol_to_string_tag(), PrimitiveString::create(vm, "Temporal.PlainMonthDay"_string), Attribute::Configurable); + + define_native_accessor(realm, vm.names.calendarId, calendar_id_getter, {}, Attribute::Configurable); + define_native_accessor(realm, vm.names.monthCode, month_code_getter, {}, Attribute::Configurable); + define_native_accessor(realm, vm.names.day, day_getter, {}, Attribute::Configurable); +} + +// 10.3.3 get Temporal.PlainMonthDay.prototype.calendarId, https://tc39.es/proposal-temporal/#sec-get-temporal.plainmonthday.prototype.calendarid +JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::calendar_id_getter) +{ + // 1. Let monthDay be the this value + // 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]). + auto month_day = TRY(typed_this_object(vm)); + + // 3. Return monthDay.[[Calendar]]. + return PrimitiveString::create(vm, month_day->calendar()); +} + +// 10.3.4 get Temporal.PlainMonthDay.prototype.monthCode, https://tc39.es/proposal-temporal/#sec-get-temporal.plainmonthday.prototype.monthcode +JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::month_code_getter) +{ + // 1. Let monthDay be the this value + // 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]). + auto month_day = TRY(typed_this_object(vm)); + + // 3. Return CalendarISOToDate(monthDay.[[Calendar]], monthDay.[[ISODate]]).[[MonthCode]]. + return PrimitiveString::create(vm, calendar_iso_to_date(month_day->calendar(), month_day->iso_date()).month_code); +} + +// 10.3.5 get Temporal.PlainMonthDay.prototype.day, https://tc39.es/proposal-temporal/#sec-get-temporal.plainmonthday.prototype.day +JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::day_getter) +{ + // 1. Let monthDay be the this value. + // 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]). + auto month_day = TRY(typed_this_object(vm)); + + // 3. Return 𝔽(CalendarISOToDate(monthDay.[[Calendar]], monthDay.[[ISODate]]).[[Day]]). + return calendar_iso_to_date(month_day->calendar(), month_day->iso_date()).day; +} + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h new file mode 100644 index 00000000000..33a00aedee9 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2021, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace JS::Temporal { + +class PlainMonthDayPrototype final : public PrototypeObject { + JS_PROTOTYPE_OBJECT(PlainMonthDayPrototype, PlainMonthDay, Temporal.PlainMonthDay); + GC_DECLARE_ALLOCATOR(PlainMonthDayPrototype); + +public: + virtual void initialize(Realm&) override; + virtual ~PlainMonthDayPrototype() override = default; + +private: + explicit PlainMonthDayPrototype(Realm&); + + JS_DECLARE_NATIVE_FUNCTION(calendar_id_getter); + JS_DECLARE_NATIVE_FUNCTION(month_code_getter); + JS_DECLARE_NATIVE_FUNCTION(day_getter); +}; + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp b/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp new file mode 100644 index 00000000000..85f6a39a3b0 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2021, Idan Horowitz + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +namespace JS::Temporal { + +// 4.5.2 CreateTimeRecord ( hour, minute, second, millisecond, microsecond, nanosecond [ , deltaDays ] ), https://tc39.es/proposal-temporal/#sec-temporal-createtimerecord +Time create_time_record(double hour, double minute, double second, double millisecond, double microsecond, double nanosecond, double delta_days) +{ + // 1. If deltaDays is not present, set deltaDays to 0. + // 2. Assert: IsValidTime(hour, minute, second, millisecond, microsecond, nanosecond). + VERIFY(is_valid_time(hour, minute, second, millisecond, microsecond, nanosecond)); + + // 3. Return Time Record { [[Days]]: deltaDays, [[Hour]]: hour, [[Minute]]: minute, [[Second]]: second, [[Millisecond]]: millisecond, [[Microsecond]]: microsecond, [[Nanosecond]]: nanosecond }. + return { + .days = delta_days, + .hour = static_cast(hour), + .minute = static_cast(minute), + .second = static_cast(second), + .millisecond = static_cast(millisecond), + .microsecond = static_cast(microsecond), + .nanosecond = static_cast(nanosecond), + }; +} + +// 4.5.4 NoonTimeRecord ( ), https://tc39.es/proposal-temporal/#sec-temporal-noontimerecord +Time noon_time_record() +{ + // 1. Return Time Record { [[Days]]: 0, [[Hour]]: 12, [[Minute]]: 0, [[Second]]: 0, [[Millisecond]]: 0, [[Microsecond]]: 0, [[Nanosecond]]: 0 }. + return { .days = 0, .hour = 12, .minute = 0, .second = 0, .millisecond = 0, .microsecond = 0, .nanosecond = 0 }; +} + +// 4.5.9 IsValidTime ( hour, minute, second, millisecond, microsecond, nanosecond ), https://tc39.es/proposal-temporal/#sec-temporal-isvalidtime +bool is_valid_time(double hour, double minute, double second, double millisecond, double microsecond, double nanosecond) +{ + // 1. If hour < 0 or hour > 23, then + if (hour < 0 || hour > 23) { + // a. Return false. + return false; + } + + // 2. If minute < 0 or minute > 59, then + if (minute < 0 || minute > 59) { + // a. Return false. + return false; + } + + // 3. If second < 0 or second > 59, then + if (second < 0 || second > 59) { + // a. Return false. + return false; + } + + // 4. If millisecond < 0 or millisecond > 999, then + if (millisecond < 0 || millisecond > 999) { + // a. Return false. + return false; + } + + // 5. If microsecond < 0 or microsecond > 999, then + if (microsecond < 0 || microsecond > 999) { + // a. Return false. + return false; + } + + // 6. If nanosecond < 0 or nanosecond > 999, then + if (nanosecond < 0 || nanosecond > 999) { + // a. Return false. + return false; + } + + // 7. Return true. + return true; +} + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainTime.h b/Libraries/LibJS/Runtime/Temporal/PlainTime.h new file mode 100644 index 00000000000..40eefc37a11 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainTime.h @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2021, Idan Horowitz + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace JS::Temporal { + +// 4.5.1 Time Records, https://tc39.es/proposal-temporal/#sec-temporal-time-records +struct Time { + double days { 0 }; + u8 hour { 0 }; + u8 minute { 0 }; + u8 second { 0 }; + u16 millisecond { 0 }; + u16 microsecond { 0 }; + u16 nanosecond { 0 }; +}; + +Time create_time_record(double hour, double minute, double second, double millisecond, double microsecond, double nanosecond, double delta_days = 0); +Time noon_time_record(); +bool is_valid_time(double hour, double minute, double second, double millisecond, double microsecond, double nanosecond); + +} diff --git a/Libraries/LibJS/Runtime/Temporal/Temporal.cpp b/Libraries/LibJS/Runtime/Temporal/Temporal.cpp index 1fa7a43ea9e..e8d067e3de5 100644 --- a/Libraries/LibJS/Runtime/Temporal/Temporal.cpp +++ b/Libraries/LibJS/Runtime/Temporal/Temporal.cpp @@ -7,6 +7,7 @@ #include #include +#include #include namespace JS::Temporal { @@ -30,6 +31,7 @@ void Temporal::initialize(Realm& realm) u8 attr = Attribute::Writable | Attribute::Configurable; define_intrinsic_accessor(vm.names.Duration, attr, [](auto& realm) -> Value { return realm.intrinsics().temporal_duration_constructor(); }); + define_intrinsic_accessor(vm.names.PlainMonthDay, attr, [](auto& realm) -> Value { return realm.intrinsics().temporal_plain_month_day_constructor(); }); } } diff --git a/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp b/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp new file mode 100644 index 00000000000..33cc50020ac --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2024, Shannon Booth + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include + +namespace JS::Temporal { + +// 11.1.5 FormatOffsetTimeZoneIdentifier ( offsetMinutes [ , style ] ), https://tc39.es/proposal-temporal/#sec-temporal-formatoffsettimezoneidentifier +String format_offset_time_zone_identifier(i64 offset_minutes, Optional style) +{ + // 1. If offsetMinutes ≥ 0, let sign be the code unit 0x002B (PLUS SIGN); otherwise, let sign be the code unit 0x002D (HYPHEN-MINUS). + auto sign = offset_minutes >= 0 ? '+' : '-'; + + // 2. Let absoluteMinutes be abs(offsetMinutes). + auto absolute_minutes = abs(offset_minutes); + + // 3. Let hour be floor(absoluteMinutes / 60). + auto hour = static_cast(floor(static_cast(absolute_minutes) / 60.0)); + + // 4. Let minute be absoluteMinutes modulo 60. + auto minute = static_cast(modulo(static_cast(absolute_minutes), 60.0)); + + // 5. Let timeString be FormatTimeString(hour, minute, 0, 0, MINUTE, style). + auto time_string = format_time_string(hour, minute, 0, 0, SecondsStringPrecision::Minute {}, style); + + // 6. Return the string-concatenation of sign and timeString. + return MUST(String::formatted("{}{}", sign, time_string)); +} + +// 11.1.8 ToTemporalTimeZoneIdentifier ( temporalTimeZoneLike ), https://tc39.es/proposal-temporal/#sec-temporal-totemporaltimezoneidentifier +ThrowCompletionOr to_temporal_time_zone_identifier(VM& vm, Value temporal_time_zone_like) +{ + // 1. If temporalTimeZoneLike is an Object, then + if (temporal_time_zone_like.is_object()) { + // FIXME: a. If temporalTimeZoneLike has an [[InitializedTemporalZonedDateTime]] internal slot, then + // FIXME: i. Return temporalTimeZoneLike.[[TimeZone]]. + } + + // 2. If temporalTimeZoneLike is not a String, throw a TypeError exception. + if (!temporal_time_zone_like.is_string()) + return vm.throw_completion(ErrorType::TemporalInvalidTimeZoneName, temporal_time_zone_like); + + // 3. Let parseResult be ? ParseTemporalTimeZoneString(temporalTimeZoneLike). + auto parse_result = TRY(parse_temporal_time_zone_string(vm, temporal_time_zone_like.as_string().utf8_string_view())); + + // 4. Let offsetMinutes be parseResult.[[OffsetMinutes]]. + // 5. If offsetMinutes is not empty, return FormatOffsetTimeZoneIdentifier(offsetMinutes). + if (parse_result.offset_minutes.has_value()) + return format_offset_time_zone_identifier(*parse_result.offset_minutes); + + // 6. Let name be parseResult.[[Name]]. + // 7. Let timeZoneIdentifierRecord be GetAvailableNamedTimeZoneIdentifier(name). + auto time_zone_identifier_record = Intl::get_available_named_time_zone_identifier(*parse_result.name); + + // 8. If timeZoneIdentifierRecord is empty, throw a RangeError exception. + if (!time_zone_identifier_record.has_value()) + return vm.throw_completion(ErrorType::TemporalInvalidTimeZoneName, temporal_time_zone_like); + + // 9. Return timeZoneIdentifierRecord.[[Identifier]]. + return time_zone_identifier_record->identifier; +} + +// 11.1.16 ParseTimeZoneIdentifier ( identifier ), https://tc39.es/proposal-temporal/#sec-parsetimezoneidentifier +ThrowCompletionOr parse_time_zone_identifier(VM& vm, StringView identifier) +{ + // 1. Let parseResult be ParseText(StringToCodePoints(identifier), TimeZoneIdentifier). + auto parse_result = parse_iso8601(Production::TimeZoneIdentifier, identifier); + + // 2. If parseResult is a List of errors, throw a RangeError exception. + if (!parse_result.has_value()) + return vm.throw_completion(ErrorType::TemporalInvalidTimeZoneString, identifier); + + return parse_time_zone_identifier(*parse_result); +} + +// 11.1.16 ParseTimeZoneIdentifier ( identifier ), https://tc39.es/proposal-temporal/#sec-parsetimezoneidentifier +TimeZone parse_time_zone_identifier(StringView identifier) +{ + // OPTIMIZATION: Some callers can assume that parsing will succeed. + + // 1. Let parseResult be ParseText(StringToCodePoints(identifier), TimeZoneIdentifier). + auto parse_result = parse_iso8601(Production::TimeZoneIdentifier, identifier); + VERIFY(parse_result.has_value()); + + return parse_time_zone_identifier(*parse_result); +} + +// 11.1.16 ParseTimeZoneIdentifier ( identifier ), https://tc39.es/proposal-temporal/#sec-parsetimezoneidentifier +TimeZone parse_time_zone_identifier(ParseResult const& parse_result) +{ + // OPTIMIZATION: Some callers will have already parsed and validated the time zone identifier. + + // 3. If parseResult contains a TimeZoneIANAName Parse Node, then + if (parse_result.time_zone_iana_name.has_value()) { + // a. Let name be the source text matched by the TimeZoneIANAName Parse Node contained within parseResult. + // b. NOTE: name is syntactically valid, but does not necessarily conform to IANA Time Zone Database naming + // guidelines or correspond with an available named time zone identifier. + // c. Return the Record { [[Name]]: CodePointsToString(name), [[OffsetMinutes]]: empty }. + return TimeZone { .name = String::from_utf8_without_validation(parse_result.time_zone_iana_name->bytes()), .offset_minutes = {} }; + } + // 4. Else, + else { + // a. Assert: parseResult contains a UTCOffset[~SubMinutePrecision] Parse Node. + VERIFY(parse_result.time_zone_offset.has_value()); + + // b. Let offsetString be the source text matched by the UTCOffset[~SubMinutePrecision] Parse Node contained within parseResult. + // c. Let offsetNanoseconds be ! ParseDateTimeUTCOffset(offsetString). + // FIXME: ParseTimeZoneOffsetString should be renamed to ParseDateTimeUTCOffset and updated for Temporal. + auto offset_nanoseconds = parse_time_zone_offset_string(parse_result.time_zone_offset->source_text); + + // d. Let offsetMinutes be offsetNanoseconds / (60 × 10**9). + auto offset_minutes = offset_nanoseconds / 60'000'000'000; + + // e. Assert: offsetMinutes is an integer. + VERIFY(trunc(offset_minutes) == offset_minutes); + + // f. Return the Record { [[Name]]: empty, [[OffsetMinutes]]: offsetMinutes }. + return TimeZone { .name = {}, .offset_minutes = static_cast(offset_minutes) }; + } +} + +} diff --git a/Libraries/LibJS/Runtime/Temporal/TimeZone.h b/Libraries/LibJS/Runtime/Temporal/TimeZone.h new file mode 100644 index 00000000000..97972ab0c88 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/TimeZone.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include + +namespace JS::Temporal { + +struct TimeZone { + Optional name; + Optional offset_minutes; +}; + +String format_offset_time_zone_identifier(i64 offset_minutes, Optional = {}); +ThrowCompletionOr to_temporal_time_zone_identifier(VM&, Value temporal_time_zone_like); +ThrowCompletionOr parse_time_zone_identifier(VM&, StringView identifier); +TimeZone parse_time_zone_identifier(StringView identifier); +TimeZone parse_time_zone_identifier(ParseResult const&); + +} diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.from.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.from.js new file mode 100644 index 00000000000..2a52adad8be --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.from.js @@ -0,0 +1,104 @@ +describe("correct behavior", () => { + test("length is 1", () => { + expect(Temporal.PlainMonthDay.from).toHaveLength(1); + }); + + test("fields object argument", () => { + const object = { + month: 7, + day: 6, + }; + const plainMonthDay = Temporal.PlainMonthDay.from(object); + expect(plainMonthDay.monthCode).toBe("M07"); + expect(plainMonthDay.day).toBe(6); + }); + + test("from month day string", () => { + const plainMonthDay = Temporal.PlainMonthDay.from("--07-06"); + expect(plainMonthDay.monthCode).toBe("M07"); + expect(plainMonthDay.day).toBe(6); + }); + + test("from date time string", () => { + const plainMonthDay = Temporal.PlainMonthDay.from("2021-07-06T23:42:01"); + expect(plainMonthDay.monthCode).toBe("M07"); + expect(plainMonthDay.day).toBe(6); + }); + + test("compares calendar name in month day string in lowercase", () => { + const values = [ + "02-10[u-ca=iso8601]", + "02-10[u-ca=isO8601]", + "02-10[u-ca=iSo8601]", + "02-10[u-ca=iSO8601]", + "02-10[u-ca=Iso8601]", + "02-10[u-ca=IsO8601]", + "02-10[u-ca=ISo8601]", + "02-10[u-ca=ISO8601]", + ]; + + for (const value of values) { + expect(() => { + Temporal.PlainMonthDay.from(value); + }).not.toThrowWithMessage( + RangeError, + "MM-DD string format can only be used with the iso8601 calendar" + ); + } + }); +}); + +describe("errors", () => { + test("missing fields", () => { + expect(() => { + Temporal.PlainMonthDay.from({}); + }).toThrowWithMessage(TypeError, "Required property day is missing or undefined"); + expect(() => { + Temporal.PlainMonthDay.from({ month: 1 }); + }).toThrowWithMessage(TypeError, "Required property day is missing or undefined"); + expect(() => { + Temporal.PlainMonthDay.from({ day: 1 }); + }).toThrowWithMessage(TypeError, "Required property month is missing or undefined"); + }); + + test("invalid month day string", () => { + expect(() => { + Temporal.PlainMonthDay.from("foo"); + }).toThrowWithMessage(RangeError, "Invalid ISO date time"); + }); + + test("string must not contain a UTC designator", () => { + expect(() => { + Temporal.PlainMonthDay.from("2021-07-06T23:42:01Z"); + }).toThrowWithMessage(RangeError, "Invalid ISO date time"); + }); + + test("extended year must not be negative zero", () => { + expect(() => { + Temporal.PlainMonthDay.from("-000000-01-01"); + }).toThrowWithMessage(RangeError, "Invalid ISO date time"); + }); + + test("can only use iso8601 calendar with month day strings", () => { + expect(() => { + Temporal.PlainMonthDay.from("02-10[u-ca=iso8602]"); + }).toThrowWithMessage(RangeError, "Invalid calendar identifier 'iso8602'"); + + expect(() => { + Temporal.PlainMonthDay.from("02-10[u-ca=ladybird]"); + }).toThrowWithMessage(RangeError, "Invalid calendar identifier 'ladybird'"); + }); + + test("doesn't throw non-iso8601 calendar error when using a superset format string such as DateTime", () => { + // NOTE: This will still throw, but only because "ladybird" is not a recognised calendar, not because of the string format restriction. + try { + Temporal.PlainMonthDay.from("2023-02-10T22:56[u-ca=ladybird]"); + } catch (e) { + expect(e).toBeInstanceOf(RangeError); + expect(e.message).not.toBe( + "MM-DD string format can only be used with the iso8601 calendar" + ); + expect(e.message).toBe("Invalid calendar identifier 'ladybird'"); + } + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.js new file mode 100644 index 00000000000..e7216ee163b --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.js @@ -0,0 +1,66 @@ +describe("errors", () => { + test("called without new", () => { + expect(() => { + Temporal.PlainMonthDay(); + }).toThrowWithMessage( + TypeError, + "Temporal.PlainMonthDay constructor must be called with 'new'" + ); + }); + + test("cannot pass Infinity", () => { + expect(() => { + new Temporal.PlainMonthDay(Infinity); + }).toThrowWithMessage(RangeError, "Invalid plain month day"); + expect(() => { + new Temporal.PlainMonthDay(1, Infinity); + }).toThrowWithMessage(RangeError, "Invalid plain month day"); + expect(() => { + new Temporal.PlainMonthDay(1, 1, undefined, Infinity); + }).toThrowWithMessage(RangeError, "Invalid plain month day"); + expect(() => { + new Temporal.PlainMonthDay(-Infinity); + }).toThrowWithMessage(RangeError, "Invalid plain month day"); + expect(() => { + new Temporal.PlainMonthDay(1, -Infinity); + }).toThrowWithMessage(RangeError, "Invalid plain month day"); + expect(() => { + new Temporal.PlainMonthDay(1, 1, undefined, -Infinity); + }).toThrowWithMessage(RangeError, "Invalid plain month day"); + }); + + test("cannot pass invalid ISO month/day", () => { + expect(() => { + new Temporal.PlainMonthDay(0, 1); + }).toThrowWithMessage(RangeError, "Invalid plain month day"); + expect(() => { + new Temporal.PlainMonthDay(1, 0); + }).toThrowWithMessage(RangeError, "Invalid plain month day"); + }); + + test("not within iso date time limit", () => { + expect(() => { + new Temporal.PlainMonthDay(9, 30, "iso8601", 999_999_999_999_999); + }).toThrowWithMessage(RangeError, "Invalid plain month day"); + }); +}); + +describe("normal behavior", () => { + test("length is 2", () => { + expect(Temporal.PlainMonthDay).toHaveLength(2); + }); + + test("basic functionality", () => { + const plainMonthDay = new Temporal.PlainMonthDay(7, 6); + expect(typeof plainMonthDay).toBe("object"); + expect(plainMonthDay).toBeInstanceOf(Temporal.PlainMonthDay); + expect(Object.getPrototypeOf(plainMonthDay)).toBe(Temporal.PlainMonthDay.prototype); + }); + + // FIXME: Re-implement this test with Temporal.PlainMonthDay.prototype.toString({ calendarName: "always" }). + // test("default reference year is 1972", () => { + // const plainMonthDay = new Temporal.PlainMonthDay(7, 6); + // const fields = plainMonthDay.getISOFields(); + // expect(fields.isoYear).toBe(1972); + // }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.calendarId.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.calendarId.js new file mode 100644 index 00000000000..8f9177df267 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.calendarId.js @@ -0,0 +1,14 @@ +describe("correct behavior", () => { + test("calendarId basic functionality", () => { + const plainMonthDay = new Temporal.PlainMonthDay(5, 1, "iso8601"); + expect(plainMonthDay.calendarId).toBe("iso8601"); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainMonthDay object", () => { + expect(() => { + Reflect.get(Temporal.PlainMonthDay.prototype, "calendarId", "foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainMonthDay"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.day.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.day.js new file mode 100644 index 00000000000..e9807f1286f --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.day.js @@ -0,0 +1,14 @@ +describe("correct behavior", () => { + test("basic functionality", () => { + const plainMonthDay = new Temporal.PlainMonthDay(7, 6); + expect(plainMonthDay.day).toBe(6); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainMonthDay object", () => { + expect(() => { + Reflect.get(Temporal.PlainMonthDay.prototype, "day", "foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainMonthDay"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.monthCode.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.monthCode.js new file mode 100644 index 00000000000..2943794e495 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.monthCode.js @@ -0,0 +1,14 @@ +describe("correct behavior", () => { + test("basic functionality", () => { + const plainMonthDay = new Temporal.PlainMonthDay(7, 6); + expect(plainMonthDay.monthCode).toBe("M07"); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainMonthDay object", () => { + expect(() => { + Reflect.get(Temporal.PlainMonthDay.prototype, "monthCode", "foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainMonthDay"); + }); +});