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.
This commit is contained in:
Linus Groh 2021-11-08 19:11:36 +00:00
parent 310016aee4
commit c3c9ac93d0
6 changed files with 154 additions and 0 deletions

View file

@ -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") \

View file

@ -1260,4 +1260,53 @@ ThrowCompletionOr<Object*> prepare_temporal_fields(GlobalObject& global_object,
return result;
}
// 13.49 PreparePartialTemporalFields ( fields, fieldNames ), https://tc39.es/proposal-temporal/#sec-temporal-preparepartialtemporalfields
ThrowCompletionOr<Object*> prepare_partial_temporal_fields(GlobalObject& global_object, Object const& fields, Vector<String> 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<TypeError>(global_object, ErrorType::TemporalObjectMustHaveOneOf, String::join(", "sv, field_names));
}
// 6. Return result.
return result;
}
}

View file

@ -126,6 +126,7 @@ ThrowCompletionOr<TemporalTimeZone> parse_temporal_time_zone_string(GlobalObject
ThrowCompletionOr<TemporalYearMonth> parse_temporal_year_month_string(GlobalObject&, String const& iso_string);
ThrowCompletionOr<double> to_positive_integer(GlobalObject&, Value argument);
ThrowCompletionOr<Object*> prepare_temporal_fields(GlobalObject&, Object const& fields, Vector<String> const& field_names, Vector<StringView> const& required_fields);
ThrowCompletionOr<Object*> prepare_partial_temporal_fields(GlobalObject&, Object const& fields, Vector<String> const& field_names);
// 13.46 ToIntegerThrowOnInfinity ( argument ), https://tc39.es/proposal-temporal/#sec-temporal-tointegerthrowoninfinity
template<typename... Args>

View file

@ -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<TypeError>(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)
{

View file

@ -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);

View file

@ -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");
});
});