Bläddra i källkod

LibJS: Implement Temporal.Duration.prototype.with()

Linus Groh 4 år sedan
förälder
incheckning
9aa1e4b885

+ 1 - 0
Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h

@@ -362,6 +362,7 @@ namespace JS {
     P(values)                                \
     P(warn)                                  \
     P(weeks)                                 \
+    P(with)                                  \
     P(writable)                              \
     P(years)
 

+ 1 - 0
Userland/Libraries/LibJS/Runtime/ErrorTypes.h

@@ -168,6 +168,7 @@
     M(StringRepeatCountMustBe, "repeat count must be a {} number")                                                                      \
     M(TemporalInvalidCalendarIdentifier, "Invalid calendar identifier '{}'")                                                            \
     M(TemporalInvalidDuration, "Invalid duration")                                                                                      \
+    M(TemporalInvalidDurationLikeObject, "Invalid duration-like object")                                                                \
     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")                                                                                              \

+ 69 - 0
Userland/Libraries/LibJS/Runtime/Temporal/Duration.cpp

@@ -70,6 +70,75 @@ bool is_valid_duration(double years, double months, double weeks, double days, d
     return true;
 }
 
+// 7.5.6 ToPartialDuration ( temporalDurationLike ), https://tc39.es/proposal-temporal/#sec-temporal-topartialduration
+PartialDuration to_partial_duration(GlobalObject& global_object, Value temporal_duration_like)
+{
+    auto& vm = global_object.vm();
+
+    // 1. If Type(temporalDurationLike) is not Object, then
+    if (!temporal_duration_like.is_object()) {
+        vm.throw_exception<TypeError>(global_object, ErrorType::NotAnObject, temporal_duration_like.to_string_without_side_effects());
+        return {};
+    }
+
+    // 2. Let result be the new Record { [[Years]]: undefined, [[Months]]: undefined, [[Weeks]]: undefined, [[Days]]: undefined, [[Hours]]: undefined, [[Minutes]]: undefined, [[Seconds]]: undefined, [[Milliseconds]]: undefined, [[Microseconds]]: undefined, [[Nanoseconds]]: undefined }.
+    auto result = PartialDuration {};
+
+    // 3. Let any be false.
+    auto any = false;
+
+    struct PartialDurationProperty {
+        Optional<double> PartialDuration::*internal_slot { nullptr };
+        PropertyName property;
+    };
+    auto properties = AK::Array<PartialDurationProperty, 10> {
+        PartialDurationProperty { &PartialDuration::years, vm.names.years },
+        PartialDurationProperty { &PartialDuration::months, vm.names.months },
+        PartialDurationProperty { &PartialDuration::weeks, vm.names.weeks },
+        PartialDurationProperty { &PartialDuration::days, vm.names.days },
+        PartialDurationProperty { &PartialDuration::hours, vm.names.hours },
+        PartialDurationProperty { &PartialDuration::minutes, vm.names.minutes },
+        PartialDurationProperty { &PartialDuration::seconds, vm.names.seconds },
+        PartialDurationProperty { &PartialDuration::milliseconds, vm.names.milliseconds },
+        PartialDurationProperty { &PartialDuration::microseconds, vm.names.microseconds },
+        PartialDurationProperty { &PartialDuration::nanoseconds, vm.names.nanoseconds },
+    };
+
+    // 4. For each row of Table 7, except the header row, in table order, do
+    for (auto& [internal_slot, property] : properties) {
+        // a. Let property be the Property value of the current row.
+
+        // b. Let value be ? Get(temporalDurationLike, property).
+        auto value = temporal_duration_like.as_object().get(property);
+        if (vm.exception())
+            return {};
+
+        // c. If value is not undefined, then
+        if (!value.is_undefined()) {
+            // i. Set any to true.
+            any = true;
+
+            // ii. Set value to ? ToIntegerOrInfinity(value).
+            auto value_number = value.to_integer_or_infinity(global_object);
+            if (vm.exception())
+                return {};
+
+            // iii. Set result's internal slot whose name is the Internal Slot value of the current row to value.
+            result.*internal_slot = value_number;
+        }
+    }
+
+    // 5. If any is false, then
+    if (!any) {
+        // a. Throw a TypeError exception.
+        vm.throw_exception<TypeError>(global_object, ErrorType::TemporalInvalidDurationLikeObject);
+        return {};
+    }
+
+    // 6. Return result.
+    return result;
+}
+
 // 7.5.7 CreateTemporalDuration ( years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds [ , newTarget ] ), https://tc39.es/proposal-temporal/#sec-temporal-createtemporalduration
 Duration* create_temporal_duration(GlobalObject& global_object, double years, double months, double weeks, double days, double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds, FunctionObject* new_target)
 {

+ 15 - 0
Userland/Libraries/LibJS/Runtime/Temporal/Duration.h

@@ -6,6 +6,7 @@
 
 #pragma once
 
+#include <AK/Optional.h>
 #include <LibJS/Runtime/Object.h>
 
 namespace JS::Temporal {
@@ -43,8 +44,22 @@ private:
     double m_nanoseconds;  // [[Nanoseconds]]
 };
 
+struct PartialDuration {
+    Optional<double> years;
+    Optional<double> months;
+    Optional<double> weeks;
+    Optional<double> days;
+    Optional<double> hours;
+    Optional<double> minutes;
+    Optional<double> seconds;
+    Optional<double> milliseconds;
+    Optional<double> microseconds;
+    Optional<double> nanoseconds;
+};
+
 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);
 Duration* create_temporal_duration(GlobalObject&, double years, double months, double weeks, double days, double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds, FunctionObject* new_target = nullptr);
 
 }

