Przeglądaj źródła

LibJS: Implement Temporal.PlainTime.prototype.with

Ticks off one box in #8982 and fixes one test262 case.
Luke Wilde 3 lat temu
rodzic
commit
d943b8f100

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

@@ -191,12 +191,14 @@
     M(TemporalInvalidPlainDateTime, "Invalid plain date time")                                                                          \
     M(TemporalInvalidPlainMonthDay, "Invalid plain month day")                                                                          \
     M(TemporalInvalidPlainTime, "Invalid plain time")                                                                                   \
+    M(TemporalInvalidPlainTimeLikeObject, "Invalid plain time-like object")                                                             \
     M(TemporalInvalidPlainYearMonth, "Invalid plain year month")                                                                        \
     M(TemporalInvalidTime, "Invalid time")                                                                                              \
     M(TemporalInvalidTimeZoneName, "Invalid time zone name")                                                                            \
     M(TemporalInvalidUnitRange, "Invalid unit range, {} is larger than {}")                                                             \
     M(TemporalMissingOptionsObject, "Required options object is missing or undefined")                                                  \
     M(TemporalMissingRequiredProperty, "Required property {} is missing or undefined")                                                  \
+    M(TemporalPlainTimeWithArgumentMustNotHave, "Argument must not have a defined {} property")                                         \
     M(TemporalPropertyMustBeFinite, "Property must not be Infinity")                                                                    \
     M(TemporalPropertyMustBePositiveInteger, "Property must be a positive integer")                                                     \
     M(ThisHasNotBeenInitialized, "|this| has not been initialized")                                                                     \

+ 17 - 0
Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp

@@ -7,6 +7,7 @@
 
 #include <AK/CharacterTypes.h>
 #include <AK/DateTimeLexer.h>
+#include <AK/TypeCasts.h>
 #include <AK/Variant.h>
 #include <LibJS/Runtime/IteratorOperations.h>
 #include <LibJS/Runtime/PropertyName.h>
@@ -14,8 +15,10 @@
 #include <LibJS/Runtime/Temporal/Calendar.h>
 #include <LibJS/Runtime/Temporal/Duration.h>
 #include <LibJS/Runtime/Temporal/PlainDate.h>
+#include <LibJS/Runtime/Temporal/PlainDateTime.h>
 #include <LibJS/Runtime/Temporal/PlainTime.h>
 #include <LibJS/Runtime/Temporal/TimeZone.h>
+#include <LibJS/Runtime/Temporal/ZonedDateTime.h>
 
 namespace JS::Temporal {
 
@@ -597,6 +600,20 @@ Optional<u16> maximum_temporal_duration_rounding_increment(StringView unit)
     return 1000;
 }
 
