From 5fe0d3352dabcb405456f53715a5e64e59f5ecec Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Mon, 18 Nov 2024 11:58:51 -0500 Subject: [PATCH] LibJS: Implement the Temporal.Duration constructor This also includes a stubbed Temporal.Duration.prototype. Until we have re-implemented Temporal.PlainDate/ZonedDateTime, some of Temporal.Duration.compare (and its invoked AOs) are left unimplemented. --- Libraries/LibJS/CMakeLists.txt | 3 + Libraries/LibJS/Forward.h | 3 +- Libraries/LibJS/Print.cpp | 10 + Libraries/LibJS/Runtime/ErrorTypes.h | 2 +- Libraries/LibJS/Runtime/Intrinsics.cpp | 2 + .../Runtime/Temporal/AbstractOperations.cpp | 289 +++++++++++ .../Runtime/Temporal/AbstractOperations.h | 75 +++ Libraries/LibJS/Runtime/Temporal/Duration.cpp | 453 ++++++++++++++++- Libraries/LibJS/Runtime/Temporal/Duration.h | 104 +++- .../Runtime/Temporal/DurationConstructor.cpp | 194 ++++++++ .../Runtime/Temporal/DurationConstructor.h | 34 ++ .../Runtime/Temporal/DurationPrototype.cpp | 59 +++ .../Runtime/Temporal/DurationPrototype.h | 32 ++ Libraries/LibJS/Runtime/Temporal/ISO8601.cpp | 465 ++++++++++++++++++ Libraries/LibJS/Runtime/Temporal/ISO8601.h | 35 ++ Libraries/LibJS/Runtime/Temporal/Temporal.cpp | 4 + .../Temporal/Duration/Duration.compare.js | 95 ++++ .../Temporal/Duration/Duration.from.js | 132 +++++ .../builtins/Temporal/Duration/Duration.js | 35 ++ .../Duration.prototype.@@toStringTag.js | 3 + .../Duration/Duration.prototype.days.js | 14 + .../Duration/Duration.prototype.hours.js | 14 + .../Duration.prototype.microseconds.js | 14 + .../Duration.prototype.milliseconds.js | 14 + .../Duration/Duration.prototype.minutes.js | 14 + .../Duration/Duration.prototype.months.js | 14 + .../Duration.prototype.nanoseconds.js | 14 + .../Duration/Duration.prototype.seconds.js | 14 + .../Duration/Duration.prototype.weeks.js | 14 + .../Duration/Duration.prototype.years.js | 14 + 30 files changed, 2143 insertions(+), 26 deletions(-) create mode 100644 Libraries/LibJS/Runtime/Temporal/DurationConstructor.cpp create mode 100644 Libraries/LibJS/Runtime/Temporal/DurationConstructor.h create mode 100644 Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp create mode 100644 Libraries/LibJS/Runtime/Temporal/DurationPrototype.h create mode 100644 Libraries/LibJS/Runtime/Temporal/ISO8601.cpp create mode 100644 Libraries/LibJS/Runtime/Temporal/ISO8601.h create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.compare.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.from.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.@@toStringTag.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.days.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.hours.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.microseconds.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.milliseconds.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.minutes.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.months.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.nanoseconds.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.seconds.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.weeks.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.years.js diff --git a/Libraries/LibJS/CMakeLists.txt b/Libraries/LibJS/CMakeLists.txt index 8e51d0a2697..ae7f6450493 100644 --- a/Libraries/LibJS/CMakeLists.txt +++ b/Libraries/LibJS/CMakeLists.txt @@ -206,7 +206,10 @@ set(SOURCES Runtime/SymbolPrototype.cpp Runtime/Temporal/AbstractOperations.cpp Runtime/Temporal/Duration.cpp + Runtime/Temporal/DurationConstructor.cpp + Runtime/Temporal/DurationPrototype.cpp Runtime/Temporal/Instant.cpp + Runtime/Temporal/ISO8601.cpp Runtime/Temporal/Temporal.cpp Runtime/TypedArray.cpp Runtime/TypedArrayConstructor.cpp diff --git a/Libraries/LibJS/Forward.h b/Libraries/LibJS/Forward.h index 606604121f7..a09c4e16cd6 100644 --- a/Libraries/LibJS/Forward.h +++ b/Libraries/LibJS/Forward.h @@ -87,7 +87,8 @@ __JS_ENUMERATE(RelativeTimeFormat, relative_time_format, RelativeTimeFormatPrototype, RelativeTimeFormatConstructor) \ __JS_ENUMERATE(Segmenter, segmenter, SegmenterPrototype, SegmenterConstructor) -#define JS_ENUMERATE_TEMPORAL_OBJECTS +#define JS_ENUMERATE_TEMPORAL_OBJECTS \ + __JS_ENUMERATE(Duration, duration, DurationPrototype, DurationConstructor) #define JS_ENUMERATE_BUILTIN_NAMESPACE_OBJECTS \ __JS_ENUMERATE(AtomicsObject, atomics) \ diff --git a/Libraries/LibJS/Print.cpp b/Libraries/LibJS/Print.cpp index 97e0611adb4..bfda483c4d9 100644 --- a/Libraries/LibJS/Print.cpp +++ b/Libraries/LibJS/Print.cpp @@ -47,6 +47,7 @@ #include #include #include +#include #include #include #include @@ -827,6 +828,13 @@ ErrorOr print_intl_duration_format(JS::PrintContext& print_context, JS::In return {}; } +ErrorOr print_temporal_duration(JS::PrintContext& print_context, JS::Temporal::Duration const& duration, HashTable&) +{ + TRY(print_type(print_context, "Temporal.Duration"sv)); + TRY(js_out(print_context, " \033[34;1m{} y, {} M, {} w, {} d, {} h, {} m, {} s, {} ms, {} us, {} ns\033[0m", duration.years(), duration.months(), duration.weeks(), duration.days(), duration.hours(), duration.minutes(), duration.seconds(), duration.milliseconds(), duration.microseconds(), duration.nanoseconds())); + return {}; +} + ErrorOr print_boolean_object(JS::PrintContext& print_context, JS::BooleanObject const& boolean_object, HashTable& seen_objects) { TRY(print_type(print_context, "Boolean"sv)); @@ -942,6 +950,8 @@ ErrorOr print_value(JS::PrintContext& print_context, JS::Value value, Hash return print_intl_segments(print_context, static_cast(object), seen_objects); if (is(object)) 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); return print_object(print_context, object, seen_objects); } diff --git a/Libraries/LibJS/Runtime/ErrorTypes.h b/Libraries/LibJS/Runtime/ErrorTypes.h index aaf76f2303b..6d11555662f 100644 --- a/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Libraries/LibJS/Runtime/ErrorTypes.h @@ -286,7 +286,7 @@ M(TemporalOnlyISO8601WithMonthDayString, "MM-DD string format can only be used with the iso8601 calendar") \ M(TemporalOnlyISO8601WithYearMonthString, "YYYY-MM string format can only be used with the iso8601 calendar") \ M(TemporalMissingOptionsObject, "Required options object is missing or undefined") \ - M(TemporalMissingStartingPoint, "A starting point is required for balancing {}") \ + M(TemporalMissingStartingPoint, "A starting point is required for comparing {}") \ M(TemporalMissingUnits, "One or both of smallestUnit or largestUnit is required") \ M(TemporalNanosecondsConvertedToDaysWithOppositeSign, "Time zone or calendar converted nanoseconds into a number of days with " \ "the opposite sign") \ diff --git a/Libraries/LibJS/Runtime/Intrinsics.cpp b/Libraries/LibJS/Runtime/Intrinsics.cpp index 53b44fdc6ba..9d929d3f494 100644 --- a/Libraries/LibJS/Runtime/Intrinsics.cpp +++ b/Libraries/LibJS/Runtime/Intrinsics.cpp @@ -99,6 +99,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 5ceb5e158af..029b0eaf4f3 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp @@ -7,11 +7,300 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include +#include +#include namespace JS::Temporal { +// https://tc39.es/proposal-temporal/#table-temporal-units +struct TemporalUnit { + Unit value; + StringView singular_property_name; + StringView plural_property_name; + UnitCategory category; + RoundingIncrement maximum_duration_rounding_increment; +}; +static auto temporal_units = to_array({ + { Unit::Year, "year"sv, "years"sv, UnitCategory::Date, Unset {} }, + { Unit::Month, "month"sv, "months"sv, UnitCategory::Date, Unset {} }, + { Unit::Week, "week"sv, "weeks"sv, UnitCategory::Date, Unset {} }, + { Unit::Day, "day"sv, "days"sv, UnitCategory::Date, Unset {} }, + { Unit::Hour, "hour"sv, "hours"sv, UnitCategory::Time, 24 }, + { Unit::Minute, "minute"sv, "minutes"sv, UnitCategory::Time, 60 }, + { Unit::Second, "second"sv, "seconds"sv, UnitCategory::Time, 60 }, + { Unit::Millisecond, "millisecond"sv, "milliseconds"sv, UnitCategory::Time, 1000 }, + { Unit::Microsecond, "microsecond"sv, "microseconds"sv, UnitCategory::Time, 1000 }, + { Unit::Nanosecond, "nanosecond"sv, "nanoseconds"sv, UnitCategory::Time, 1000 }, +}); + +StringView temporal_unit_to_string(Unit unit) +{ + return temporal_units[to_underlying(unit)].singular_property_name; +} + +// 13.18 GetTemporalRelativeToOption ( options ), https://tc39.es/proposal-temporal/#sec-temporal-gettemporalrelativetooption +ThrowCompletionOr get_temporal_relative_to_option(VM& vm, Object const& options) +{ + // 1. Let value be ? Get(options, "relativeTo"). + auto value = TRY(options.get(vm.names.relativeTo)); + + // 2. If value is undefined, return the Record { [[PlainRelativeTo]]: undefined, [[ZonedRelativeTo]]: undefined }. + if (value.is_undefined()) + return RelativeTo { .plain_relative_to = {}, .zoned_relative_to = {} }; + + // FIXME: Implement the remaining steps of this AO when we have implemented PlainRelativeTo and ZonedRelativeTo. + return RelativeTo { .plain_relative_to = {}, .zoned_relative_to = {} }; +} + +// 13.20 IsCalendarUnit ( unit ), https://tc39.es/proposal-temporal/#sec-temporal-iscalendarunit +bool is_calendar_unit(Unit unit) +{ + // 1. If unit is year, return true. + if (unit == Unit::Year) + return true; + + // 2. If unit is month, return true. + if (unit == Unit::Month) + return true; + + // 3. If unit is week, return true. + if (unit == Unit::Week) + return true; + + // 4. Return false. + return false; +} + +// 13.21 TemporalUnitCategory ( unit ), https://tc39.es/proposal-temporal/#sec-temporal-temporalunitcategory +UnitCategory temporal_unit_category(Unit unit) +{ + // 1. Return the value from the "Category" column of the row of Table 21 in which unit is in the "Value" column. + return temporal_units[to_underlying(unit)].category; +} + +// 13.35 ParseTemporalDurationString ( isoString ), https://tc39.es/proposal-temporal/#sec-temporal-parsetemporaldurationstring +ThrowCompletionOr> parse_temporal_duration_string(VM& vm, StringView iso_string) +{ + // 1. Let duration be ParseText(StringToCodePoints(isoString), TemporalDurationString). + auto parse_result = parse_iso8601(Production::TemporalDurationString, iso_string); + + // 2. If duration is a List of errors, throw a RangeError exception. + if (!parse_result.has_value()) + return vm.throw_completion(ErrorType::TemporalInvalidDurationString, iso_string); + + // 3. Let sign be the source text matched by the ASCIISign Parse Node contained within duration, or an empty sequence + // of code points if not present. + auto sign = parse_result->sign; + + // 4. If duration contains a DurationYearsPart Parse Node, then + // a. Let yearsNode be that DurationYearsPart Parse Node contained within duration. + // b. Let years be the source text matched by the DecimalDigits Parse Node contained within yearsNode. + // 5. Else, + // a. Let years be an empty sequence of code points. + auto years = parse_result->duration_years.value_or({}); + + // 6. If duration contains a DurationMonthsPart Parse Node, then + // a. Let monthsNode be the DurationMonthsPart Parse Node contained within duration. + // b. Let months be the source text matched by the DecimalDigits Parse Node contained within monthsNode. + // 7. Else, + // a. Let months be an empty sequence of code points. + auto months = parse_result->duration_months.value_or({}); + + // 8. If duration contains a DurationWeeksPart Parse Node, then + // a. Let weeksNode be the DurationWeeksPart Parse Node contained within duration. + // b. Let weeks be the source text matched by the DecimalDigits Parse Node contained within weeksNode. + // 9. Else, + // a. Let weeks be an empty sequence of code points. + auto weeks = parse_result->duration_weeks.value_or({}); + + // 10. If duration contains a DurationDaysPart Parse Node, then + // a. Let daysNode be the DurationDaysPart Parse Node contained within duration. + // b. Let days be the source text matched by the DecimalDigits Parse Node contained within daysNode. + // 11. Else, + // a. Let days be an empty sequence of code points. + auto days = parse_result->duration_days.value_or({}); + + // 12. If duration contains a DurationHoursPart Parse Node, then + // a. Let hoursNode be the DurationHoursPart Parse Node contained within duration. + // b. Let hours be the source text matched by the DecimalDigits Parse Node contained within hoursNode. + // c. Let fHours be the source text matched by the TemporalDecimalFraction Parse Node contained within + // hoursNode, or an empty sequence of code points if not present. + // 13. Else, + // a. Let hours be an empty sequence of code points. + // b. Let fHours be an empty sequence of code points. + auto hours = parse_result->duration_hours.value_or({}); + auto fractional_hours = parse_result->duration_hours_fraction.value_or({}); + + // 14. If duration contains a DurationMinutesPart Parse Node, then + // a. Let minutesNode be the DurationMinutesPart Parse Node contained within duration. + // b. Let minutes be the source text matched by the DecimalDigits Parse Node contained within minutesNode. + // c. Let fMinutes be the source text matched by the TemporalDecimalFraction Parse Node contained within + // minutesNode, or an empty sequence of code points if not present. + // 15. Else, + // a. Let minutes be an empty sequence of code points. + // b. Let fMinutes be an empty sequence of code points. + auto minutes = parse_result->duration_minutes.value_or({}); + auto fractional_minutes = parse_result->duration_minutes_fraction.value_or({}); + + // 16. If duration contains a DurationSecondsPart Parse Node, then + // a. Let secondsNode be the DurationSecondsPart Parse Node contained within duration. + // b. Let seconds be the source text matched by the DecimalDigits Parse Node contained within secondsNode. + // c. Let fSeconds be the source text matched by the TemporalDecimalFraction Parse Node contained within + // secondsNode, or an empty sequence of code points if not present. + // 17. Else, + // a. Let seconds be an empty sequence of code points. + // b. Let fSeconds be an empty sequence of code points. + auto seconds = parse_result->duration_seconds.value_or({}); + auto fractional_seconds = parse_result->duration_seconds_fraction.value_or({}); + + // 18. Let yearsMV be ? ToIntegerWithTruncation(CodePointsToString(years)). + auto years_value = TRY(to_integer_with_truncation(vm, years, ErrorType::TemporalInvalidDurationString, iso_string)); + + // 19. Let monthsMV be ? ToIntegerWithTruncation(CodePointsToString(months)). + auto months_value = TRY(to_integer_with_truncation(vm, months, ErrorType::TemporalInvalidDurationString, iso_string)); + + // 20. Let weeksMV be ? ToIntegerWithTruncation(CodePointsToString(weeks)). + auto weeks_value = TRY(to_integer_with_truncation(vm, weeks, ErrorType::TemporalInvalidDurationString, iso_string)); + + // 21. Let daysMV be ? ToIntegerWithTruncation(CodePointsToString(days)). + auto days_value = TRY(to_integer_with_truncation(vm, days, ErrorType::TemporalInvalidDurationString, iso_string)); + + // 22. Let hoursMV be ? ToIntegerWithTruncation(CodePointsToString(hours)). + auto hours_value = TRY(to_integer_with_truncation(vm, hours, ErrorType::TemporalInvalidDurationString, iso_string)); + + Crypto::BigFraction minutes_value; + Crypto::BigFraction seconds_value; + Crypto::BigFraction milliseconds_value; + + auto remainder_one = [](Crypto::BigFraction const& value) { + // FIXME: We should add a generic remainder() method to BigFraction, or a method equivalent to modf(). But for + // now, since we know we are only dividing by powers of 10, we can implement a very situationally specific + // method to extract the fractional part of the BigFraction. + auto res = value.numerator().divided_by(value.denominator()); + return Crypto::BigFraction { move(res.remainder), value.denominator() }; + }; + + // 23. If fHours is not empty, then + if (!fractional_hours.is_empty()) { + // a. Assert: minutes, fMinutes, seconds, and fSeconds are empty. + VERIFY(minutes.is_empty()); + VERIFY(fractional_minutes.is_empty()); + VERIFY(seconds.is_empty()); + VERIFY(fractional_seconds.is_empty()); + + // b. Let fHoursDigits be the substring of CodePointsToString(fHours) from 1. + auto fractional_hours_digits = fractional_hours.substring_view(1); + + // c. Let fHoursScale be the length of fHoursDigits. + auto fractional_hours_scale = fractional_hours_digits.length(); + + // d. Let minutesMV be ? ToIntegerWithTruncation(fHoursDigits) / 10**fHoursScale × 60. + auto minutes_integer = TRY(to_integer_with_truncation(vm, fractional_hours_digits, ErrorType::TemporalInvalidDurationString, iso_string)); + minutes_value = Crypto::BigFraction { minutes_integer } / Crypto::BigFraction { pow(10.0, fractional_hours_scale) } * Crypto::BigFraction { 60.0 }; + } + // 24. Else, + else { + // a. Let minutesMV be ? ToIntegerWithTruncation(CodePointsToString(minutes)). + auto minutes_integer = TRY(to_integer_with_truncation(vm, minutes, ErrorType::TemporalInvalidDurationString, iso_string)); + minutes_value = Crypto::BigFraction { minutes_integer }; + } + + // 25. If fMinutes is not empty, then + if (!fractional_minutes.is_empty()) { + // a. Assert: seconds and fSeconds are empty. + VERIFY(seconds.is_empty()); + VERIFY(fractional_seconds.is_empty()); + + // b. Let fMinutesDigits be the substring of CodePointsToString(fMinutes) from 1. + auto fractional_minutes_digits = fractional_minutes.substring_view(1); + + // c. Let fMinutesScale be the length of fMinutesDigits. + auto fractional_minutes_scale = fractional_minutes_digits.length(); + + // d. Let secondsMV be ? ToIntegerWithTruncation(fMinutesDigits) / 10**fMinutesScale × 60. + auto seconds_integer = TRY(to_integer_with_truncation(vm, fractional_minutes_digits, ErrorType::TemporalInvalidDurationString, iso_string)); + seconds_value = Crypto::BigFraction { seconds_integer } / Crypto::BigFraction { pow(10.0, fractional_minutes_scale) } * Crypto::BigFraction { 60.0 }; + } + // 26. Else if seconds is not empty, then + else if (!seconds.is_empty()) { + // a. Let secondsMV be ? ToIntegerWithTruncation(CodePointsToString(seconds)). + auto seconds_integer = TRY(to_integer_with_truncation(vm, seconds, ErrorType::TemporalInvalidDurationString, iso_string)); + seconds_value = Crypto::BigFraction { seconds_integer }; + } + // 27. Else, + else { + // a. Let secondsMV be remainder(minutesMV, 1) × 60. + seconds_value = remainder_one(minutes_value) * Crypto::BigFraction { 60.0 }; + } + + // 28. 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 fSecondsScale be the length of fSecondsDigits. + auto fractional_seconds_scale = fractional_seconds_digits.length(); + + // c. Let millisecondsMV be ? ToIntegerWithTruncation(fSecondsDigits) / 10**fSecondsScale × 1000. + auto milliseconds_integer = TRY(to_integer_with_truncation(vm, fractional_seconds_digits, ErrorType::TemporalInvalidDurationString, iso_string)); + milliseconds_value = Crypto::BigFraction { milliseconds_integer } / Crypto::BigFraction { pow(10.0, fractional_seconds_scale) } * Crypto::BigFraction { 1000.0 }; + + } + // 29. Else, + else { + // a. Let millisecondsMV be remainder(secondsMV, 1) × 1000. + milliseconds_value = remainder_one(seconds_value) * Crypto::BigFraction { 1000.0 }; + } + + // 30. Let microsecondsMV be remainder(millisecondsMV, 1) × 1000. + auto microseconds_value = remainder_one(milliseconds_value) * Crypto::BigFraction { 1000.0 }; + + // 31. Let nanosecondsMV be remainder(microsecondsMV, 1) × 1000. + auto nanoseconds_value = remainder_one(microseconds_value) * Crypto::BigFraction { 1000.0 }; + + // 32. If sign contains the code point U+002D (HYPHEN-MINUS), then + // a. Let factor be -1. + // 33. Else, + // a. Let factor be 1. + i8 factor = sign == '-' ? -1 : 1; + + // 34. Set yearsMV to yearsMV × factor. + years_value *= factor; + + // 35. Set monthsMV to monthsMV × factor. + months_value *= factor; + + // 36. Set weeksMV to weeksMV × factor. + weeks_value *= factor; + + // 37. Set daysMV to daysMV × factor. + days_value *= factor; + + // 38. Set hoursMV to hoursMV × factor. + hours_value *= factor; + + // 39. Set minutesMV to floor(minutesMV) × factor. + auto factored_minutes_value = floor(minutes_value.to_double()) * factor; + + // 40. Set secondsMV to floor(secondsMV) × factor. + auto factored_seconds_value = floor(seconds_value.to_double()) * factor; + + // 41. Set millisecondsMV to floor(millisecondsMV) × factor. + auto factored_milliseconds_value = floor(milliseconds_value.to_double()) * factor; + + // 42. Set microsecondsMV to floor(microsecondsMV) × factor. + auto factored_microseconds_value = floor(microseconds_value.to_double()) * factor; + + // 43. Set nanosecondsMV to floor(nanosecondsMV) × factor. + auto factored_nanoseconds_value = floor(nanoseconds_value.to_double()) * factor; + + // 44. Return ? CreateTemporalDuration(yearsMV, monthsMV, weeksMV, daysMV, hoursMV, minutesMV, secondsMV, millisecondsMV, microsecondsMV, nanosecondsMV). + 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)); +} + // 14.4.1.1 GetOptionsObject ( options ), https://tc39.es/proposal-temporal/#sec-getoptionsobject ThrowCompletionOr> get_options_object(VM& vm, Value options) { diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h index 38c40c9995f..97bf5723c47 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h @@ -14,9 +14,84 @@ #include #include #include +#include namespace JS::Temporal { +// https://tc39.es/proposal-temporal/#sec-temporal-units +enum class Unit { + Year, + Month, + Week, + Day, + Hour, + Minute, + Second, + Millisecond, + Microsecond, + Nanosecond, +}; +StringView temporal_unit_to_string(Unit); + +// https://tc39.es/proposal-temporal/#sec-temporal-units +enum class UnitCategory { + Date, + Time, +}; + +// https://tc39.es/proposal-temporal/#sec-temporal-units +enum class UnitGroup { + Date, + Time, + DateTime, +}; + +struct Unset { }; +using RoundingIncrement = Variant; + +struct RelativeTo { + // FIXME: Make these objects represent their actual types when we re-implement them. + GC::Ptr plain_relative_to; // [[PlainRelativeTo]] + GC::Ptr zoned_relative_to; // [[ZonedRelativeTo]] +}; + +ThrowCompletionOr get_temporal_relative_to_option(VM&, Object const& options); +bool is_calendar_unit(Unit); +UnitCategory temporal_unit_category(Unit); +ThrowCompletionOr> parse_temporal_duration_string(VM&, StringView iso_string); + +// 13.38 ToIntegerWithTruncation ( argument ), https://tc39.es/proposal-temporal/#sec-tointegerwithtruncation +template +ThrowCompletionOr to_integer_with_truncation(VM& vm, Value argument, ErrorType error_type, Args&&... args) +{ + // 1. Let number be ? ToNumber(argument). + auto number = TRY(argument.to_number(vm)); + + // 2. If number is NaN, +∞𝔽 or -∞𝔽, throw a RangeError exception. + if (number.is_nan() || number.is_infinity()) + return vm.throw_completion(error_type, forward(args)...); + + // 3. Return truncate(ℝ(number)). + return trunc(number.as_double()); +} + +// 13.38 ToIntegerWithTruncation ( argument ), https://tc39.es/proposal-temporal/#sec-tointegerwithtruncation +// AD-HOC: We often need to use this AO when we have a parsed StringView. This overload allows callers to avoid creating +// a PrimitiveString for the primary definition. +template +ThrowCompletionOr to_integer_with_truncation(VM& vm, StringView argument, ErrorType error_type, Args&&... args) +{ + // 1. Let number be ? ToNumber(argument). + auto number = string_to_number(argument); + + // 2. If number is NaN, +∞𝔽 or -∞𝔽, throw a RangeError exception. + if (isnan(number) || isinf(number)) + return vm.throw_completion(error_type, forward(args)...); + + // 3. Return truncate(ℝ(number)). + return trunc(number); +} + // 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) diff --git a/Libraries/LibJS/Runtime/Temporal/Duration.cpp b/Libraries/LibJS/Runtime/Temporal/Duration.cpp index 26273329f4f..c58a9c2999c 100644 --- a/Libraries/LibJS/Runtime/Temporal/Duration.cpp +++ b/Libraries/LibJS/Runtime/Temporal/Duration.cpp @@ -9,13 +9,220 @@ #include #include -#include +#include +#include +#include #include -#include +#include +#include +#include +#include #include namespace JS::Temporal { +GC_DEFINE_ALLOCATOR(Duration); + +// 7 Temporal.Duration Objects, https://tc39.es/proposal-temporal/#sec-temporal-duration-objects +Duration::Duration(double years, double months, double weeks, double days, double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds, Object& prototype) + : Object(ConstructWithPrototypeTag::Tag, prototype) + , m_years(years) + , m_months(months) + , m_weeks(weeks) + , m_days(days) + , m_hours(hours) + , m_minutes(minutes) + , m_seconds(seconds) + , m_milliseconds(milliseconds) + , m_microseconds(microseconds) + , m_nanoseconds(nanoseconds) +{ + auto fields = AK::Array { + &Duration::m_years, + &Duration::m_months, + &Duration::m_weeks, + &Duration::m_days, + &Duration::m_hours, + &Duration::m_minutes, + &Duration::m_seconds, + &Duration::m_milliseconds, + &Duration::m_microseconds, + &Duration::m_nanoseconds, + }; + + // NOTE: The spec stores these fields as mathematical values. VERIFY() that we have finite, integral values in them, + // and normalize any negative zeros caused by floating point math. This is usually done using ℝ(𝔽(value)) at + // the call site. + for (auto const& field : fields) { + auto& value = this->*field; + VERIFY(isfinite(value)); + // FIXME: test-js contains a small number of cases where a Temporal.Duration is constructed with a non-integral + // double. Eliminate these and VERIFY(trunc(value) == value) instead. + if (trunc(value) != value) + value = trunc(value); + else if (bit_cast(value) == NEGATIVE_ZERO_BITS) + value = 0; + } +} + +// maxTimeDuration = 2**53 × 10**9 - 1 = 9,007,199,254,740,991,999,999,999 +TimeDuration const MAX_TIME_DURATION = "9007199254740991999999999"_sbigint; + +// 7.5.4 ZeroDateDuration ( ), https://tc39.es/proposal-temporal/#sec-temporal-zerodateduration +DateDuration zero_date_duration(VM& vm) +{ + // 1. Return ! CreateDateDurationRecord(0, 0, 0, 0). + return MUST(create_date_duration_record(vm, 0, 0, 0, 0)); +} + +// 7.5.5 ToInternalDurationRecord ( duration ), https://tc39.es/proposal-temporal/#sec-temporal-tointernaldurationrecord +InternalDuration to_internal_duration_record(VM& vm, Duration const& duration) +{ + // 1. Let dateDuration be ! CreateDateDurationRecord(duration.[[Years]], duration.[[Months]], duration.[[Weeks]], duration.[[Days]]). + auto date_duration = MUST(create_date_duration_record(vm, duration.years(), duration.months(), duration.weeks(), duration.days())); + + // 2. Let timeDuration be TimeDurationFromComponents(duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]]). + auto time_duration = time_duration_from_components(duration.hours(), duration.minutes(), duration.seconds(), duration.milliseconds(), duration.microseconds(), duration.nanoseconds()); + + // 3. Return ! CombineDateAndTimeDuration(dateDuration, timeDuration). + return MUST(combine_date_and_time_duration(vm, date_duration, move(time_duration))); +} + +// 7.5.9 CreateDateDurationRecord ( years, months, weeks, days ), https://tc39.es/proposal-temporal/#sec-temporal-createdatedurationrecord +ThrowCompletionOr create_date_duration_record(VM& vm, double years, double months, double weeks, double days) +{ + // 1. If IsValidDuration(years, months, weeks, days, 0, 0, 0, 0, 0, 0) is false, throw a RangeError exception. + if (!is_valid_duration(years, months, weeks, days, 0, 0, 0, 0, 0, 0)) + return vm.throw_completion(ErrorType::TemporalInvalidDuration); + + // 2. Return Date Duration Record { [[Years]]: ℝ(𝔽(years)), [[Months]]: ℝ(𝔽(months)), [[Weeks]]: ℝ(𝔽(weeks)), [[Days]]: ℝ(𝔽(days)) }. + return DateDuration { years, months, weeks, days }; +} + +// 7.5.11 CombineDateAndTimeDuration ( dateDuration, timeDuration ), https://tc39.es/proposal-temporal/#sec-temporal-combinedateandtimeduration +ThrowCompletionOr combine_date_and_time_duration(VM& vm, DateDuration date_duration, TimeDuration time_duration) +{ + // 1. Let dateSign be DateDurationSign(dateDuration). + auto date_sign = date_duration_sign(date_duration); + + // 2. Let timeSign be TimeDurationSign(timeDuration). + auto time_sign = time_duration_sign(time_duration); + + // 3. If dateSign ≠ 0 and timeSign ≠ 0 and dateSign ≠ timeSign, throw a RangeError exception. + if (date_sign != 0 && time_sign != 0 && date_sign != time_sign) + return vm.throw_completion(ErrorType::TemporalInvalidDuration); + + // 4. Return Internal Duration Record { [[Date]]: dateDuration, [[Time]]: timeDuration }. + return InternalDuration { date_duration, move(time_duration) }; +} + +// 7.5.12 ToTemporalDuration ( item ), https://tc39.es/proposal-temporal/#sec-temporal-totemporalduration +ThrowCompletionOr> to_temporal_duration(VM& vm, Value item) +{ + // 1. If item is an Object and item has an [[InitializedTemporalDuration]] internal slot, then + if (item.is_object() && is(item.as_object())) { + auto const& duration = static_cast(item.as_object()); + + // a. Return ! CreateTemporalDuration(item.[[Years]], item.[[Months]], item.[[Weeks]], item.[[Days]], item.[[Hours]], item.[[Minutes]], item.[[Seconds]], item.[[Milliseconds]], item.[[Microseconds]], item.[[Nanoseconds]]). + return MUST(create_temporal_duration(vm, duration.years(), duration.months(), duration.weeks(), duration.days(), duration.hours(), duration.minutes(), duration.seconds(), duration.milliseconds(), duration.microseconds(), duration.nanoseconds())); + } + + // 2. If item is not an Object, then + if (!item.is_object()) { + // a. If item is not a String, throw a TypeError exception. + if (!item.is_string()) + return vm.throw_completion(ErrorType::NotAString, item); + + // b. Return ? ParseTemporalDurationString(item). + return TRY(parse_temporal_duration_string(vm, item.as_string().utf8_string_view())); + } + + // 3. Let result be a new Partial Duration Record with each field set to 0. + auto result = PartialDuration::zero(); + + // 4. Let partial be ? ToTemporalPartialDurationRecord(item). + auto partial = TRY(to_temporal_partial_duration_record(vm, item)); + + // 5. If partial.[[Years]] is not undefined, set result.[[Years]] to partial.[[Years]]. + if (partial.years.has_value()) + result.years = *partial.years; + + // 6. If partial.[[Months]] is not undefined, set result.[[Months]] to partial.[[Months]]. + if (partial.months.has_value()) + result.months = *partial.months; + + // 7. If partial.[[Weeks]] is not undefined, set result.[[Weeks]] to partial.[[Weeks]]. + if (partial.weeks.has_value()) + result.weeks = *partial.weeks; + + // 8. If partial.[[Days]] is not undefined, set result.[[Days]] to partial.[[Days]]. + if (partial.days.has_value()) + result.days = *partial.days; + + // 9. If partial.[[Hours]] is not undefined, set result.[[Hours]] to partial.[[Hours]]. + if (partial.hours.has_value()) + result.hours = *partial.hours; + + // 10. If partial.[[Minutes]] is not undefined, set result.[[Minutes]] to partial.[[Minutes]]. + if (partial.minutes.has_value()) + result.minutes = *partial.minutes; + + // 11. If partial.[[Seconds]] is not undefined, set result.[[Seconds]] to partial.[[Seconds]]. + if (partial.seconds.has_value()) + result.seconds = *partial.seconds; + + // 12. If partial.[[Milliseconds]] is not undefined, set result.[[Milliseconds]] to partial.[[Milliseconds]]. + if (partial.milliseconds.has_value()) + result.milliseconds = *partial.milliseconds; + + // 13. If partial.[[Microseconds]] is not undefined, set result.[[Microseconds]] to partial.[[Microseconds]]. + if (partial.microseconds.has_value()) + result.microseconds = *partial.microseconds; + + // 14. If partial.[[Nanoseconds]] is not undefined, set result.[[Nanoseconds]] to partial.[[Nanoseconds]]. + if (partial.nanoseconds.has_value()) + result.nanoseconds = *partial.nanoseconds; + + // 15. Return ? CreateTemporalDuration(result.[[Years]], result.[[Months]], result.[[Weeks]], result.[[Days]], result.[[Hours]], result.[[Minutes]], result.[[Seconds]], result.[[Milliseconds]], result.[[Microseconds]], result.[[Nanoseconds]]). + return TRY(create_temporal_duration(vm, *result.years, *result.months, *result.weeks, *result.days, *result.hours, *result.minutes, *result.seconds, *result.milliseconds, *result.microseconds, *result.nanoseconds)); +} + +// 7.5.13 DurationSign ( duration ), https://tc39.es/proposal-temporal/#sec-temporal-durationsign +i8 duration_sign(Duration const& duration) +{ + // 1. For each value v of « duration.[[Years]], duration.[[Months]], duration.[[Weeks]], duration.[[Days]], duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]] », do + for (auto value : { duration.years(), duration.months(), duration.weeks(), duration.days(), duration.hours(), duration.minutes(), duration.seconds(), duration.milliseconds(), duration.microseconds(), duration.nanoseconds() }) { + // a. If v < 0, return -1. + if (value < 0) + return -1; + + // b. If v > 0, return 1. + if (value > 0) + return 1; + } + + // 2. Return 0. + return 0; +} + +// 7.5.14 DateDurationSign ( dateDuration ), https://tc39.es/proposal-temporal/#sec-temporal-datedurationsign +i8 date_duration_sign(DateDuration const& date_duration) +{ + // 1. For each value v of « dateDuration.[[Years]], dateDuration.[[Months]], dateDuration.[[Weeks]], dateDuration.[[Days]] », do + for (auto value : { date_duration.years, date_duration.months, date_duration.weeks, date_duration.days }) { + // a. If v < 0, return -1. + if (value < 0) + return -1; + + // b. If v > 0, return 1. + if (value > 0) + return 1; + } + + // 2. Return 0. + return 0; +} + // 7.5.16 IsValidDuration ( years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds ), https://tc39.es/proposal-temporal/#sec-temporal-isvalidduration bool is_valid_duration(double years, double months, double weeks, double days, double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds) { @@ -66,32 +273,232 @@ bool is_valid_duration(double years, double months, double weeks, double days, d // unsafe integer. This multiplication can be implemented in C++ with an implementation of std::remquo() // with sufficient bits in the quotient. String manipulation will also give an exact result, since the // multiplication is by a power of 10. - static Crypto::SignedBigInteger days_to_nanoseconds { 8.64e13 }; - static Crypto::SignedBigInteger hours_to_nanoseconds { 3.6e12 }; - static Crypto::SignedBigInteger minutes_to_nanoseconds { 6e10 }; - static Crypto::SignedBigInteger seconds_to_nanoseconds { 1e9 }; - static Crypto::SignedBigInteger milliseconds_to_nanoseconds { 1e6 }; - static Crypto::SignedBigInteger microseconds_to_nanoseconds { 1e3 }; + auto total_fractional_seconds = TimeDuration { days }.multiplied_by(NANOSECONDS_PER_DAY); + total_fractional_seconds = total_fractional_seconds.plus(TimeDuration { hours }.multiplied_by(NANOSECONDS_PER_HOUR)); + total_fractional_seconds = total_fractional_seconds.plus(TimeDuration { minutes }.multiplied_by(NANOSECONDS_PER_MINUTE)); + total_fractional_seconds = total_fractional_seconds.plus(TimeDuration { seconds }.multiplied_by(NANOSECONDS_PER_SECOND)); + total_fractional_seconds = total_fractional_seconds.plus(TimeDuration { milliseconds }.multiplied_by(NANOSECONDS_PER_MILLISECOND)); + total_fractional_seconds = total_fractional_seconds.plus(TimeDuration { microseconds }.multiplied_by(NANOSECONDS_PER_MICROSECOND)); + total_fractional_seconds = total_fractional_seconds.plus(TimeDuration { nanoseconds }); - auto normalized_nanoseconds = Crypto::SignedBigInteger { days }.multiplied_by(days_to_nanoseconds); - normalized_nanoseconds = normalized_nanoseconds.plus(Crypto::SignedBigInteger { hours }.multiplied_by(hours_to_nanoseconds)); - normalized_nanoseconds = normalized_nanoseconds.plus(Crypto::SignedBigInteger { minutes }.multiplied_by(minutes_to_nanoseconds)); - normalized_nanoseconds = normalized_nanoseconds.plus(Crypto::SignedBigInteger { seconds }.multiplied_by(seconds_to_nanoseconds)); - normalized_nanoseconds = normalized_nanoseconds.plus(Crypto::SignedBigInteger { milliseconds }.multiplied_by(milliseconds_to_nanoseconds)); - normalized_nanoseconds = normalized_nanoseconds.plus(Crypto::SignedBigInteger { microseconds }.multiplied_by(microseconds_to_nanoseconds)); - normalized_nanoseconds = normalized_nanoseconds.plus(Crypto::SignedBigInteger { nanoseconds }); - - // 8. If abs(normalizedSeconds) ≥ 2**53, return false. - static auto maximum_time = Crypto::SignedBigInteger { MAX_ARRAY_LIKE_INDEX }.plus(1_bigint).multiplied_by(seconds_to_nanoseconds); - - if (normalized_nanoseconds.is_negative()) - normalized_nanoseconds.negate(); - - if (normalized_nanoseconds >= maximum_time) + // 8. If abs(totalFractionalSeconds) ≥ 2**53, return false. + if (total_fractional_seconds.unsigned_value() > MAX_TIME_DURATION.unsigned_value()) return false; // 9. Return true. return true; } +// 7.5.17 DefaultTemporalLargestUnit ( duration ), https://tc39.es/proposal-temporal/#sec-temporal-defaulttemporallargestunit +Unit default_temporal_largest_unit(Duration const& duration) +{ + // 1. If duration.[[Years]] ≠ 0, return YEAR. + if (duration.years() != 0) + return Unit::Year; + + // 2. If duration.[[Months]] ≠ 0, return MONTH. + if (duration.months() != 0) + return Unit::Month; + + // 3. If duration.[[Weeks]] ≠ 0, return WEEK. + if (duration.weeks() != 0) + return Unit::Week; + + // 4. If duration.[[Days]] ≠ 0, return DAY. + if (duration.days() != 0) + return Unit::Day; + + // 5. If duration.[[Hours]] ≠ 0, return HOUR. + if (duration.hours() != 0) + return Unit::Hour; + + // 6. If duration.[[Minutes]] ≠ 0, return MINUTE. + if (duration.minutes() != 0) + return Unit::Minute; + + // 7. If duration.[[Seconds]] ≠ 0, return SECOND. + if (duration.seconds() != 0) + return Unit::Second; + + // 8. If duration.[[Milliseconds]] ≠ 0, return MILLISECOND. + if (duration.milliseconds() != 0) + return Unit::Millisecond; + + // 9. If duration.[[Microseconds]] ≠ 0, return MICROSECOND. + if (duration.microseconds() != 0) + return Unit::Microsecond; + + // 10. Return NANOSECOND. + return Unit::Nanosecond; +} + +// 7.5.18 ToTemporalPartialDurationRecord ( temporalDurationLike ), https://tc39.es/proposal-temporal/#sec-temporal-totemporalpartialdurationrecord +ThrowCompletionOr to_temporal_partial_duration_record(VM& vm, Value temporal_duration_like) +{ + // 1. If temporalDurationLike is not an Object, then + if (!temporal_duration_like.is_object()) { + // a. Throw a TypeError exception. + return vm.throw_completion(ErrorType::NotAnObject, temporal_duration_like); + } + + // 2. Let result be a new partial Duration Record with each field set to undefined. + PartialDuration result {}; + + // 3. NOTE: The following steps read properties and perform independent validation in alphabetical order. + + auto to_integral_if_defined = [&vm, &temporal_duration = temporal_duration_like.as_object()](auto const& property, auto& field) -> ThrowCompletionOr { + if (auto value = TRY(temporal_duration.get(property)); !value.is_undefined()) + field = TRY(to_integer_if_integral(vm, value, ErrorType::TemporalInvalidDurationPropertyValueNonIntegral, property, value)); + return {}; + }; + + // 4. Let days be ? Get(temporalDurationLike, "days"). + // 5. If days is not undefined, set result.[[Days]] to ? ToIntegerIfIntegral(days). + TRY(to_integral_if_defined(vm.names.days, result.days)); + + // 6. Let hours be ? Get(temporalDurationLike, "hours"). + // 7. If hours is not undefined, set result.[[Hours]] to ? ToIntegerIfIntegral(hours). + TRY(to_integral_if_defined(vm.names.hours, result.hours)); + + // 8. Let microseconds be ? Get(temporalDurationLike, "microseconds"). + // 9. If microseconds is not undefined, set result.[[Microseconds]] to ? ToIntegerIfIntegral(microseconds). + TRY(to_integral_if_defined(vm.names.microseconds, result.microseconds)); + + // 10. Let milliseconds be ? Get(temporalDurationLike, "milliseconds"). + // 11. If milliseconds is not undefined, set result.[[Milliseconds]] to ? ToIntegerIfIntegral(milliseconds). + TRY(to_integral_if_defined(vm.names.milliseconds, result.milliseconds)); + + // 12. Let minutes be ? Get(temporalDurationLike, "minutes"). + // 13. If minutes is not undefined, set result.[[Minutes]] to ? ToIntegerIfIntegral(minutes). + TRY(to_integral_if_defined(vm.names.minutes, result.minutes)); + + // 14. Let months be ? Get(temporalDurationLike, "months"). + // 15. If months is not undefined, set result.[[Months]] to ? ToIntegerIfIntegral(months). + TRY(to_integral_if_defined(vm.names.months, result.months)); + + // 16. Let nanoseconds be ? Get(temporalDurationLike, "nanoseconds"). + // 17. If nanoseconds is not undefined, set result.[[Nanoseconds]] to ? ToIntegerIfIntegral(nanoseconds). + TRY(to_integral_if_defined(vm.names.nanoseconds, result.nanoseconds)); + + // 18. Let seconds be ? Get(temporalDurationLike, "seconds"). + // 19. If seconds is not undefined, set result.[[Seconds]] to ? ToIntegerIfIntegral(seconds). + TRY(to_integral_if_defined(vm.names.seconds, result.seconds)); + + // 20. Let weeks be ? Get(temporalDurationLike, "weeks"). + // 21. If weeks is not undefined, set result.[[Weeks]] to ? ToIntegerIfIntegral(weeks). + TRY(to_integral_if_defined(vm.names.weeks, result.weeks)); + + // 22. Let years be ? Get(temporalDurationLike, "years"). + // 23. If years is not undefined, set result.[[Years]] to ? ToIntegerIfIntegral(years). + TRY(to_integral_if_defined(vm.names.years, result.years)); + + // 24. If years is undefined, and months is undefined, and weeks is undefined, and days is undefined, and hours is + // undefined, and minutes is undefined, and seconds is undefined, and milliseconds is undefined, and microseconds + // is undefined, and nanoseconds is undefined, throw a TypeError exception. + if (!result.any_field_defined()) + return vm.throw_completion(ErrorType::TemporalInvalidDurationLikeObject); + + // 25. Return result. + return result; +} + +// 7.5.19 CreateTemporalDuration ( years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds [ , newTarget ] ), https://tc39.es/proposal-temporal/#sec-temporal-createtemporalduration +ThrowCompletionOr> create_temporal_duration(VM& vm, double years, double months, double weeks, double days, double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds, GC::Ptr new_target) +{ + auto& realm = *vm.current_realm(); + + // 1. If IsValidDuration(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds) is false, throw a RangeError exception. + if (!is_valid_duration(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds)) + return vm.throw_completion(ErrorType::TemporalInvalidDuration); + + // 2. If newTarget is not present, set newTarget to %Temporal.Duration%. + if (!new_target) + new_target = realm.intrinsics().temporal_duration_constructor(); + + // 3. Let object be ? OrdinaryCreateFromConstructor(newTarget, "%Temporal.Duration.prototype%", « [[InitializedTemporalDuration]], [[Years]], [[Months]], [[Weeks]], [[Days]], [[Hours]], [[Minutes]], [[Seconds]], [[Milliseconds]], [[Microseconds]], [[Nanoseconds]] »). + // 4. Set object.[[Years]] to ℝ(𝔽(years)). + // 5. Set object.[[Months]] to ℝ(𝔽(months)). + // 6. Set object.[[Weeks]] to ℝ(𝔽(weeks)). + // 7. Set object.[[Days]] to ℝ(𝔽(days)). + // 8. Set object.[[Hours]] to ℝ(𝔽(hours)). + // 9. Set object.[[Minutes]] to ℝ(𝔽(minutes)). + // 10. Set object.[[Seconds]] to ℝ(𝔽(seconds)). + // 11. Set object.[[Milliseconds]] to ℝ(𝔽(milliseconds)). + // 12. Set object.[[Microseconds]] to ℝ(𝔽(microseconds)). + // 13. Set object.[[Nanoseconds]] to ℝ(𝔽(nanoseconds)). + auto object = TRY(ordinary_create_from_constructor(vm, *new_target, &Intrinsics::temporal_duration_prototype, years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds)); + + // 14. Return object. + return object; +} + +// 7.5.21 TimeDurationFromComponents ( hours, minutes, seconds, milliseconds, microseconds, nanoseconds ), https://tc39.es/proposal-temporal/#sec-temporal-timedurationfromcomponents +TimeDuration time_duration_from_components(double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds) +{ + // 1. Set minutes to minutes + hours × 60. + auto total_minutes = TimeDuration { minutes }.plus(TimeDuration { hours }.multiplied_by(60_bigint)); + + // 2. Set seconds to seconds + minutes × 60. + auto total_seconds = TimeDuration { seconds }.plus(total_minutes.multiplied_by(60_bigint)); + + // 3. Set milliseconds to milliseconds + seconds × 1000. + auto total_milliseconds = TimeDuration { milliseconds }.plus(total_seconds.multiplied_by(1000_bigint)); + + // 4. Set microseconds to microseconds + milliseconds × 1000. + auto total_microseconds = TimeDuration { microseconds }.plus(total_milliseconds.multiplied_by(1000_bigint)); + + // 5. Set nanoseconds to nanoseconds + microseconds × 1000. + auto total_nanoseconds = TimeDuration { nanoseconds }.plus(total_microseconds.multiplied_by(1000_bigint)); + + // 6. Assert: abs(nanoseconds) ≤ maxTimeDuration. + VERIFY(total_nanoseconds.unsigned_value() <= MAX_TIME_DURATION.unsigned_value()); + + // 7. Return nanoseconds. + return total_nanoseconds; +} + +// 7.5.23 Add24HourDaysToTimeDuration ( d, days ), https://tc39.es/proposal-temporal/#sec-temporal-add24hourdaystonormalizedtimeduration +ThrowCompletionOr add_24_hour_days_to_time_duration(VM& vm, TimeDuration const& time_duration, double days) +{ + // 1. Let result be d + days × nsPerDay. + auto result = time_duration.plus(TimeDuration { days }.multiplied_by(NANOSECONDS_PER_DAY)); + + // 2. If abs(result) > maxTimeDuration, throw a RangeError exception. + if (result.unsigned_value() > MAX_TIME_DURATION.unsigned_value()) + return vm.throw_completion(ErrorType::TemporalInvalidDuration); + + // 3. Return result. + return result; +} + +// 7.5.25 CompareTimeDuration ( one, two ), https://tc39.es/proposal-temporal/#sec-temporal-comparetimeduration +i8 compare_time_duration(TimeDuration const& one, TimeDuration const& two) +{ + // 1. If one > two, return 1. + if (one > two) + return 1; + + // 2. If one < two, return -1. + if (one < two) + return -1; + + // 3. Return 0. + return 0; +} + +// 7.5.28 TimeDurationSign ( d ), https://tc39.es/proposal-temporal/#sec-temporal-timedurationsign +i8 time_duration_sign(TimeDuration const& time_duration) +{ + // 1. If d < 0, return -1. + if (time_duration.is_negative()) + return -1; + + // 2. If d > 0, return 1. + if (time_duration.is_positive()) + return 1; + + // 3. Return 0. + return 0; +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/Duration.h b/Libraries/LibJS/Runtime/Temporal/Duration.h index e8c7769df65..3e82ef249e8 100644 --- a/Libraries/LibJS/Runtime/Temporal/Duration.h +++ b/Libraries/LibJS/Runtime/Temporal/Duration.h @@ -8,10 +8,112 @@ #pragma once -#include +#include +#include +#include +#include +#include +#include namespace JS::Temporal { +#define JS_ENUMERATE_DURATION_UNITS \ + __JS_ENUMERATE(years) \ + __JS_ENUMERATE(months) \ + __JS_ENUMERATE(weeks) \ + __JS_ENUMERATE(days) \ + __JS_ENUMERATE(hours) \ + __JS_ENUMERATE(minutes) \ + __JS_ENUMERATE(seconds) \ + __JS_ENUMERATE(milliseconds) \ + __JS_ENUMERATE(microseconds) \ + __JS_ENUMERATE(nanoseconds) + +class Duration final : public Object { + JS_OBJECT(Duration, Object); + GC_DECLARE_ALLOCATOR(Duration); + +public: + virtual ~Duration() override = default; + +#define __JS_ENUMERATE(unit) \ + [[nodiscard]] double unit() const { return m_##unit; } + JS_ENUMERATE_DURATION_UNITS +#undef __JS_ENUMERATE + +private: + Duration(double years, double months, double weeks, double days, double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds, Object& prototype); + + double m_years { 0 }; // [[Years]] + double m_months { 0 }; // [[Months]] + double m_weeks { 0 }; // [[Weeks]] + double m_days { 0 }; // [[Days]] + double m_hours { 0 }; // [[Hours]] + double m_minutes { 0 }; // [[Minutes]] + double m_seconds { 0 }; // [[Seconds]] + double m_milliseconds { 0 }; // [[Milliseconds]] + double m_microseconds { 0 }; // [[Microseconds]] + double m_nanoseconds { 0 }; // [[Nanoseconds]] +}; + +// 7.5.1 Date Duration Records, https://tc39.es/proposal-temporal/#sec-temporal-date-duration-records +struct DateDuration { + double years { 0 }; + double months { 0 }; + double weeks { 0 }; + double days { 0 }; +}; + +// 7.5.2 Partial Duration Records, https://tc39.es/proposal-temporal/#sec-temporal-partial-duration-records +struct PartialDuration { + static PartialDuration zero() + { + return { .years = 0, .months = 0, .weeks = 0, .days = 0, .hours = 0, .minutes = 0, .seconds = 0, .milliseconds = 0, .microseconds = 0, .nanoseconds = 0 }; + } + + bool any_field_defined() const + { + return years.has_value() || months.has_value() || weeks.has_value() || days.has_value() || hours.has_value() || minutes.has_value() || seconds.has_value() || milliseconds.has_value() || microseconds.has_value() || nanoseconds.has_value(); + } + + Optional years; + Optional months; + Optional weeks; + Optional days; + Optional hours; + Optional minutes; + Optional seconds; + Optional milliseconds; + Optional microseconds; + Optional nanoseconds; +}; + +// A time duration is an integer in the inclusive interval from -maxTimeDuration to maxTimeDuration, where +// maxTimeDuration = 2**53 × 10**9 - 1 = 9,007,199,254,740,991,999,999,999. It represents the portion of a +// Temporal.Duration object that deals with time units, but as a combined value of total nanoseconds. +using TimeDuration = Crypto::SignedBigInteger; +extern TimeDuration const MAX_TIME_DURATION; + +// 7.5.3 Internal Duration Records, https://tc39.es/proposal-temporal/#sec-temporal-internal-duration-records +struct InternalDuration { + DateDuration date; + TimeDuration time; +}; + +DateDuration zero_date_duration(VM&); +InternalDuration to_internal_duration_record(VM&, Duration const&); +ThrowCompletionOr create_date_duration_record(VM&, double years, double months, double weeks, double days); +ThrowCompletionOr combine_date_and_time_duration(VM&, DateDuration, TimeDuration); +ThrowCompletionOr> to_temporal_duration(VM&, Value); +i8 duration_sign(Duration const&); +i8 date_duration_sign(DateDuration const&); bool is_valid_duration(double years, double months, double weeks, double days, double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds); +Unit default_temporal_largest_unit(Duration const&); +ThrowCompletionOr to_temporal_partial_duration_record(VM&, Value temporal_duration_like); +ThrowCompletionOr> create_temporal_duration(VM&, double years, double months, double weeks, double days, double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds, GC::Ptr new_target = {}); +TimeDuration time_duration_from_components(double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds); +ThrowCompletionOr add_24_hour_days_to_time_duration(VM&, TimeDuration const&, double days); +i8 compare_time_duration(TimeDuration const&, TimeDuration const&); +i8 time_duration_sign(TimeDuration const&); } diff --git a/Libraries/LibJS/Runtime/Temporal/DurationConstructor.cpp b/Libraries/LibJS/Runtime/Temporal/DurationConstructor.cpp new file mode 100644 index 00000000000..c848dbbe728 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/DurationConstructor.cpp @@ -0,0 +1,194 @@ +/* + * 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 + +namespace JS::Temporal { + +GC_DEFINE_ALLOCATOR(DurationConstructor); + +// 7.1 The Temporal.Duration Constructor, https://tc39.es/proposal-temporal/#sec-temporal-duration-constructor +DurationConstructor::DurationConstructor(Realm& realm) + : NativeFunction(realm.vm().names.Duration.as_string(), realm.intrinsics().function_prototype()) +{ +} + +void DurationConstructor::initialize(Realm& realm) +{ + Base::initialize(realm); + + auto& vm = this->vm(); + + // 7.2.1 Temporal.Duration.prototype, https://tc39.es/proposal-temporal/#sec-temporal.duration.prototype + define_direct_property(vm.names.prototype, realm.intrinsics().temporal_duration_prototype(), 0); + + u8 attr = Attribute::Writable | Attribute::Configurable; + define_native_function(realm, vm.names.from, from, 1, attr); + define_native_function(realm, vm.names.compare, compare, 2, attr); + + define_direct_property(vm.names.length, Value(0), Attribute::Configurable); +} + +// 7.1.1 Temporal.Duration ( [ years [ , months [ , weeks [ , days [ , hours [ , minutes [ , seconds [ , milliseconds [ , microseconds [ , nanoseconds ] ] ] ] ] ] ] ] ] ] ), https://tc39.es/proposal-temporal/#sec-temporal.duration +ThrowCompletionOr DurationConstructor::call() +{ + auto& vm = this->vm(); + + // 1. If NewTarget is undefined, then + // a. Throw a TypeError exception. + return vm.throw_completion(ErrorType::ConstructorWithoutNew, "Temporal.Duration"); +} + +// 7.1.1 Temporal.Duration ( [ years [ , months [ , weeks [ , days [ , hours [ , minutes [ , seconds [ , milliseconds [ , microseconds [ , nanoseconds ] ] ] ] ] ] ] ] ] ] ), https://tc39.es/proposal-temporal/#sec-temporal.duration +ThrowCompletionOr> DurationConstructor::construct(FunctionObject& new_target) +{ + auto& vm = this->vm(); + + auto next_integer_argument = [&, index = 0]() mutable -> ThrowCompletionOr { + if (auto value = vm.argument(index++); !value.is_undefined()) + return to_integer_if_integral(vm, value, ErrorType::TemporalInvalidDuration); + return 0; + }; + + // 2. If years is undefined, let y be 0; else let y be ? ToIntegerIfIntegral(years). + auto years = TRY(next_integer_argument()); + + // 3. If months is undefined, let mo be 0; else let mo be ? ToIntegerIfIntegral(months). + auto months = TRY(next_integer_argument()); + + // 4. If weeks is undefined, let w be 0; else let w be ? ToIntegerIfIntegral(weeks). + auto weeks = TRY(next_integer_argument()); + + // 5. If days is undefined, let d be 0; else let d be ? ToIntegerIfIntegral(days). + auto days = TRY(next_integer_argument()); + + // 6. If hours is undefined, let h be 0; else let h be ? ToIntegerIfIntegral(hours). + auto hours = TRY(next_integer_argument()); + + // 7. If minutes is undefined, let m be 0; else let m be ? ToIntegerIfIntegral(minutes). + auto minutes = TRY(next_integer_argument()); + + // 8. If seconds is undefined, let s be 0; else let s be ? ToIntegerIfIntegral(seconds). + auto seconds = TRY(next_integer_argument()); + + // 9. If milliseconds is undefined, let ms be 0; else let ms be ? ToIntegerIfIntegral(milliseconds). + auto milliseconds = TRY(next_integer_argument()); + + // 10. If microseconds is undefined, let mis be 0; else let mis be ? ToIntegerIfIntegral(microseconds). + auto microseconds = TRY(next_integer_argument()); + + // 11. If nanoseconds is undefined, let ns be 0; else let ns be ? ToIntegerIfIntegral(nanoseconds). + auto nanoseconds = TRY(next_integer_argument()); + + // 12. Return ? CreateTemporalDuration(y, mo, w, d, h, m, s, ms, mis, ns, NewTarget). + return TRY(create_temporal_duration(vm, years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, new_target)); +} + +// 7.2.2 Temporal.Duration.from ( item ), https://tc39.es/proposal-temporal/#sec-temporal.duration.from +JS_DEFINE_NATIVE_FUNCTION(DurationConstructor::from) +{ + // 1. Return ? ToTemporalDuration(item). + return TRY(to_temporal_duration(vm, vm.argument(0))); +} + +// 7.2.3 Temporal.Duration.compare ( one, two [ , options ] ), https://tc39.es/proposal-temporal/#sec-temporal.duration.compare +JS_DEFINE_NATIVE_FUNCTION(DurationConstructor::compare) +{ + // 1. Set one to ? ToTemporalDuration(one). + auto one = TRY(to_temporal_duration(vm, vm.argument(0))); + + // 2. Set two to ? ToTemporalDuration(two). + auto two = TRY(to_temporal_duration(vm, vm.argument(1))); + + // 3. Let resolvedOptions be ? GetOptionsObject(options). + auto resolved_options = TRY(get_options_object(vm, vm.argument(2))); + + // 4. Let relativeToRecord be ? GetTemporalRelativeToOption(resolvedOptions). + auto relative_to_record = TRY(get_temporal_relative_to_option(vm, resolved_options)); + + // 5. If one.[[Years]] = two.[[Years]], and one.[[Months]] = two.[[Months]], and one.[[Weeks]] = two.[[Weeks]], and + // one.[[Days]] = two.[[Days]], and one.[[Hours]] = two.[[Hours]], and one.[[Minutes]] = two.[[Minutes]], and + // one.[[Seconds]] = two.[[Seconds]], and one.[[Milliseconds]] = two.[[Milliseconds]], and + // one.[[Microseconds]] = two.[[Microseconds]], and one.[[Nanoseconds]] = two.[[Nanoseconds]], then + if (one->years() == two->years() + && one->months() == two->months() + && one->weeks() == two->weeks() + && one->days() == two->days() + && one->hours() == two->hours() + && one->minutes() == two->minutes() + && one->seconds() == two->seconds() + && one->milliseconds() == two->milliseconds() + && one->microseconds() == two->microseconds() + && one->nanoseconds() == two->nanoseconds()) { + // a. Return +0𝔽. + return 0; + } + + // 6. Let zonedRelativeTo be relativeToRecord.[[ZonedRelativeTo]]. + // 7. Let plainRelativeTo be relativeToRecord.[[PlainRelativeTo]]. + auto [zoned_relative_to, plain_relative_to] = relative_to_record; + + // 8. Let largestUnit1 be DefaultTemporalLargestUnit(one). + auto largest_unit1 = default_temporal_largest_unit(one); + + // 9. Let largestUnit2 be DefaultTemporalLargestUnit(two). + auto largest_unit2 = default_temporal_largest_unit(two); + + // 10. Let duration1 be ToInternalDurationRecord(one). + auto duration1 = to_internal_duration_record(vm, one); + + // 11. Let duration2 be ToInternalDurationRecord(two). + auto duration2 = to_internal_duration_record(vm, two); + + // 12. If zonedRelativeTo is not undefined, and either TemporalUnitCategory(largestUnit1) or TemporalUnitCategory(largestUnit2) is date, then + if (zoned_relative_to && (temporal_unit_category(largest_unit1) == UnitCategory::Date || temporal_unit_category(largest_unit2) == UnitCategory::Date)) { + // FIXME: a. Let timeZone be zonedRelativeTo.[[TimeZone]]. + // FIXME: b. Let calendar be zonedRelativeTo.[[Calendar]]. + // FIXME: c. Let after1 be ? AddZonedDateTime(zonedRelativeTo.[[EpochNanoseconds]], timeZone, calendar, duration1, constrain). + // FIXME: d. Let after2 be ? AddZonedDateTime(zonedRelativeTo.[[EpochNanoseconds]], timeZone, calendar, duration2, constrain). + // FIXME: e. If after1 > after2, return 1𝔽. + // FIXME: f. If after1 < after2, return -1𝔽. + + // g. Return +0𝔽. + return 0; + } + + double days1 = 0; + double days2 = 0; + + // 13. If IsCalendarUnit(largestUnit1) is true or IsCalendarUnit(largestUnit2) is true, then + if (is_calendar_unit(largest_unit1) || is_calendar_unit(largest_unit2)) { + // a. If plainRelativeTo is undefined, throw a RangeError exception. + if (!plain_relative_to) + return vm.throw_completion(ErrorType::TemporalMissingStartingPoint, "calendar units"); + + // FIXME: b. Let days1 be ? DateDurationDays(duration1.[[Date]], plainRelativeTo). + // FIXME: c. Let days2 be ? DateDurationDays(duration2.[[Date]], plainRelativeTo). + } + // 14. Else, + else { + // a. Let days1 be one.[[Days]]. + days1 = one->days(); + + // b. Let days2 be two.[[Days]]. + days2 = two->days(); + } + + // 15. Let timeDuration1 be ? Add24HourDaysToTimeDuration(duration1.[[Time]], days1). + auto time_duration1 = TRY(add_24_hour_days_to_time_duration(vm, duration1.time, days1)); + + // 16. Let timeDuration2 be ? Add24HourDaysToTimeDuration(duration2.[[Time]], days2). + auto time_duration2 = TRY(add_24_hour_days_to_time_duration(vm, duration2.time, days2)); + + // 17. Return 𝔽(CompareTimeDuration(timeDuration1, timeDuration2)). + return compare_time_duration(time_duration1, time_duration2); +} + +} diff --git a/Libraries/LibJS/Runtime/Temporal/DurationConstructor.h b/Libraries/LibJS/Runtime/Temporal/DurationConstructor.h new file mode 100644 index 00000000000..2ffd47e4782 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/DurationConstructor.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2021-2022, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace JS::Temporal { + +class DurationConstructor final : public NativeFunction { + JS_OBJECT(DurationConstructor, NativeFunction); + GC_DECLARE_ALLOCATOR(DurationConstructor); + +public: + virtual void initialize(Realm&) override; + virtual ~DurationConstructor() override = default; + + virtual ThrowCompletionOr call() override; + virtual ThrowCompletionOr> construct(FunctionObject& new_target) override; + +private: + explicit DurationConstructor(Realm&); + + virtual bool has_constructor() const override { return true; } + + JS_DECLARE_NATIVE_FUNCTION(from); + JS_DECLARE_NATIVE_FUNCTION(compare); +}; + +} diff --git a/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp b/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp new file mode 100644 index 00000000000..fffe0d4489e --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +namespace JS::Temporal { + +GC_DEFINE_ALLOCATOR(DurationPrototype); + +// 7.3 Properties of the Temporal.Duration Prototype Object, https://tc39.es/proposal-temporal/#sec-properties-of-the-temporal-duration-prototype-object +DurationPrototype::DurationPrototype(Realm& realm) + : PrototypeObject(realm.intrinsics().object_prototype()) +{ +} + +void DurationPrototype::initialize(Realm& realm) +{ + Base::initialize(realm); + + auto& vm = this->vm(); + + // 7.3.2 Temporal.Duration.prototype[ %Symbol.toStringTag% ], https://tc39.es/proposal-temporal/#sec-temporal.duration.prototype-%symbol.tostringtag% + define_direct_property(vm.well_known_symbol_to_string_tag(), PrimitiveString::create(vm, "Temporal.Duration"_string), Attribute::Configurable); + +#define __JS_ENUMERATE(unit) \ + define_native_accessor(realm, vm.names.unit, unit##_getter, {}, Attribute::Configurable); + JS_ENUMERATE_DURATION_UNITS +#undef __JS_ENUMERATE +} + +// 7.3.3 get Temporal.Duration.prototype.years, https://tc39.es/proposal-temporal/#sec-get-temporal.duration.prototype.years +// 7.3.4 get Temporal.Duration.prototype.months, https://tc39.es/proposal-temporal/#sec-get-temporal.duration.prototype.months +// 7.3.5 get Temporal.Duration.prototype.weeks, https://tc39.es/proposal-temporal/#sec-get-temporal.duration.prototype.weeks +// 7.3.6 get Temporal.Duration.prototype.days, https://tc39.es/proposal-temporal/#sec-get-temporal.duration.prototype.days +// 7.3.7 get Temporal.Duration.prototype.hours, https://tc39.es/proposal-temporal/#sec-get-temporal.duration.prototype.hours +// 7.3.8 get Temporal.Duration.prototype.minutes, https://tc39.es/proposal-temporal/#sec-get-temporal.duration.prototype.minutes +// 7.3.9 get Temporal.Duration.prototype.seconds, https://tc39.es/proposal-temporal/#sec-get-temporal.duration.prototype.seconds +// 7.3.10 get Temporal.Duration.prototype.milliseconds, https://tc39.es/proposal-temporal/#sec-get-temporal.duration.prototype.milliseconds +// 7.3.11 get Temporal.Duration.prototype.microseconds, https://tc39.es/proposal-temporal/#sec-get-temporal.duration.prototype.microseconds +// 7.3.12 get Temporal.Duration.prototype.nanoseconds, https://tc39.es/proposal-temporal/#sec-get-temporal.duration.prototype.nanoseconds +#define __JS_ENUMERATE(unit) \ + JS_DEFINE_NATIVE_FUNCTION(DurationPrototype::unit##_getter) \ + { \ + /* 1. Let duration be the this value. */ \ + /* 2. Perform ? RequireInternalSlot(duration, [[InitializedTemporalDuration]]). */ \ + auto duration = TRY(typed_this_object(vm)); \ + \ + /* 3. Return 𝔽(duration.[[]]). */ \ + return duration->unit(); \ + } +JS_ENUMERATE_DURATION_UNITS +#undef __JS_ENUMERATE + +} diff --git a/Libraries/LibJS/Runtime/Temporal/DurationPrototype.h b/Libraries/LibJS/Runtime/Temporal/DurationPrototype.h new file mode 100644 index 00000000000..cdbf221779e --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/DurationPrototype.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2021, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace JS::Temporal { + +class DurationPrototype final : public PrototypeObject { + JS_PROTOTYPE_OBJECT(DurationPrototype, Duration, Temporal.Duration); + GC_DECLARE_ALLOCATOR(DurationPrototype); + +public: + virtual void initialize(Realm&) override; + virtual ~DurationPrototype() override = default; + +private: + explicit DurationPrototype(Realm&); + +#define __JS_ENUMERATE(unit) \ + JS_DECLARE_NATIVE_FUNCTION(unit##_getter); + JS_ENUMERATE_DURATION_UNITS +#undef __JS_ENUMERATE +}; + +} diff --git a/Libraries/LibJS/Runtime/Temporal/ISO8601.cpp b/Libraries/LibJS/Runtime/Temporal/ISO8601.cpp new file mode 100644 index 00000000000..7526486fc3c --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/ISO8601.cpp @@ -0,0 +1,465 @@ +/* + * Copyright (c) 2021-2022, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +namespace JS::Temporal { + +// 13.30 ISO 8601 grammar, https://tc39.es/proposal-temporal/#sec-temporal-iso8601grammar +class ISO8601Parser { +public: + explicit ISO8601Parser(StringView input) + : m_input(input) + , m_state({ + .lexer = GenericLexer { input }, + .parse_result = {}, + }) + { + } + + [[nodiscard]] GenericLexer const& lexer() const { return m_state.lexer; } + [[nodiscard]] ParseResult const& parse_result() const { return m_state.parse_result; } + + enum class Separator { + No, + Yes, + }; + + // https://tc39.es/proposal-temporal/#prod-TemporalDurationString + [[nodiscard]] bool parse_temporal_duration_string() + { + // TemporalDurationString : + // Duration + return parse_duration(); + } + + // https://tc39.es/proposal-temporal/#prod-DurationDate + [[nodiscard]] bool parse_duration_date() + { + // DurationDate : + // DurationYearsPart DurationTime[opt] + // DurationMonthsPart DurationTime[opt] + // DurationWeeksPart DurationTime[opt] + // DurationDaysPart DurationTime[opt] + auto success = parse_duration_years_part() || parse_duration_months_part() || parse_duration_weeks_part() || parse_duration_days_part(); + if (!success) + return false; + + (void)parse_duration_time(); + return true; + } + + // https://tc39.es/proposal-temporal/#prod-Duration + [[nodiscard]] bool parse_duration() + { + StateTransaction transaction { *this }; + + // Duration ::: + // ASCIISign[opt] DurationDesignator DurationDate + // ASCIISign[opt] DurationDesignator DurationTime + (void)parse_ascii_sign(); + + if (!parse_duration_designator()) + return false; + + auto success = parse_duration_date() || parse_duration_time(); + if (!success) + return false; + + transaction.commit(); + return true; + } + + // https://tc39.es/proposal-temporal/#prod-DurationYearsPart + [[nodiscard]] bool parse_duration_years_part() + { + StateTransaction transaction { *this }; + + // DurationYearsPart : + // DecimalDigits[~Sep] YearsDesignator DurationMonthsPart + // DecimalDigits[~Sep] YearsDesignator DurationWeeksPart + // DecimalDigits[~Sep] YearsDesignator DurationDaysPart[opt] + if (!parse_decimal_digits(Separator::No, m_state.parse_result.duration_years)) + return false; + + if (!parse_years_designator()) + return false; + + (void)(parse_duration_months_part() || parse_duration_weeks_part() || parse_duration_days_part()); + + transaction.commit(); + return true; + } + + // https://tc39.es/proposal-temporal/#prod-DurationMonthsPart + [[nodiscard]] bool parse_duration_months_part() + { + StateTransaction transaction { *this }; + + // DurationMonthsPart : + // DecimalDigits[~Sep] MonthsDesignator DurationWeeksPart + // DecimalDigits[~Sep] MonthsDesignator DurationDaysPart[opt] + if (!parse_decimal_digits(Separator::No, m_state.parse_result.duration_months)) + return false; + + if (!parse_months_designator()) + return false; + + (void)(parse_duration_weeks_part() || parse_duration_days_part()); + + transaction.commit(); + return true; + } + + // https://tc39.es/proposal-temporal/#prod-DurationWeeksPart + [[nodiscard]] bool parse_duration_weeks_part() + { + StateTransaction transaction { *this }; + + // DurationWeeksPart : + // DecimalDigits[~Sep] WeeksDesignator DurationDaysPart[opt] + if (!parse_decimal_digits(Separator::No, m_state.parse_result.duration_weeks)) + return false; + + if (!parse_weeks_designator()) + return false; + + (void)parse_duration_days_part(); + + transaction.commit(); + return true; + } + + // https://tc39.es/proposal-temporal/#prod-DurationDaysPart + [[nodiscard]] bool parse_duration_days_part() + { + StateTransaction transaction { *this }; + + // DurationDaysPart : + // DecimalDigits[~Sep] DaysDesignator + if (!parse_decimal_digits(Separator::No, m_state.parse_result.duration_days)) + return false; + + if (!parse_days_designator()) + return false; + + transaction.commit(); + return true; + } + + // https://tc39.es/proposal-temporal/#prod-DurationTime + [[nodiscard]] bool parse_duration_time() + { + StateTransaction transaction { *this }; + + // DurationTime : + // TimeDesignator DurationHoursPart + // TimeDesignator DurationMinutesPart + // TimeDesignator DurationSecondsPart + if (!parse_time_designator()) + return false; + + auto success = parse_duration_hours_part() || parse_duration_minutes_part() || parse_duration_seconds_part(); + if (!success) + return false; + + transaction.commit(); + return true; + } + + // https://tc39.es/proposal-temporal/#prod-DurationHoursPart + [[nodiscard]] bool parse_duration_hours_part() + { + StateTransaction transaction { *this }; + + // DurationHoursPart : + // DecimalDigits[~Sep] TemporalDecimalFraction HoursDesignator + // DecimalDigits[~Sep] HoursDesignator DurationMinutesPart + // DecimalDigits[~Sep] HoursDesignator DurationSecondsPart[opt] + if (!parse_decimal_digits(Separator::No, m_state.parse_result.duration_hours)) + return false; + + auto is_fractional = parse_temporal_decimal_fraction(m_state.parse_result.duration_hours_fraction); + + if (!parse_hours_designator()) + return false; + if (!is_fractional) + (void)(parse_duration_minutes_part() || parse_duration_seconds_part()); + + transaction.commit(); + return true; + } + + // https://tc39.es/proposal-temporal/#prod-DurationMinutesPart + [[nodiscard]] bool parse_duration_minutes_part() + { + StateTransaction transaction { *this }; + + // DurationMinutesPart : + // DecimalDigits[~Sep] TemporalDecimalFraction MinutesDesignator + // DecimalDigits[~Sep] MinutesDesignator DurationSecondsPart[opt] + if (!parse_decimal_digits(Separator::No, m_state.parse_result.duration_minutes)) + return false; + + auto is_fractional = parse_temporal_decimal_fraction(m_state.parse_result.duration_minutes_fraction); + + if (!parse_minutes_designator()) + return false; + if (!is_fractional) + (void)parse_duration_seconds_part(); + + transaction.commit(); + return true; + } + + // https://tc39.es/proposal-temporal/#prod-DurationSecondsPart + [[nodiscard]] bool parse_duration_seconds_part() + { + StateTransaction transaction { *this }; + + // DurationSecondsPart : + // DecimalDigits[~Sep] TemporalDecimalFraction[opt] SecondsDesignator + if (!parse_decimal_digits(Separator::No, m_state.parse_result.duration_seconds)) + return false; + + (void)parse_temporal_decimal_fraction(m_state.parse_result.duration_seconds_fraction); + + if (!parse_seconds_designator()) + return false; + + transaction.commit(); + return true; + } + + // https://tc39.es/ecma262/#prod-DecimalDigit + [[nodiscard]] bool parse_decimal_digit() + { + // DecimalDigit : one of + // 0 1 2 3 4 5 6 7 8 9 + if (m_state.lexer.next_is(is_ascii_digit)) { + m_state.lexer.consume(); + return true; + } + return false; + } + + // https://tc39.es/ecma262/#prod-DecimalDigits + [[nodiscard]] bool parse_decimal_digits(Separator separator, Optional& result) + { + StateTransaction transaction { *this }; + + // FIXME: Implement [+Sep] if it's ever needed. + VERIFY(separator == Separator::No); + + // DecimalDigits[Sep] :: + // DecimalDigit + // DecimalDigits[?Sep] DecimalDigit + // [+Sep] DecimalDigits[+Sep] NumericLiteralSeparator DecimalDigit + if (!parse_decimal_digit()) + return {}; + while (parse_decimal_digit()) + ; + + result = transaction.parsed_string_view(); + + transaction.commit(); + return true; + } + + // https://tc39.es/ecma262/#prod-TemporalDecimalFraction + [[nodiscard]] bool parse_temporal_decimal_fraction(Optional& result) + { + StateTransaction transaction { *this }; + + // TemporalDecimalFraction ::: + // TemporalDecimalSeparator DecimalDigit + // TemporalDecimalSeparator DecimalDigit DecimalDigit + // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit + // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit + // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit + // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit + // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit + // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit + // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit + if (!parse_temporal_decimal_separator()) + return false; + if (!parse_decimal_digit()) + return false; + + for (size_t i = 0; i < 8; ++i) { + if (!parse_decimal_digit()) + break; + } + + result = transaction.parsed_string_view(); + + transaction.commit(); + return true; + } + + // https://tc39.es/ecma262/#prod-ASCIISign + [[nodiscard]] bool parse_ascii_sign() + { + StateTransaction transaction { *this }; + + // ASCIISign : one of + // + - + if (!m_state.lexer.next_is(is_any_of("+-"sv))) + return false; + + m_state.parse_result.sign = m_state.lexer.consume(); + + transaction.commit(); + return true; + } + + // https://tc39.es/ecma262/#prod-TemporalDecimalSeparator + [[nodiscard]] bool parse_temporal_decimal_separator() + { + // TemporalDecimalSeparator ::: one of + // . , + return m_state.lexer.consume_specific('.') || m_state.lexer.consume_specific(','); + } + + // https://tc39.es/proposal-temporal/#prod-DurationDesignator + [[nodiscard]] bool parse_duration_designator() + { + // DurationDesignator : one of + // P p + return m_state.lexer.consume_specific('P') || m_state.lexer.consume_specific('p'); + } + + // https://tc39.es/proposal-temporal/#prod-TimeDesignator + [[nodiscard]] bool parse_time_designator() + { + // TimeDesignator : one of + // T t + return m_state.lexer.consume_specific('T') || m_state.lexer.consume_specific('t'); + } + + // https://tc39.es/proposal-temporal/#prod-YearsDesignator + [[nodiscard]] bool parse_years_designator() + { + // YearsDesignator : one of + // Y y + return m_state.lexer.consume_specific('Y') || m_state.lexer.consume_specific('y'); + } + + // https://tc39.es/proposal-temporal/#prod-MonthsDesignator + [[nodiscard]] bool parse_months_designator() + { + // MonthsDesignator : one of + // M m + return m_state.lexer.consume_specific('M') || m_state.lexer.consume_specific('m'); + } + + // https://tc39.es/proposal-temporal/#prod-WeeksDesignator + [[nodiscard]] bool parse_weeks_designator() + { + // WeeksDesignator : one of + // W w + return m_state.lexer.consume_specific('W') || m_state.lexer.consume_specific('w'); + } + + // https://tc39.es/proposal-temporal/#prod-DaysDesignator + [[nodiscard]] bool parse_days_designator() + { + // DaysDesignator : one of + // D d + return m_state.lexer.consume_specific('D') || m_state.lexer.consume_specific('d'); + } + + // https://tc39.es/proposal-temporal/#prod-HoursDesignator + [[nodiscard]] bool parse_hours_designator() + { + // HoursDesignator : one of + // H h + return m_state.lexer.consume_specific('H') || m_state.lexer.consume_specific('h'); + } + + // https://tc39.es/proposal-temporal/#prod-MinutesDesignator + [[nodiscard]] bool parse_minutes_designator() + { + // MinutesDesignator : one of + // M m + return m_state.lexer.consume_specific('M') || m_state.lexer.consume_specific('m'); + } + + // https://tc39.es/proposal-temporal/#prod-SecondsDesignator + [[nodiscard]] bool parse_seconds_designator() + { + // SecondsDesignator : one of + // S s + return m_state.lexer.consume_specific('S') || m_state.lexer.consume_specific('s'); + } + +private: + struct State { + GenericLexer lexer; + ParseResult parse_result; + }; + + struct StateTransaction { + explicit StateTransaction(ISO8601Parser& parser) + : m_parser(parser) + , m_saved_state(parser.m_state) + , m_start_index(parser.m_state.lexer.tell()) + { + } + + ~StateTransaction() + { + if (!m_commit) + m_parser.m_state = move(m_saved_state); + } + + void commit() { m_commit = true; } + StringView parsed_string_view() const + { + return m_parser.m_input.substring_view(m_start_index, m_parser.m_state.lexer.tell() - m_start_index); + } + + private: + ISO8601Parser& m_parser; + State m_saved_state; + size_t m_start_index { 0 }; + bool m_commit { false }; + }; + + StringView m_input; + State m_state; +}; + +#define JS_ENUMERATE_ISO8601_PRODUCTION_PARSERS \ + __JS_ENUMERATE(TemporalDurationString, parse_temporal_duration_string) + +Optional parse_iso8601(Production production, StringView input) +{ + ISO8601Parser parser { input }; + + switch (production) { +#define __JS_ENUMERATE(ProductionName, parse_production) \ + case Production::ProductionName: \ + if (!parser.parse_production()) \ + return {}; \ + break; + JS_ENUMERATE_ISO8601_PRODUCTION_PARSERS +#undef __JS_ENUMERATE + default: + VERIFY_NOT_REACHED(); + } + + // If we parsed successfully but didn't reach the end, the string doesn't match the given production. + if (!parser.lexer().is_eof()) + return {}; + + return parser.parse_result(); +} + +} diff --git a/Libraries/LibJS/Runtime/Temporal/ISO8601.h b/Libraries/LibJS/Runtime/Temporal/ISO8601.h new file mode 100644 index 00000000000..0396a8661aa --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/ISO8601.h @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021-2022, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace JS::Temporal { + +struct ParseResult { + Optional sign; + Optional duration_years; + Optional duration_months; + Optional duration_weeks; + Optional duration_days; + Optional duration_hours; + Optional duration_hours_fraction; + Optional duration_minutes; + Optional duration_minutes_fraction; + Optional duration_seconds; + Optional duration_seconds_fraction; +}; + +enum class Production { + TemporalDurationString, +}; + +Optional parse_iso8601(Production, StringView); + +} diff --git a/Libraries/LibJS/Runtime/Temporal/Temporal.cpp b/Libraries/LibJS/Runtime/Temporal/Temporal.cpp index c0d97891b9f..1fa7a43ea9e 100644 --- a/Libraries/LibJS/Runtime/Temporal/Temporal.cpp +++ b/Libraries/LibJS/Runtime/Temporal/Temporal.cpp @@ -6,6 +6,7 @@ */ #include +#include #include namespace JS::Temporal { @@ -26,6 +27,9 @@ void Temporal::initialize(Realm& realm) // 1.1.1 Temporal [ %Symbol.toStringTag% ], https://tc39.es/proposal-temporal/#sec-temporal-%symbol.tostringtag% define_direct_property(vm.well_known_symbol_to_string_tag(), PrimitiveString::create(vm, "Temporal"_string), Attribute::Configurable); + + u8 attr = Attribute::Writable | Attribute::Configurable; + define_intrinsic_accessor(vm.names.Duration, attr, [](auto& realm) -> Value { return realm.intrinsics().temporal_duration_constructor(); }); } } diff --git a/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.compare.js b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.compare.js new file mode 100644 index 00000000000..078192885d2 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.compare.js @@ -0,0 +1,95 @@ +describe("correct behavior", () => { + test("length is 2", () => { + expect(Temporal.Duration.compare).toHaveLength(2); + }); + + function checkCommonResults(duration1, duration2) { + expect(Temporal.Duration.compare(duration1, duration1)).toBe(0); + expect(Temporal.Duration.compare(duration2, duration2)).toBe(0); + expect(Temporal.Duration.compare(duration1, duration2)).toBe(-1); + expect(Temporal.Duration.compare(duration2, duration1)).toBe(1); + } + + test("basic functionality", () => { + const duration1 = new Temporal.Duration(0, 0, 0, 1); + const duration2 = new Temporal.Duration(0, 0, 0, 2); + checkCommonResults(duration1, duration2); + }); + + test("duration-like objects", () => { + const duration1 = { years: 0, months: 0, weeks: 0, days: 1 }; + const duration2 = { years: 0, months: 0, weeks: 0, days: 2 }; + checkCommonResults(duration1, duration2); + }); + + test("duration strings", () => { + const duration1 = "P1D"; + const duration2 = "P2D"; + checkCommonResults(duration1, duration2); + }); +}); + +describe("errors", () => { + test("invalid duration-like object", () => { + expect(() => { + Temporal.Duration.compare({}); + }).toThrowWithMessage(TypeError, "Invalid duration-like object"); + + expect(() => { + Temporal.Duration.compare({ years: 0, months: 0, weeks: 0, days: 1 }, {}); + }).toThrowWithMessage(TypeError, "Invalid duration-like object"); + }); + + test("relativeTo is required for comparing calendar units (year, month, week)", () => { + const duration1 = new Temporal.Duration(1); + const duration2 = new Temporal.Duration(2); + + expect(() => { + Temporal.Duration.compare(duration1, duration2); + }).toThrowWithMessage( + RangeError, + "A starting point is required for comparing calendar units" + ); + + const duration3 = new Temporal.Duration(0, 3); + const duration4 = new Temporal.Duration(0, 4); + + expect(() => { + Temporal.Duration.compare(duration3, duration4); + }).toThrowWithMessage( + RangeError, + "A starting point is required for comparing calendar units" + ); + + const duration5 = new Temporal.Duration(0, 0, 5); + const duration6 = new Temporal.Duration(0, 0, 6); + + expect(() => { + Temporal.Duration.compare(duration5, duration6); + }).toThrowWithMessage( + RangeError, + "A starting point is required for comparing calendar units" + ); + + // Still throws if year/month/week of one the duration objects is non-zero. + const duration7 = new Temporal.Duration(0, 0, 0, 7); + const duration8 = new Temporal.Duration(0, 0, 8); + + expect(() => { + Temporal.Duration.compare(duration7, duration8); + }).toThrowWithMessage( + RangeError, + "A starting point is required for comparing calendar units" + ); + + const duration9 = new Temporal.Duration(0, 0, 9); + const duration10 = new Temporal.Duration(0, 0, 0, 10); + + expect(() => { + Temporal.Duration.compare(duration9, duration10); + }).toThrowWithMessage( + RangeError, + "A starting point is required for comparing calendar units" + ); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.from.js b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.from.js new file mode 100644 index 00000000000..bd2e09be7e0 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.from.js @@ -0,0 +1,132 @@ +const expectDurationOneToTen = duration => { + expect(duration.years).toBe(1); + expect(duration.months).toBe(2); + expect(duration.weeks).toBe(3); + expect(duration.days).toBe(4); + expect(duration.hours).toBe(5); + expect(duration.minutes).toBe(6); + expect(duration.seconds).toBe(7); + expect(duration.milliseconds).toBe(8); + expect(duration.microseconds).toBe(9); + expect(duration.nanoseconds).toBe(10); +}; + +describe("correct behavior", () => { + test("length is 1", () => { + expect(Temporal.Duration.from).toHaveLength(1); + }); + + test("Duration instance argument", () => { + const duration = Temporal.Duration.from( + new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + ); + expectDurationOneToTen(duration); + }); + + test("Duration-like object argument", () => { + const duration = Temporal.Duration.from({ + years: 1, + months: 2, + weeks: 3, + days: 4, + hours: 5, + minutes: 6, + seconds: 7, + milliseconds: 8, + microseconds: 9, + nanoseconds: 10, + }); + expectDurationOneToTen(duration); + }); + + test("Duration string argument", () => { + const duration = Temporal.Duration.from("P1Y2M3W4DT5H6M7.008009010S"); + expectDurationOneToTen(duration); + }); +}); + +describe("errors", () => { + test("Invalid duration-like object", () => { + expect(() => { + Temporal.Duration.from({}); + }).toThrowWithMessage(TypeError, "Invalid duration-like object"); + }); + + test("Invalid duration property value", () => { + expect(() => { + Temporal.Duration.from({ years: 1.23 }); + }).toThrowWithMessage( + RangeError, + "Invalid value for duration property 'years': must be an integer, got 1.2" // ...29999999999999 - let's not include that in the test :^) + ); + + expect(() => { + Temporal.Duration.from({ years: "foo" }); + }).toThrowWithMessage( + RangeError, + "Invalid value for duration property 'years': must be an integer, got foo" + ); + + expect(() => { + Temporal.Duration.from({ years: NaN }); + }).toThrowWithMessage( + RangeError, + "Invalid value for duration property 'years': must be an integer, got NaN" + ); + }); + + test("invalid duration string", () => { + expect(() => { + Temporal.Duration.from("foo"); + }).toThrowWithMessage(RangeError, "Invalid duration string 'foo'"); + }); + + test("invalid duration string: fractional hours proceeded by minutes or seconds", () => { + const values = [ + "PT1.23H1M", + "PT1.23H1.23M", + "PT1.23H1S", + "PT1.23H1.23S", + "PT1.23H1M1S", + "PT1.23H1M1.23S", + "PT1.23H1.23M1S", + "PT1.23H1.23M1.23S", + ]; + for (const value of values) { + expect(() => { + Temporal.Duration.from(value); + }).toThrowWithMessage(RangeError, `Invalid duration string '${value}'`); + } + }); + + test("invalid duration string: fractional minutes proceeded by seconds", () => { + const values = ["PT1.23M1S", "PT1.23M1.23S"]; + for (const value of values) { + expect(() => { + Temporal.Duration.from(value); + }).toThrowWithMessage(RangeError, `Invalid duration string '${value}'`); + } + }); + + test("invalid duration string: exceed duration limits", () => { + const values = [ + "P4294967296Y", // abs(years) >= 2**32 + "P4294967296M", // abs(months) >= 2**32 + "P4294967296W", // abs(weeks) >= 2**32 + "P104249991375D", // days >= 2*53 seconds + "PT2501999792984H", // hours >= 2*53 seconds + "PT150119987579017M", // minutes >= 2*53 seconds + "PT9007199254740992S", // seconds >= 2*53 seconds + ]; + + for (const value of values) { + expect(() => { + Temporal.Duration.from(value); + }).toThrowWithMessage(RangeError, `Invalid duration`); + + expect(() => { + Temporal.Duration.from("-" + value); + }).toThrowWithMessage(RangeError, `Invalid duration`); + } + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.js b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.js new file mode 100644 index 00000000000..df1b3c660a0 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.js @@ -0,0 +1,35 @@ +describe("errors", () => { + test("called without new", () => { + expect(() => { + Temporal.Duration(); + }).toThrowWithMessage(TypeError, "Temporal.Duration constructor must be called with 'new'"); + }); + + test("cannot mix arguments with different signs", () => { + expect(() => { + new Temporal.Duration(-1, 1); + }).toThrowWithMessage(RangeError, "Invalid duration"); + expect(() => { + new Temporal.Duration(1, -1); + }).toThrowWithMessage(RangeError, "Invalid duration"); + }); + + test("cannot pass Infinity", () => { + expect(() => { + new Temporal.Duration(Infinity); + }).toThrowWithMessage(RangeError, "Invalid duration"); + }); +}); + +describe("normal behavior", () => { + test("length is 0", () => { + expect(Temporal.Duration).toHaveLength(0); + }); + + test("basic functionality", () => { + const duration = new Temporal.Duration(); + expect(typeof duration).toBe("object"); + expect(duration).toBeInstanceOf(Temporal.Duration); + expect(Object.getPrototypeOf(duration)).toBe(Temporal.Duration.prototype); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.@@toStringTag.js b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.@@toStringTag.js new file mode 100644 index 00000000000..93c0c32a946 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.@@toStringTag.js @@ -0,0 +1,3 @@ +test("basic functionality", () => { + expect(Temporal.Duration.prototype[Symbol.toStringTag]).toBe("Temporal.Duration"); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.days.js b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.days.js new file mode 100644 index 00000000000..6f41661e41f --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.days.js @@ -0,0 +1,14 @@ +describe("correct behavior", () => { + test("basic functionality", () => { + const duration = new Temporal.Duration(0, 0, 0, 123); + expect(duration.days).toBe(123); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.Duration object", () => { + expect(() => { + Reflect.get(Temporal.Duration.prototype, "days", "foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.Duration"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.hours.js b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.hours.js new file mode 100644 index 00000000000..e325bf7060d --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.hours.js @@ -0,0 +1,14 @@ +describe("correct behavior", () => { + test("basic functionality", () => { + const duration = new Temporal.Duration(0, 0, 0, 0, 123); + expect(duration.hours).toBe(123); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.Duration object", () => { + expect(() => { + Reflect.get(Temporal.Duration.prototype, "hours", "foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.Duration"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.microseconds.js b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.microseconds.js new file mode 100644 index 00000000000..5230e2b5591 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.microseconds.js @@ -0,0 +1,14 @@ +describe("correct behavior", () => { + test("basic functionality", () => { + const duration = new Temporal.Duration(0, 0, 0, 0, 0, 0, 0, 0, 123); + expect(duration.microseconds).toBe(123); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.Duration object", () => { + expect(() => { + Reflect.get(Temporal.Duration.prototype, "microseconds", "foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.Duration"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.milliseconds.js b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.milliseconds.js new file mode 100644 index 00000000000..31d90bfda67 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.milliseconds.js @@ -0,0 +1,14 @@ +describe("correct behavior", () => { + test("basic functionality", () => { + const duration = new Temporal.Duration(0, 0, 0, 0, 0, 0, 0, 123); + expect(duration.milliseconds).toBe(123); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.Duration object", () => { + expect(() => { + Reflect.get(Temporal.Duration.prototype, "milliseconds", "foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.Duration"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.minutes.js b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.minutes.js new file mode 100644 index 00000000000..d47b8296b9b --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.minutes.js @@ -0,0 +1,14 @@ +describe("correct behavior", () => { + test("basic functionality", () => { + const duration = new Temporal.Duration(0, 0, 0, 0, 0, 123); + expect(duration.minutes).toBe(123); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.Duration object", () => { + expect(() => { + Reflect.get(Temporal.Duration.prototype, "minutes", "foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.Duration"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.months.js b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.months.js new file mode 100644 index 00000000000..01424ffe945 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.months.js @@ -0,0 +1,14 @@ +describe("correct behavior", () => { + test("basic functionality", () => { + const duration = new Temporal.Duration(0, 123); + expect(duration.months).toBe(123); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.Duration object", () => { + expect(() => { + Reflect.get(Temporal.Duration.prototype, "months", "foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.Duration"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.nanoseconds.js b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.nanoseconds.js new file mode 100644 index 00000000000..d9b5117a8fa --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.nanoseconds.js @@ -0,0 +1,14 @@ +describe("correct behavior", () => { + test("basic functionality", () => { + const duration = new Temporal.Duration(0, 0, 0, 0, 0, 0, 0, 0, 0, 123); + expect(duration.nanoseconds).toBe(123); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.Duration object", () => { + expect(() => { + Reflect.get(Temporal.Duration.prototype, "nanoseconds", "foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.Duration"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.seconds.js b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.seconds.js new file mode 100644 index 00000000000..f15a023e960 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.seconds.js @@ -0,0 +1,14 @@ +describe("correct behavior", () => { + test("basic functionality", () => { + const duration = new Temporal.Duration(0, 0, 0, 0, 0, 0, 123); + expect(duration.seconds).toBe(123); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.Duration object", () => { + expect(() => { + Reflect.get(Temporal.Duration.prototype, "seconds", "foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.Duration"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.weeks.js b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.weeks.js new file mode 100644 index 00000000000..b1c002cf3bd --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.weeks.js @@ -0,0 +1,14 @@ +describe("correct behavior", () => { + test("basic functionality", () => { + const duration = new Temporal.Duration(0, 0, 123); + expect(duration.weeks).toBe(123); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.Duration object", () => { + expect(() => { + Reflect.get(Temporal.Duration.prototype, "weeks", "foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.Duration"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.years.js b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.years.js new file mode 100644 index 00000000000..43175e6037d --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.years.js @@ -0,0 +1,14 @@ +describe("correct behavior", () => { + test("basic functionality", () => { + const duration = new Temporal.Duration(123); + expect(duration.years).toBe(123); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.Duration object", () => { + expect(() => { + Reflect.get(Temporal.Duration.prototype, "years", "foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.Duration"); + }); +});