+ 79 - 0
Userland/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp

@@ -39,6 +39,7 @@ void DurationPrototype::initialize(GlobalObject& global_object)
     define_native_accessor(vm.names.blank, blank_getter, {}, Attribute::Configurable);
 
     u8 attr = Attribute::Writable | Attribute::Configurable;
+    define_native_function(vm.names.with, with, 1, attr);
     define_native_function(vm.names.valueOf, value_of, 0, attr);
 }
 
@@ -218,6 +219,84 @@ JS_DEFINE_NATIVE_FUNCTION(DurationPrototype::blank_getter)
     return Value(false);
 }
 
+// 7.3.15 Temporal.Duration.prototype.with ( temporalDurationLike ), https://tc39.es/proposal-temporal/#sec-temporal.duration.prototype.with
+JS_DEFINE_NATIVE_FUNCTION(DurationPrototype::with)
+{
+    // 1. Let duration be the this value.
+    // 2. Perform ? RequireInternalSlot(duration, [[InitializedTemporalDuration]]).
+    auto* duration = typed_this(global_object);
+    if (vm.exception())
+        return {};
+
+    // 3. Let temporalDurationLike be ? ToPartialDuration(temporalDurationLike).
+    auto temporal_duration_like = to_partial_duration(global_object, vm.argument(0));
+    if (vm.exception())
+        return {};
+
+    // 4. If temporalDurationLike.[[Years]] is not undefined, then
+    //     a. Let years be temporalDurationLike.[[Years]].
+    // 5. Else,
+    //     a. Let years be duration.[[Years]].
+    auto years = temporal_duration_like.years.value_or(duration->years());
+
+    // 6. If temporalDurationLike.[[Months]] is not undefined, then
+    //     a. Let months be temporalDurationLike.[[Months]].
+    // 7. Else,
+    //     a. Let months be duration.[[Months]].
+    auto months = temporal_duration_like.months.value_or(duration->months());
+
+    // 8. If temporalDurationLike.[[Weeks]] is not undefined, then
+    //     a. Let weeks be temporalDurationLike.[[Weeks]].
+    // 9. Else,
+    //     a. Let weeks be duration.[[Weeks]].
+    auto weeks = temporal_duration_like.weeks.value_or(duration->weeks());
+
+    // 10. If temporalDurationLike.[[Days]] is not undefined, then
+    //     a. Let days be temporalDurationLike.[[Days]].
+    // 11. Else,
+    //     a. Let days be duration.[[Days]].
+    auto days = temporal_duration_like.days.value_or(duration->days());
+
+    // 12. If temporalDurationLike.[[Hours]] is not undefined, then
+    //     a. Let hours be temporalDurationLike.[[Hours]].
+    // 13. Else,
+    //     a. Let hours be duration.[[Hours]].
+    auto hours = temporal_duration_like.hours.value_or(duration->hours());
+
+    // 14. If temporalDurationLike.[[Minutes]] is not undefined, then
+    //     a. Let minutes be temporalDurationLike.[[Minutes]].
+    // 15. Else,
+    //     a. Let minutes be duration.[[Minutes]].
+    auto minutes = temporal_duration_like.minutes.value_or(duration->minutes());
+
+    // 16. If temporalDurationLike.[[Seconds]] is not undefined, then
+    //     a. Let seconds be temporalDurationLike.[[Seconds]].
+    // 17. Else,
+    //     a. Let seconds be duration.[[Seconds]].
+    auto seconds = temporal_duration_like.seconds.value_or(duration->seconds());
+
+    // 18. If temporalDurationLike.[[Milliseconds]] is not undefined, then
+    //     a. Let milliseconds be temporalDurationLike.[[Milliseconds]].
+    // 19. Else,
+    //     a. Let milliseconds be duration.[[Milliseconds]].
+    auto milliseconds = temporal_duration_like.milliseconds.value_or(duration->milliseconds());
+
+    // 20. If temporalDurationLike.[[Microseconds]] is not undefined, then
+    //     a. Let microseconds be temporalDurationLike.[[Microseconds]].
+    // 21. Else,
+    //     a. Let microseconds be duration.[[Microseconds]].
+    auto microseconds = temporal_duration_like.microseconds.value_or(duration->microseconds());
+
+    // 22. If temporalDurationLike.[[Nanoseconds]] is not undefined, then
+    //     a. Let nanoseconds be temporalDurationLike.[[Nanoseconds]].
+    // 23. Else,
+    //     a. Let nanoseconds be duration.[[Nanoseconds]].
+    auto nanoseconds = temporal_duration_like.nanoseconds.value_or(duration->nanoseconds());
+
+    // 24. Return ? CreateTemporalDuration(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds).
+    return create_temporal_duration(global_object, years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds);
+}
+
 // 7.3.25 Temporal.Duration.prototype.valueOf ( ), https://tc39.es/proposal-temporal/#sec-temporal.duration.prototype.valueof
 JS_DEFINE_NATIVE_FUNCTION(DurationPrototype::value_of)
 {

+ 1 - 0
Userland/Libraries/LibJS/Runtime/Temporal/DurationPrototype.h

@@ -31,6 +31,7 @@ private:
     JS_DECLARE_NATIVE_FUNCTION(nanoseconds_getter);
     JS_DECLARE_NATIVE_FUNCTION(sign_getter);
     JS_DECLARE_NATIVE_FUNCTION(blank_getter);
+    JS_DECLARE_NATIVE_FUNCTION(with);
     JS_DECLARE_NATIVE_FUNCTION(value_of);
 };
 

