diff options
author | Luke Wilde <lukew@serenityos.org> | 2021-09-08 18:59:09 +0100 |
---|---|---|
committer | Linus Groh <mail@linusgroh.de> | 2021-09-08 19:57:29 +0100 |
commit | d943b8f1005e4446ffe9bfd84dbd1014025f35d2 (patch) | |
tree | a3e262d580f5afd1a4ede7a0527eaa5c7e4d1012 /Userland/Libraries/LibJS | |
parent | b8d683c5fb57fac120405d31792a363b6f4e0d85 (diff) | |
download | serenity-d943b8f1005e4446ffe9bfd84dbd1014025f35d2.zip |
LibJS: Implement Temporal.PlainTime.prototype.with
Ticks off one box in #8982 and fixes one test262 case.
Diffstat (limited to 'Userland/Libraries/LibJS')
8 files changed, 399 insertions, 0 deletions
diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index dbd64f2660..2a7a2a37eb 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/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") \ diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp index 045fa75282..f314bf0a21 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/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) { diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h index 0245accce7..9581c96394 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h +++ b/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); diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp index e11e714d3a..866b96c185 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp +++ b/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) { diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/PlainTime.h b/Userland/Libraries/LibJS/Runtime/Temporal/PlainTime.h index 80992540f0..aa7b78ff6b 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/PlainTime.h +++ b/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); diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.cpp index 1eb60b0275..c45711b311 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.cpp +++ b/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) { diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.h b/Userland/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.h index d590872bca..9296b4e4c6 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.h +++ b/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); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Temporal/PlainTime/PlainTime.prototype.with.js b/Userland/Libraries/LibJS/Tests/builtins/Temporal/PlainTime/PlainTime.prototype.with.js new file mode 100644 index 0000000000..a0ffa7fb99 --- /dev/null +++ b/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" + ); + }); +}); |