+// 13.26 RejectTemporalCalendarType ( object ), https://tc39.es/proposal-temporal/#sec-temporal-rejecttemporalcalendartype
+void reject_temporal_calendar_type(GlobalObject& global_object, Object& object)
+{
+    auto& vm = global_object.vm();
+
+    // 1. Assert: Type(object) is Object.
+
+    // 2. If object has an [[InitializedTemporalDate]], [[InitializedTemporalDateTime]], [[InitializedTemporalMonthDay]], [[InitializedTemporalTime]], [[InitializedTemporalYearMonth]], or [[InitializedTemporalZonedDateTime]] internal slot, then
+    if (is<PlainDate>(object) || is<PlainDateTime>(object) || is<PlainMonthDay>(object) || is<PlainTime>(object) || is<PlainYearMonth>(object) || is<ZonedDateTime>(object)) {
+        // a. Throw a TypeError exception.
+        vm.throw_exception<TypeError>(global_object, ErrorType::TemporalPlainTimeWithArgumentMustNotHave, "calendar or timeZone");
+    }
+}
+
 // 13.27 FormatSecondsStringPart ( second, millisecond, microsecond, nanosecond, precision ), https://tc39.es/proposal-temporal/#sec-temporal-formatsecondsstringpart
 String format_seconds_string_part(u8 second, u16 millisecond, u16 microsecond, u16 nanosecond, Variant<String, u8> const& precision)
 {

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

@@ -91,6 +91,7 @@ Optional<String> to_smallest_temporal_unit(GlobalObject&, Object& normalized_opt
 void validate_temporal_unit_range(GlobalObject&, String const& largest_unit, String const& smallest_unit);
 String larger_of_two_temporal_units(StringView, StringView);
 Optional<u16> maximum_temporal_duration_rounding_increment(StringView unit);
+void reject_temporal_calendar_type(GlobalObject&, Object&);
 String format_seconds_string_part(u8 second, u16 millisecond, u16 microsecond, u16 nanosecond, Variant<String, u8> const& precision);
 double constrain_to_range(double x, double minimum, double maximum);
 BigInt* round_number_to_increment(GlobalObject&, BigInt const&, u64 increment, StringView rounding_mode);

+ 48 - 0
Userland/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp

@@ -134,6 +134,54 @@ PlainTime* to_temporal_time(GlobalObject& global_object, Value item, Optional<St
     return create_temporal_time(global_object, result->hour, result->minute, result->second, result->millisecond, result->microsecond, result->nanosecond);
 }
 
+// 4.5.3 ToPartialTime ( temporalTimeLike ), https://tc39.es/proposal-temporal/#sec-temporal-topartialtime
+Optional<PartialUnregulatedTemporalTime> to_partial_time(GlobalObject& global_object, Object& temporal_time_like)
+{
+    auto& vm = global_object.vm();
+
+    // 1. Assert: Type(temporalTimeLike) is Object.
+
+    // 2. Let result be the Record { [[Hour]]: undefined, [[Minute]]: undefined, [[Second]]: undefined, [[Millisecond]]: undefined, [[Microsecond]]: undefined, [[Nanosecond]]: undefined }.
+    auto result = PartialUnregulatedTemporalTime {};
+
+    // 3. Let any be false.
+    bool any = false;
+
+    // 4. For each row of Table 3, except the header row, in table order, do
+    for (auto& [internal_slot, property] : temporal_time_like_properties<PartialUnregulatedTemporalTime, Optional<double>>(vm)) {
+        // a. Let property be the Property value of the current row.
+
+        // b. Let value be ? Get(temporalTimeLike, property).
+        auto value = temporal_time_like.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 ? ToIntegerThrowOnInfinity(value).
+            auto value_number = to_integer_throw_on_infinity(global_object, value, ErrorType::TemporalPropertyMustBeFinite);
+            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::TemporalInvalidPlainTimeLikeObject);
+        return {};
+    }
+
+    // 6. Return result.
+    return result;
+}
+
 // 4.5.4 RegulateTime ( hour, minute, second, millisecond, microsecond, nanosecond, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-regulatetime
 Optional<TemporalTime> regulate_time(GlobalObject& global_object, double hour, double minute, double second, double millisecond, double microsecond, double nanosecond, StringView overflow)
 {

+ 10 - 0
Userland/Libraries/LibJS/Runtime/Temporal/PlainTime.h

@@ -60,6 +60,15 @@ struct UnregulatedTemporalTime {
     double nanosecond;
 };
 
+struct PartialUnregulatedTemporalTime {
+    Optional<double> hour;
+    Optional<double> minute;
+    Optional<double> second;
+    Optional<double> millisecond;
+    Optional<double> microsecond;
+    Optional<double> nanosecond;
+};
+
 // Table 3: Properties of a TemporalTimeLike, https://tc39.es/proposal-temporal/#table-temporal-temporaltimelike-properties
 
 template<typename StructT, typename ValueT>
@@ -82,6 +91,7 @@ auto temporal_time_like_properties = [](VM& vm) {
 };
 
 PlainTime* to_temporal_time(GlobalObject&, Value item, Optional<StringView> overflow = {});
+Optional<PartialUnregulatedTemporalTime> to_partial_time(GlobalObject&, Object& temporal_time_like);
 Optional<TemporalTime> regulate_time(GlobalObject&, double hour, double minute, double second, double millisecond, double microsecond, double nanosecond, StringView overflow);
 bool is_valid_time(double hour, double minute, double second, double millisecond, double microsecond, double nanosecond);
 DaysAndTime balance_time(i64 hour, i64 minute, i64 second, i64 millisecond, i64 microsecond, i64 nanosecond);

+ 110 - 0
Userland/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.cpp

@@ -38,6 +38,7 @@ void PlainTimePrototype::initialize(GlobalObject& global_object)
     define_native_accessor(vm.names.nanosecond, nanosecond_getter, {}, Attribute::Configurable);
 
     u8 attr = Attribute::Writable | Attribute::Configurable;
+    define_native_function(vm.names.with, with, 1, attr);
     define_native_function(vm.names.equals, equals, 1, attr);
     define_native_function(vm.names.toPlainDateTime, to_plain_date_time, 1, attr);
     define_native_function(vm.names.getISOFields, get_iso_fields, 0, attr);
@@ -148,6 +149,115 @@ JS_DEFINE_NATIVE_FUNCTION(PlainTimePrototype::nanosecond_getter)
     return Value(temporal_time->iso_nanosecond());
 }
 
+// 4.3.12 Temporal.PlainTime.prototype.with ( temporalTimeLike [ , options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plaintime.prototype.with
+JS_DEFINE_NATIVE_FUNCTION(PlainTimePrototype::with)
+{
+    // 1. Let temporalTime be the this value.
+    // 2. Perform ? RequireInternalSlot(temporalTime, [[InitializedTemporalTime]]).
+    auto* temporal_time = typed_this(global_object);
+    if (vm.exception())
+        return {};
+
+    auto temporal_time_like_argument = vm.argument(0);
+
+    // 3. If Type(temporalTimeLike) is not Object, then
+    if (!temporal_time_like_argument.is_object()) {
+        // a. Throw a TypeError exception.
+        vm.throw_exception<TypeError>(global_object, ErrorType::NotAnObject, temporal_time_like_argument.to_string_without_side_effects());
+        return {};
+    }
+
+    auto& temporal_time_like = temporal_time_like_argument.as_object();
+
+    // 4. Perform ? RejectTemporalCalendarType(temporalTimeLike).
+    reject_temporal_calendar_type(global_object, temporal_time_like);
+    if (vm.exception())
+        return {};
+
+    // 5. Let calendarProperty be ? Get(temporalTimeLike, "calendar").
+    auto calendar_property = temporal_time_like.get(vm.names.calendar);
+    if (vm.exception())
+        return {};
+
+    // 6. If calendarProperty is not undefined, then
+    if (!calendar_property.is_undefined()) {
+        // a. Throw a TypeError exception.
+        vm.throw_exception<TypeError>(global_object, ErrorType::TemporalPlainTimeWithArgumentMustNotHave, "calendar");
+        return {};
+    }
+
+    // 7. Let timeZoneProperty be ? Get(temporalTimeLike, "timeZone").
+    auto time_zone_property = temporal_time_like.get(vm.names.timeZone);
+    if (vm.exception())
+        return {};
+
+    // 8. If timeZoneProperty is not undefined, then
+    if (!time_zone_property.is_undefined()) {
+        // a. Throw a TypeError exception.
+        vm.throw_exception<TypeError>(global_object, ErrorType::TemporalPlainTimeWithArgumentMustNotHave, "timeZone");
+        return {};
+    }
+
+    // 9. Let partialTime be ? ToPartialTime(temporalTimeLike).
+    auto partial_time = to_partial_time(global_object, temporal_time_like);
+    if (vm.exception())
+        return {};
+
+    // 10. Set options to ? GetOptionsObject(options).
+    auto* options = get_options_object(global_object, vm.argument(1));
+    if (vm.exception())
+        return {};
+
+    // 11. Let overflow be ? ToTemporalOverflow(options).
+    auto overflow = to_temporal_overflow(global_object, *options);
+    if (vm.exception())
+        return {};
+
+    // 12. If partialTime.[[Hour]] is not undefined, then
+    //      a. Let hour be partialTime.[[Hour]].
+    // 13. Else,
+    //      a. Let hour be temporalTime.[[ISOHour]].
+    auto hour = partial_time->hour.value_or(temporal_time->iso_hour());
+
+    // 14. If partialTime.[[Minute]] is not undefined, then
+    //      a. Let minute be partialTime.[[Minute]].
+    // 15. Else,
+    //      a. Let minute be temporalTime.[[ISOMinute]].
+    auto minute = partial_time->minute.value_or(temporal_time->iso_minute());
+
+    // 16. If partialTime.[[Second]] is not undefined, then
+    //      a. Let second be partialTime.[[Second]].
+    // 17. Else,
+    //      a. Let second be temporalTime.[[ISOSecond]].
+    auto second = partial_time->second.value_or(temporal_time->iso_second());
+
+    // 18. If partialTime.[[Millisecond]] is not undefined, then
+    //      a. Let millisecond be partialTime.[[Millisecond]].
+    // 19. Else,
+    //      a. Let millisecond be temporalTime.[[ISOMillisecond]].
+    auto millisecond = partial_time->millisecond.value_or(temporal_time->iso_millisecond());
+
+    // 20. If partialTime.[[Microsecond]] is not undefined, then
+    //      a. Let microsecond be partialTime.[[Microsecond]].
+    // 21. Else,
+    //      a. Let microsecond be temporalTime.[[ISOMicrosecond]].
+    auto microsecond = partial_time->microsecond.value_or(temporal_time->iso_microsecond());
+
+    // 22. If partialTime.[[Nanosecond]] is not undefined, then
+    //      a. Let nanosecond be partialTime.[[Nanosecond]].
+    // 23. Else,
+    //      a. Let nanosecond be temporalTime.[[ISONanosecond]].
+    auto nanosecond = partial_time->nanosecond.value_or(temporal_time->iso_nanosecond());
+
+    // 24. Let result be ? RegulateTime(hour, minute, second, millisecond, microsecond, nanosecond, overflow).
+    auto result = regulate_time(global_object, hour, minute, second, millisecond, microsecond, nanosecond, *overflow);
+    if (vm.exception())
+        return {};
+
+    // 25. Return ? CreateTemporalTime(result.[[Hour]], result.[[Minute]], result.[[Second]], result.[[Millisecond]], result.[[Microsecond]], result.[[Nanosecond]]).
+    return create_temporal_time(global_object, result->hour, result->minute, result->second, result->millisecond, result->microsecond, result->nanosecond);
+}
+
 // 4.3.16 Temporal.PlainTime.prototype.equals ( other ), https://tc39.es/proposal-temporal/#sec-temporal.plaintime.prototype.equals
 JS_DEFINE_NATIVE_FUNCTION(PlainTimePrototype::equals)
 {

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

@@ -26,6 +26,7 @@ private:
     JS_DECLARE_NATIVE_FUNCTION(millisecond_getter);
     JS_DECLARE_NATIVE_FUNCTION(microsecond_getter);
     JS_DECLARE_NATIVE_FUNCTION(nanosecond_getter);
+    JS_DECLARE_NATIVE_FUNCTION(with);
     JS_DECLARE_NATIVE_FUNCTION(equals);
     JS_DECLARE_NATIVE_FUNCTION(to_plain_date_time);
     JS_DECLARE_NATIVE_FUNCTION(get_iso_fields);

+ 210 - 0
Userland/Libraries/LibJS/Tests/builtins/Temporal/PlainTime/PlainTime.prototype.with.js

@@ -0,0 +1,210 @@
+const PLAIN_TIME_PROPERTIES = [
+    "hour",
+    "minute",
+    "second",
+    "millisecond",
+    "microsecond",
+    "nanosecond",
+];
+
+const REJECTED_CALENDAR_TYPES_THREE_ARGUMENTS = [
+    Temporal.PlainDate,
+    Temporal.PlainDateTime,
+    Temporal.PlainTime,
+];
+
+const REJECTED_CALENDAR_TYPES_TWO_ARGUMENTS = [Temporal.PlainMonthDay, Temporal.PlainYearMonth];
+
+describe("correct behaviour", () => {
+    test("length is 1", () => {
+        expect(Temporal.PlainTime.prototype.with).toHaveLength(1);
+    });
+
+    test("basic functionality", () => {
+        const plainTime = new Temporal.PlainTime(1, 2, 3).with({ hour: 4, foo: 5, second: 6 });
+        expect(plainTime.hour).toBe(4);
+        expect(plainTime.minute).toBe(2);
+        expect(plainTime.second).toBe(6);
+    });
+
+    test("each property is looked up from the object", () => {
+        for (const property of PLAIN_TIME_PROPERTIES) {
+            const plainTime = new Temporal.PlainTime().with({ [property]: 1 });
+            expect(plainTime[property]).toBe(1);
+        }
+    });
+
+    test("each property is coerced to number", () => {
+        for (const property of PLAIN_TIME_PROPERTIES) {
+            const plainTime = new Temporal.PlainTime().with({ [property]: "1" });
+            expect(plainTime[property]).toBe(1);
+        }
+    });
+
+    test("argument can have a calendar property as long as it's undefined", () => {
+        expect(() => {
+            new Temporal.PlainTime().with({
+                calendar: undefined,
+            });
+        }).not.toThrowWithMessage(TypeError, "Argument must not have a defined calendar property");
+    });
+
+    test("argument can have a timeZone property as long as it's undefined", () => {
+        expect(() => {
+            new Temporal.PlainTime().with({
+                timeZone: undefined,
+            });
+        }).not.toThrowWithMessage(TypeError, "Argument must not have a defined timeZone property");
+    });
+});
+
+describe("errors", () => {
+    test("this value must be a Temporal.PlainTime object", () => {
+        expect(() => {
+            Temporal.PlainTime.prototype.with.call("foo");
+        }).toThrowWithMessage(TypeError, "Not a Temporal.PlainTime object");
+    });
+
+    test("argument is not an object", () => {
+        expect(() => {
+            new Temporal.PlainTime().with("foo");
+        }).toThrowWithMessage(TypeError, "foo is not an object");
+        expect(() => {
+            new Temporal.PlainTime().with(42);
+        }).toThrowWithMessage(TypeError, "42 is not an object");
+    });
+
+    test("options is not an object", () => {
+        expect(() => {
+            new Temporal.PlainTime().with({ hour: 1 }, "foo");
+        }).toThrowWithMessage(TypeError, "Options is not an object");
+        expect(() => {
+            new Temporal.PlainTime().with({ hour: 1 }, 42);
+        }).toThrowWithMessage(TypeError, "Options is not an object");
+    });
+
+    test("invalid overflow option", () => {
+        expect(() => {
+            new Temporal.PlainTime().with({ hour: 1 }, { overflow: "a" });
+        }).toThrowWithMessage(RangeError, "a is not a valid value for option overflow");
+    });
+
+    test("argument is an invalid plain time-like object", () => {
+        expect(() => {
+            new Temporal.PlainTime().with({});
+        }).toThrowWithMessage(TypeError, "Invalid plain time-like object");
+        expect(() => {
+            new Temporal.PlainTime().with({ foo: 1, bar: 2 });
+        }).toThrowWithMessage(TypeError, "Invalid plain time-like object");
+    });
+
+    test("error when coercing property to number", () => {
+        for (const property of PLAIN_TIME_PROPERTIES) {
+            expect(() => {
+                new Temporal.PlainTime().with({
+                    [property]: {
+                        valueOf() {
+                            throw new Error("error occurred");
+                        },
+                    },
+                });
+            }).toThrowWithMessage(Error, "error occurred");
+        }
+    });
+
+    test("property must be finite", () => {
+        for (const property of PLAIN_TIME_PROPERTIES) {
+            expect(() => {
+                new Temporal.PlainTime().with({ [property]: Infinity });
+            }).toThrowWithMessage(RangeError, "Property must not be Infinity");
+            expect(() => {
+                new Temporal.PlainTime().with({ [property]: -Infinity });
+            }).toThrowWithMessage(RangeError, "Property must not be Infinity");
+        }
+    });
+
+    test("error when getting property", () => {
+        for (const property of PLAIN_TIME_PROPERTIES) {
+            expect(() => {
+                new Temporal.PlainTime().with({
+                    get [property]() {
+                        throw new Error("error occurred");
+                    },
+                });
+            }).toThrowWithMessage(Error, "error occurred");
+        }
+    });
+
+    test("argument must not have a defined calendar property", () => {
+        expect(() => {
+            new Temporal.PlainTime().with({
+                calendar: null,
+            });
+        }).toThrowWithMessage(TypeError, "Argument must not have a defined calendar property");
+        expect(() => {
+            new Temporal.PlainTime().with({
+                calendar: 1,
+            });
+        }).toThrowWithMessage(TypeError, "Argument must not have a defined calendar property");
+    });
+
+    test("argument must not have a defined timeZone property", () => {
+        expect(() => {
+            new Temporal.PlainTime().with({
+                timeZone: null,
+            });
+        }).toThrowWithMessage(TypeError, "Argument must not have a defined timeZone property");
+        expect(() => {
+            new Temporal.PlainTime().with({
+                timeZone: 1,
+            });
+        }).toThrowWithMessage(TypeError, "Argument must not have a defined timeZone property");
+    });
+
+    test("error when getting calendar", () => {
+        expect(() => {
+            new Temporal.PlainTime().with({
+                get calendar() {
+                    throw new Error("error occurred");
+                },
+            });
+        }).toThrowWithMessage(Error, "error occurred");
+    });
+
+    test("error when getting timeZone", () => {
+        expect(() => {
+            new Temporal.PlainTime().with({
+                get timeZone() {
+                    throw new Error("error occurred");
+                },
+            });
+        }).toThrowWithMessage(Error, "error occurred");
+    });
+
+    test("rejects calendar types", () => {
+        for (const typeWithCalendar of REJECTED_CALENDAR_TYPES_THREE_ARGUMENTS) {
+            expect(() => {
+                new Temporal.PlainTime().with(new typeWithCalendar(1, 1, 1));
+            }).toThrowWithMessage(
+                TypeError,
+                "Argument must not have a defined calendar or timeZone property"
+            );
+        }
+
+        for (const typeWithCalendar of REJECTED_CALENDAR_TYPES_TWO_ARGUMENTS) {
+            expect(() => {
+                new Temporal.PlainTime().with(new typeWithCalendar(1, 1));
+            }).toThrowWithMessage(
+                TypeError,
+                "Argument must not have a defined calendar or timeZone property"
+            );
+        }
+
+        expect(() => {
+            new Temporal.PlainTime().with(new Temporal.ZonedDateTime(1n, {}));
+        }).toThrowWithMessage(
+            TypeError,
+            "Argument must not have a defined calendar or timeZone property"
+        );
+    });
+});