+ 87 - 0
Userland/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.with.js

@@ -0,0 +1,87 @@
+const DURATION_PROPERTIES = [
+    "years",
+    "months",
+    "weeks",
+    "days",
+    "hours",
+    "minutes",
+    "seconds",
+    "milliseconds",
+    "microseconds",
+    "nanoseconds",
+];
+
+describe("correct behavior", () => {
+    test("length is 1", () => {
+        expect(Temporal.Duration.prototype.with).toHaveLength(1);
+    });
+
+    test("basic functionality", () => {
+        const duration = new Temporal.Duration(1, 2, 3).with({ years: 4, foo: 5, weeks: 6 });
+        expect(duration.years).toBe(4);
+        expect(duration.months).toBe(2);
+        expect(duration.weeks).toBe(6);
+    });
+
+    test("each property is looked up from the object", () => {
+        for (const property of DURATION_PROPERTIES) {
+            const duration = new Temporal.Duration().with({ [property]: 1 });
+            expect(duration[property]).toBe(1);
+        }
+    });
+
+    test("each property is coerced to number", () => {
+        for (const property of DURATION_PROPERTIES) {
+            const duration = new Temporal.Duration().with({ [property]: "1" });
+            expect(duration[property]).toBe(1);
+        }
+    });
+});
+
+test("errors", () => {
+    test("this value must be a Temporal.Duration object", () => {
+        expect(() => {
+            Temporal.Duration.prototype.with.call("foo");
+        }).toThrowWithMessage(TypeError, "Not a Temporal.Duration");
+    });
+
+    test("argument is not an object", () => {
+        expect(() => {
+            new Temporal.Duration().with("foo");
+        }).toThrowWithMessage(TypeError, "foo is not an object");
+        expect(() => {
+            new Temporal.Duration().with(42);
+        }).toThrowWithMessage(TypeError, "42 is not an object");
+    });
+
+    test("argument is an invalid duration-like object", () => {
+        expect(() => {
+            new Temporal.Duration().with({});
+        }).toThrowWithMessage(TypeError, "Invalid duration-like object");
+        expect(() => {
+            new Temporal.Duration().with({ foo: 1, bar: 2 });
+        }).toThrowWithMessage(TypeError, "Invalid duration-like object");
+    });
+
+    test("error when coercing property to number", () => {
+        for (const property of DURATION_PROPERTIES) {
+            expect(() => {
+                new Temporal.Duration().with({
+                    [property]: {
+                        valueOf() {
+                            throw new Error();
+                        },
+                    },
+                });
+            }).toThrow(Error);
+        }
+    });
+
+    test("invalid duration value", () => {
+        for (const property of DURATION_PROPERTIES) {
+            expect(() => {
+                new Temporal.Duration().with({ [property]: Infinity });
+            }).toThrowWithMessage(RangeError, "Invalid duration");
+        }
+    });
+});