From c3c9ac93d0ff07c56ff041b258776c21bc81fd37 Mon Sep 17 00:00:00 2001 From: Linus Groh Date: Mon, 8 Nov 2021 19:11:36 +0000 Subject: [PATCH] LibJS: Implement Temporal.PlainDate.prototype.with() With one caveat: in the PreparePartialTemporalFields AO I made a change to fix a spec issue that would require the input object to always have a month or monthCode property. This is tracked in https://github.com/tc39/proposal-temporal/issues/1910 and may get accepted as-is, in which case we simply need to remove the NOTE comment. --- Userland/Libraries/LibJS/Runtime/ErrorTypes.h | 1 + .../Runtime/Temporal/AbstractOperations.cpp | 49 ++++++++++++++++ .../Runtime/Temporal/AbstractOperations.h | 1 + .../Runtime/Temporal/PlainDatePrototype.cpp | 44 ++++++++++++++ .../Runtime/Temporal/PlainDatePrototype.h | 1 + .../PlainDate/PlainDate.prototype.with.js | 58 +++++++++++++++++++ 6 files changed, 154 insertions(+) create mode 100644 Userland/Libraries/LibJS/Tests/builtins/Temporal/PlainDate/PlainDate.prototype.with.js diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index 9ef3a79c3d..a26a1cad63 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -225,6 +225,7 @@ M(TemporalInvalidUnitRange, "Invalid unit range, {} is larger than {}") \ M(TemporalInvalidZonedDateTimeOffset, "Invalid offset for the provided date and time in the current time zone") \ M(TemporalMissingOptionsObject, "Required options object is missing or undefined") \ + M(TemporalObjectMustHaveOneOf, "Object must have at least one of the following properties: {}") \ M(TemporalObjectMustNotHave, "Object must not have a defined {} property") \ M(TemporalPropertyMustBeFinite, "Property must not be Infinity") \ M(TemporalPropertyMustBePositiveInteger, "Property must be a positive integer") \ diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp index fa66b72850..c501cb78a8 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp @@ -1260,4 +1260,53 @@ ThrowCompletionOr prepare_temporal_fields(GlobalObject& global_object, return result; } +// 13.49 PreparePartialTemporalFields ( fields, fieldNames ), https://tc39.es/proposal-temporal/#sec-temporal-preparepartialtemporalfields +ThrowCompletionOr prepare_partial_temporal_fields(GlobalObject& global_object, Object const& fields, Vector const& field_names) +{ + auto& vm = global_object.vm(); + + // 1. Assert: Type(fields) is Object. + + // 2. Let result be ! OrdinaryObjectCreate(%Object.prototype%). + auto* result = Object::create(global_object, global_object.object_prototype()); + + // 3. Let any be false. + auto any = false; + + // 4. For each value property of fieldNames, do + for (auto& property : field_names) { + // a. Let value be ? Get(fields, property). + auto value = TRY(fields.get(property)); + + // b. If value is not undefined, then + if (!value.is_undefined()) { + // i. Set any to true. + any = true; + + // ii. If property is in the Property column of Table 13, then + // 1. Let Conversion represent the abstract operation named by the Conversion value of the same row. + // 2. Set value to ? Conversion(value). + if (property.is_one_of("year"sv, "hour"sv, "minute"sv, "second"sv, "millisecond"sv, "microsecond"sv, "nanosecond"sv, "eraYear"sv)) + value = Value(TRY(to_integer_throw_on_infinity(global_object, value, ErrorType::TemporalPropertyMustBeFinite))); + else if (property.is_one_of("month"sv, "day"sv)) + value = Value(TRY(to_positive_integer(global_object, value))); + else if (property.is_one_of("monthCode"sv, "offset"sv, "era"sv)) + value = TRY(value.to_primitive_string(global_object)); + + // NOTE: According to the spec this is step 4c, but I believe that's incorrect. See https://github.com/tc39/proposal-temporal/issues/1910. + // iii. Perform ! CreateDataPropertyOrThrow(result, property, value). + MUST(result->create_data_property_or_throw(property, value)); + } + } + + // 5. If any is false, then + if (!any) { + // a. Throw a TypeError exception. + return vm.throw_completion(global_object, ErrorType::TemporalObjectMustHaveOneOf, String::join(", "sv, field_names)); + } + + // 6. Return result. + return result; +} + } diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h index 6e85ea9e39..ba8c80b80e 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h @@ -126,6 +126,7 @@ ThrowCompletionOr parse_temporal_time_zone_string(GlobalObject ThrowCompletionOr parse_temporal_year_month_string(GlobalObject&, String const& iso_string); ThrowCompletionOr to_positive_integer(GlobalObject&, Value argument); ThrowCompletionOr prepare_temporal_fields(GlobalObject&, Object const& fields, Vector const& field_names, Vector const& required_fields); +ThrowCompletionOr prepare_partial_temporal_fields(GlobalObject&, Object const& fields, Vector const& field_names); // 13.46 ToIntegerThrowOnInfinity ( argument ), https://tc39.es/proposal-temporal/#sec-temporal-tointegerthrowoninfinity template diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDatePrototype.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDatePrototype.cpp index 9118fd3287..932a1d790d 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDatePrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDatePrototype.cpp @@ -58,6 +58,7 @@ void PlainDatePrototype::initialize(GlobalObject& global_object) define_native_function(vm.names.getISOFields, get_iso_fields, 0, attr); define_native_function(vm.names.add, add, 1, attr); define_native_function(vm.names.subtract, subtract, 1, attr); + define_native_function(vm.names.with, with, 1, attr); define_native_function(vm.names.withCalendar, with_calendar, 1, attr); define_native_function(vm.names.equals, equals, 1, attr); define_native_function(vm.names.toPlainDateTime, to_plain_date_time, 0, attr); @@ -378,6 +379,49 @@ JS_DEFINE_NATIVE_FUNCTION(PlainDatePrototype::subtract) return TRY(calendar_date_add(global_object, temporal_date->calendar(), temporal_date, *negated_duration, options)); } +// 3.3.21 Temporal.PlainDate.prototype.with ( temporalDateLike [ , options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plaindate.prototype.with +JS_DEFINE_NATIVE_FUNCTION(PlainDatePrototype::with) +{ + auto temporal_date_like = vm.argument(0); + + // 1. Let temporalDate be the this value. + // 2. Perform ? RequireInternalSlot(temporalDate, [[InitializedTemporalDate]]). + auto* temporal_date = TRY(typed_this_object(global_object)); + + // 3. If Type(temporalDateLike) is not Object, then + if (!temporal_date_like.is_object()) { + // a. Throw a TypeError exception. + return vm.throw_completion(global_object, ErrorType::NotAnObject, temporal_date_like.to_string_without_side_effects()); + } + + // 4. Perform ? RejectObjectWithCalendarOrTimeZone(temporalDateLike). + TRY(reject_object_with_calendar_or_time_zone(global_object, temporal_date_like.as_object())); + + // 5. Let calendar be temporalDate.[[Calendar]]. + auto& calendar = temporal_date->calendar(); + + // 6. Let fieldNames be ? CalendarFields(calendar, « "day", "month", "monthCode", "year" »). + auto field_names = TRY(calendar_fields(global_object, calendar, { "day"sv, "month"sv, "monthCode"sv, "year"sv })); + + // 7. Let partialDate be ? PreparePartialTemporalFields(temporalDateLike, fieldNames). + auto* partial_date = TRY(prepare_partial_temporal_fields(global_object, temporal_date_like.as_object(), field_names)); + + // 8. Set options to ? GetOptionsObject(options). + auto* options = TRY(get_options_object(global_object, vm.argument(1))); + + // 9. Let fields be ? PrepareTemporalFields(temporalDate, fieldNames, «»). + auto* fields = TRY(prepare_temporal_fields(global_object, *temporal_date, field_names, {})); + + // 10. Set fields to ? CalendarMergeFields(calendar, fields, partialDate). + fields = TRY(calendar_merge_fields(global_object, calendar, *fields, *partial_date)); + + // 11. Set fields to ? PrepareTemporalFields(fields, fieldNames, «»). + fields = TRY(prepare_temporal_fields(global_object, *fields, field_names, {})); + + // 12. Return ? DateFromFields(calendar, fields, options). + return TRY(date_from_fields(global_object, calendar, *fields, *options)); +} + // 3.3.22 Temporal.PlainDate.prototype.withCalendar ( calendar ), https://tc39.es/proposal-temporal/#sec-temporal.plaindate.prototype.withcalendar JS_DEFINE_NATIVE_FUNCTION(PlainDatePrototype::with_calendar) { diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDatePrototype.h b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDatePrototype.h index 8d7c6e4fc8..309604b76c 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDatePrototype.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDatePrototype.h @@ -40,6 +40,7 @@ private: JS_DECLARE_NATIVE_FUNCTION(get_iso_fields); JS_DECLARE_NATIVE_FUNCTION(add); JS_DECLARE_NATIVE_FUNCTION(subtract); + JS_DECLARE_NATIVE_FUNCTION(with); JS_DECLARE_NATIVE_FUNCTION(with_calendar); JS_DECLARE_NATIVE_FUNCTION(equals); JS_DECLARE_NATIVE_FUNCTION(to_plain_date_time); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Temporal/PlainDate/PlainDate.prototype.with.js b/Userland/Libraries/LibJS/Tests/builtins/Temporal/PlainDate/PlainDate.prototype.with.js new file mode 100644 index 0000000000..d29c33873c --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Temporal/PlainDate/PlainDate.prototype.with.js @@ -0,0 +1,58 @@ +describe("correct behavior", () => { + test("length is 1", () => { + expect(Temporal.PlainDate.prototype.with).toHaveLength(1); + }); + + test("basic functionality", () => { + const plainDate = new Temporal.PlainDate(1970, 1, 1); + const values = [ + [{ year: 2021 }, new Temporal.PlainDate(2021, 1, 1)], + [{ year: 2021, month: 7 }, new Temporal.PlainDate(2021, 7, 1)], + [{ year: 2021, month: 7, day: 6 }, new Temporal.PlainDate(2021, 7, 6)], + [{ year: 2021, monthCode: "M07", day: 6 }, new Temporal.PlainDate(2021, 7, 6)], + ]; + for (const [arg, expected] of values) { + expect(plainDate.with(arg).equals(expected)).toBeTrue(); + } + + // Supplying the same values doesn't change the date, but still creates a new object + const plainDateLike = { year: plainDate.year, month: plainDate.month, day: plainDate.day }; + expect(plainDate.with(plainDateLike)).not.toBe(plainDate); + expect(plainDate.with(plainDateLike).equals(plainDate)).toBeTrue(); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainDate object", () => { + expect(() => { + Temporal.PlainDate.prototype.with.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainDate"); + }); + + test("argument must be an object", () => { + expect(() => { + new Temporal.PlainDate(1970, 1, 1).with("foo"); + }).toThrowWithMessage(TypeError, "foo is not an object"); + expect(() => { + new Temporal.PlainDate(1970, 1, 1).with(42); + }).toThrowWithMessage(TypeError, "42 is not an object"); + }); + + test("argument must have one of 'day', 'month', 'monthCode', 'year'", () => { + expect(() => { + new Temporal.PlainDate(1970, 1, 1).with({}); + }).toThrowWithMessage( + TypeError, + "Object must have at least one of the following properties: day, month, monthCode, year" + ); + }); + + test("argument must not have 'calendar' or 'timeZone'", () => { + expect(() => { + new Temporal.PlainDate(1970, 1, 1).with({ calendar: {} }); + }).toThrowWithMessage(TypeError, "Object must not have a defined calendar property"); + expect(() => { + new Temporal.PlainDate(1970, 1, 1).with({ timeZone: {} }); + }).toThrowWithMessage(TypeError, "Object must not have a defined timeZone property"); + }); +});