From 5c7788587324dea861c9fcc1db2d1ec3048aeaa6 Mon Sep 17 00:00:00 2001 From: Linus Groh Date: Sun, 18 Jul 2021 21:44:05 +0100 Subject: [PATCH] LibJS: Implement Temporal.Duration.from() ...with ParseTemporalDurationString currently TODO()'d. --- Userland/Libraries/LibJS/Forward.h | 1 + Userland/Libraries/LibJS/Runtime/ErrorTypes.h | 1 + .../Runtime/Temporal/AbstractOperations.cpp | 12 ++ .../Runtime/Temporal/AbstractOperations.h | 3 + .../LibJS/Runtime/Temporal/Duration.cpp | 110 ++++++++++++++++++ .../LibJS/Runtime/Temporal/Duration.h | 17 +++ .../Runtime/Temporal/DurationConstructor.cpp | 20 ++++ .../Runtime/Temporal/DurationConstructor.h | 2 + .../Temporal/Duration/Duration.from.js | 70 +++++++++++ 9 files changed, 236 insertions(+) create mode 100644 Userland/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.from.js diff --git a/Userland/Libraries/LibJS/Forward.h b/Userland/Libraries/LibJS/Forward.h index bf22bb714a..977c8960ed 100644 --- a/Userland/Libraries/LibJS/Forward.h +++ b/Userland/Libraries/LibJS/Forward.h @@ -199,6 +199,7 @@ namespace Temporal { class PrototypeName; JS_ENUMERATE_TEMPORAL_OBJECTS #undef __JS_ENUMERATE +struct TemporalDuration; }; template diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index 0b00e360d7..9f21032df3 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -169,6 +169,7 @@ M(TemporalInvalidCalendarIdentifier, "Invalid calendar identifier '{}'") \ M(TemporalInvalidDuration, "Invalid duration") \ M(TemporalInvalidDurationLikeObject, "Invalid duration-like object") \ + M(TemporalInvalidDurationPropertyValue, "Invalid value for duration property '{}': must be an integer, got {}") \ M(TemporalInvalidEpochNanoseconds, "Invalid epoch nanoseconds value, must be in range -86400 * 10^17 to 86400 * 10^17") \ M(TemporalInvalidISODate, "Invalid ISO date") \ M(TemporalInvalidTime, "Invalid time") \ diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp index 0add7e093b..dafc0302a8 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp @@ -1,5 +1,6 @@ /* * Copyright (c) 2021, Idan Horowitz + * Copyright (c) 2021, Linus Groh * * SPDX-License-Identifier: BSD-2-Clause */ @@ -7,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -301,6 +303,7 @@ Optional parse_iso_date_time(GlobalObject& global_object, [[maybe_u Optional fraction_part; Optional calendar_part; TODO(); + // 3. Let year be the part of isoString produced by the DateYear production. // 4. If the first code unit of year is 0x2212 (MINUS SIGN), replace it with the code unit 0x002D (HYPHEN-MINUS). String normalized_year; @@ -420,10 +423,19 @@ Optional parse_temporal_instant_string(GlobalObject& global_obj return TemporalInstant { .year = result->year, .month = result->month, .day = result->day, .hour = result->hour, .minute = result->minute, .second = result->second, .millisecond = result->millisecond, .microsecond = result->microsecond, .nanosecond = result->nanosecond, .time_zone_offset = move(time_zone_result->offset) }; } +// 13.40 ParseTemporalDurationString ( isoString ), https://tc39.es/proposal-temporal/#sec-temporal-parsetemporaldurationstring +Optional parse_temporal_duration_string(GlobalObject& global_object, String const& iso_string) +{ + (void)global_object; + (void)iso_string; + TODO(); +} + // 13.43 ParseTemporalTimeZoneString ( isoString ), https://tc39.es/proposal-temporal/#sec-temporal-parsetemporaltimezonestring Optional parse_temporal_time_zone_string(GlobalObject& global_object, [[maybe_unused]] String const& iso_string) { auto& vm = global_object.vm(); + // 1. Assert: Type(isoString) is String. // 2. If isoString does not satisfy the syntax of a TemporalTimeZoneString (see 13.33), then diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h index 82f19b84c2..c02ccf196b 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h @@ -1,5 +1,6 @@ /* * Copyright (c) 2021, Idan Horowitz + * Copyright (c) 2021, Linus Groh * * SPDX-License-Identifier: BSD-2-Clause */ @@ -8,6 +9,7 @@ #include #include +#include #include namespace JS::Temporal { @@ -58,6 +60,7 @@ Optional to_smallest_temporal_unit(GlobalObject&, Object& normalized_opt BigInt* round_number_to_increment(GlobalObject&, BigInt const&, u64 increment, String const& rounding_mode); Optional parse_iso_date_time(GlobalObject&, String const& iso_string); Optional parse_temporal_instant_string(GlobalObject&, String const& iso_string); +Optional parse_temporal_duration_string(GlobalObject&, String const& iso_string); Optional parse_temporal_time_zone_string(GlobalObject&, String const& iso_string); } diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/Duration.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/Duration.cpp index cda244512e..5d5cec2d0c 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/Duration.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/Duration.cpp @@ -6,6 +6,8 @@ #include #include +#include +#include #include #include @@ -27,6 +29,114 @@ Duration::Duration(double years, double months, double weeks, double days, doubl { } +// 7.5.1 ToTemporalDuration ( item ), https://tc39.es/proposal-temporal/#sec-temporal-totemporalduration +Duration* to_temporal_duration(GlobalObject& global_object, Value item) +{ + auto& vm = global_object.vm(); + + Optional result; + + // 1. If Type(item) is Object, then + if (item.is_object()) { + // a. If item has an [[InitializedTemporalDuration]] internal slot, then + if (is(item.as_object())) { + // i. Return item. + return &static_cast(item.as_object()); + } + // b. Let result be ? ToTemporalDurationRecord(item). + result = to_temporal_duration_record(global_object, item.as_object()); + if (vm.exception()) + return {}; + } + // 2. Else, + else { + // a. Let string be ? ToString(item). + auto string = item.to_string(global_object); + if (vm.exception()) + return {}; + + // b. Let result be ? ParseTemporalDurationString(string). + result = parse_temporal_duration_string(global_object, string); + if (vm.exception()) + return {}; + } + + // 3. Return ? CreateTemporalDuration(result.[[Years]], result.[[Months]], result.[[Weeks]], result.[[Days]], result.[[Hours]], result.[[Minutes]], result.[[Seconds]], result.[[Milliseconds]], result.[[Microseconds]], result.[[Nanoseconds]]). + return create_temporal_duration(global_object, result->years, result->months, result->weeks, result->days, result->hours, result->minutes, result->seconds, result->milliseconds, result->microseconds, result->nanoseconds); +} + +// 7.5.2 ToTemporalDurationRecord ( temporalDurationLike ), https://tc39.es/proposal-temporal/#sec-temporal-totemporaldurationrecord +TemporalDuration to_temporal_duration_record(GlobalObject& global_object, Object& temporal_duration_like) +{ + auto& vm = global_object.vm(); + + // 1. Assert: Type(temporalDurationLike) is Object. + + // 2. If temporalDurationLike has an [[InitializedTemporalDuration]] internal slot, then + if (is(temporal_duration_like)) { + auto& duration = static_cast(temporal_duration_like); + + // a. Return the Record { [[Years]]: temporalDurationLike.[[Years]], [[Months]]: temporalDurationLike.[[Months]], [[Weeks]]: temporalDurationLike.[[Weeks]], [[Days]]: temporalDurationLike.[[Days]], [[Hours]]: temporalDurationLike.[[Hours]], [[Minutes]]: temporalDurationLike.[[Minutes]], [[Seconds]]: temporalDurationLike.[[Seconds]], [[Milliseconds]]: temporalDurationLike.[[Milliseconds]], [[Microseconds]]: temporalDurationLike.[[Microseconds]], [[Nanoseconds]]: temporalDurationLike.[[Nanoseconds]] }. + return TemporalDuration { .years = duration.years(), .months = duration.months(), .weeks = duration.weeks(), .days = duration.days(), .hours = duration.hours(), .minutes = duration.minutes(), .seconds = duration.seconds(), .milliseconds = duration.milliseconds(), .microseconds = duration.microseconds(), .nanoseconds = duration.nanoseconds() }; + } + + // 3. Let result be a new Record with all the internal slots given in the Internal Slot column in Table 7. + auto result = TemporalDuration {}; + + // 4. Let any be false. + auto any = false; + + // 5. For each row of Table 7, except the header row, in table order, do + for (auto& [internal_slot, property] : temporal_duration_like_properties(vm)) { + // a. Let prop be the Property value of the current row. + + // b. Let val be ? Get(temporalDurationLike, prop). + auto value = temporal_duration_like.get(property); + if (vm.exception()) + return {}; + + // c. If val is not undefined, then + if (!value.is_undefined()) { + // i. Set any to true. + any = true; + } + + // TODO: This is not in the spec but it seems to be implied, and is also what the polyfill does. + // I think the steps d, e, and f should be conditional based on c - otherwise we call ToNumber(undefined), + // get NaN and immediately fail the floor(val) ≠ val check, making the `any` flag pointless. See: + // - https://github.com/tc39/proposal-temporal/blob/4b4dbd427d4b0468a3b064ca7082f25b209923bc/polyfill/lib/ecmascript.mjs#L556-L607 + // - https://github.com/tc39/proposal-temporal/blob/4b4dbd427d4b0468a3b064ca7082f25b209923bc/polyfill/lib/ecmascript.mjs#L876-L893 + else { + continue; + } + + // d. Let val be ? ToNumber(val). + value = value.to_number(global_object); + if (vm.exception()) + return {}; + + // e. If floor(val) ≠ val, then + if (floor(value.as_double()) != value.as_double()) { + // i. Throw a RangeError exception. + vm.throw_exception(global_object, ErrorType::TemporalInvalidDurationPropertyValue, property.as_string(), value.to_string_without_side_effects()); + return {}; + } + + // f. Set result's internal slot whose name is the Internal Slot value of the current row to val. + result.*internal_slot = value.as_double(); + } + + // 6. If any is false, then + if (!any) { + // a. Throw a TypeError exception. + vm.throw_exception(global_object, ErrorType::TemporalInvalidDurationLikeObject); + return {}; + } + + // 7. Return result. + return result; +} + // 7.5.3 DurationSign ( years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds ) i8 duration_sign(double years, double months, double weeks, double days, double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds) { diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/Duration.h b/Userland/Libraries/LibJS/Runtime/Temporal/Duration.h index d492ea3940..1ca1a16d6d 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/Duration.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/Duration.h @@ -44,6 +44,21 @@ private: double m_nanoseconds; // [[Nanoseconds]] }; +// Used by ToTemporalDurationRecord to temporarily hold values +struct TemporalDuration { + double years; + double months; + double weeks; + double days; + double hours; + double minutes; + double seconds; + double milliseconds; + double microseconds; + double nanoseconds; +}; + +// Used by ToPartialDuration to temporarily hold values struct PartialDuration { Optional years; Optional months; @@ -82,6 +97,8 @@ auto temporal_duration_like_properties = [](VM& vm) { }; }; +Duration* to_temporal_duration(GlobalObject&, Value item); +TemporalDuration to_temporal_duration_record(GlobalObject&, Object& temporal_duration_like); i8 duration_sign(double years, double months, double weeks, double days, double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds); bool is_valid_duration(double years, double months, double weeks, double days, double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds); PartialDuration to_partial_duration(GlobalObject&, Value temporal_duration_like); diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/DurationConstructor.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/DurationConstructor.cpp index 41a03f5473..5f565987b7 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/DurationConstructor.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/DurationConstructor.cpp @@ -25,6 +25,9 @@ void DurationConstructor::initialize(GlobalObject& global_object) // 7.2.1 Temporal.Duration.prototype, https://tc39.es/proposal-temporal/#sec-temporal-duration-prototype define_direct_property(vm.names.prototype, global_object.temporal_duration_prototype(), 0); + u8 attr = Attribute::Writable | Attribute::Configurable; + define_native_function(vm.names.from, from, 1, attr); + define_direct_property(vm.names.length, Value(0), Attribute::Configurable); } @@ -99,4 +102,21 @@ Value DurationConstructor::construct(FunctionObject& new_target) return create_temporal_duration(global_object, y, mo, w, d, h, m, s, ms, mis, ns, &new_target); } +// 7.2.2 Temporal.Duration.from ( item ), https://tc39.es/proposal-temporal/#sec-temporal.duration.from +JS_DEFINE_NATIVE_FUNCTION(DurationConstructor::from) +{ + auto item = vm.argument(0); + + // 1. If Type(item) is Object and item has an [[InitializedTemporalDuration]] internal slot, then + if (item.is_object() && is(item.as_object())) { + auto& 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 create_temporal_duration(global_object, duration.years(), duration.months(), duration.weeks(), duration.days(), duration.hours(), duration.minutes(), duration.seconds(), duration.milliseconds(), duration.microseconds(), duration.nanoseconds()); + } + + // 2. Return ? ToTemporalDuration(item). + return to_temporal_duration(global_object, item); +} + } diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/DurationConstructor.h b/Userland/Libraries/LibJS/Runtime/Temporal/DurationConstructor.h index 74871c0c71..73fff2b2f8 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/DurationConstructor.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/DurationConstructor.h @@ -23,6 +23,8 @@ public: private: virtual bool has_constructor() const override { return true; } + + JS_DECLARE_NATIVE_FUNCTION(from); }; } diff --git a/Userland/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.from.js b/Userland/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.from.js new file mode 100644 index 0000000000..91441327a3 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.from.js @@ -0,0 +1,70 @@ +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); + }); + + // Un-skip once ParseTemporalDurationString is implemented + test.skip("Duration string argument", () => { + const duration = Temporal.Duration.from("TODO"); + 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 NaN" + ); + }); +});