diff --git a/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.cpp b/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.cpp index 1eb43e8bcd6..ad4facd86d7 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.cpp @@ -38,6 +38,7 @@ void PlainTimePrototype::initialize(Realm& realm) u8 attr = Attribute::Writable | Attribute::Configurable; define_native_function(realm, vm.names.add, add, 1, attr); define_native_function(realm, vm.names.subtract, subtract, 1, attr); + define_native_function(realm, vm.names.with, with, 1, attr); define_native_function(realm, vm.names.until, until, 1, attr); define_native_function(realm, vm.names.since, since, 1, attr); define_native_function(realm, vm.names.toString, to_string, 0, attr); @@ -98,6 +99,72 @@ JS_DEFINE_NATIVE_FUNCTION(PlainTimePrototype::subtract) return TRY(add_duration_to_time(vm, ArithmeticOperation::Subtract, temporal_time, temporal_duration_like)); } +// 4.3.11 Temporal.PlainTime.prototype.with ( temporalTimeLike [ , options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plaintime.prototype.with +JS_DEFINE_NATIVE_FUNCTION(PlainTimePrototype::with) +{ + auto temporal_time_like = vm.argument(0); + auto options = vm.argument(1); + + // 1. Let temporalTime be the this value. + // 2. Perform ? RequireInternalSlot(temporalTime, [[InitializedTemporalTime]]). + auto temporal_time = TRY(typed_this_object(vm)); + + // 3. If ? IsPartialTemporalObject(temporalTimeLike) is false, throw a TypeError exception. + if (!TRY(is_partial_temporal_object(vm, temporal_time_like))) + return vm.throw_completion(ErrorType::TemporalObjectMustBePartialTemporalObject); + + // 4. Let partialTime be ? ToTemporalTimeRecord(temporalTimeLike, PARTIAL). + auto partial_time = TRY(to_temporal_time_record(vm, temporal_time_like.as_object(), Completeness::Partial)); + + // 5. If partialTime.[[Hour]] is not undefined, then + // a. Let hour be partialTime.[[Hour]]. + // 6. Else, + // a. Let hour be temporalTime.[[Time]].[[Hour]]. + auto hour = partial_time.hour.value_or(temporal_time->time().hour); + + // 7. If partialTime.[[Minute]] is not undefined, then + // a. Let minute be partialTime.[[Minute]]. + // 8. Else, + // a. Let minute be temporalTime.[[Time]].[[Minute]]. + auto minute = partial_time.minute.value_or(temporal_time->time().minute); + + // 9. If partialTime.[[Second]] is not undefined, then + // a. Let second be partialTime.[[Second]]. + // 10. Else, + // a. Let second be temporalTime.[[Time]].[[Second]]. + auto second = partial_time.second.value_or(temporal_time->time().second); + + // 11. If partialTime.[[Millisecond]] is not undefined, then + // a. Let millisecond be partialTime.[[Millisecond]]. + // 12. Else, + // a. Let millisecond be temporalTime.[[Time]].[[Millisecond]]. + auto millisecond = partial_time.millisecond.value_or(temporal_time->time().millisecond); + + // 13. If partialTime.[[Microsecond]] is not undefined, then + // a. Let microsecond be partialTime.[[Microsecond]]. + // 14. Else, + // a. Let microsecond be temporalTime.[[Time]].[[Microsecond]]. + auto microsecond = partial_time.microsecond.value_or(temporal_time->time().microsecond); + + // 15. If partialTime.[[Nanosecond]] is not undefined, then + // a. Let nanosecond be partialTime.[[Nanosecond]]. + // 16. Else, + // a. Let nanosecond be temporalTime.[[Time]].[[Nanosecond]]. + auto nanosecond = partial_time.nanosecond.value_or(temporal_time->time().nanosecond); + + // 17. Let resolvedOptions be ? GetOptionsObject(options). + auto resolved_options = TRY(get_options_object(vm, options)); + + // 18. Let overflow be ? GetTemporalOverflowOption(resolvedOptions). + auto overflow = TRY(get_temporal_overflow_option(vm, resolved_options)); + + // 19. Let result be ? RegulateTime(hour, minute, second, millisecond, microsecond, nanosecond, overflow). + auto result = TRY(regulate_time(vm, hour, minute, second, millisecond, microsecond, nanosecond, overflow)); + + // 20. Return ! CreateTemporalTime(result). + return MUST(create_temporal_time(vm, result)); +} + // 4.3.12 Temporal.PlainTime.prototype.until ( other [ , options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plaintime.prototype.until JS_DEFINE_NATIVE_FUNCTION(PlainTimePrototype::until) { diff --git a/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.h b/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.h index 8f7492ec277..ae2c19d525a 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.h +++ b/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.h @@ -31,6 +31,7 @@ private: JS_DECLARE_NATIVE_FUNCTION(nanosecond_getter); JS_DECLARE_NATIVE_FUNCTION(add); JS_DECLARE_NATIVE_FUNCTION(subtract); + JS_DECLARE_NATIVE_FUNCTION(with); JS_DECLARE_NATIVE_FUNCTION(until); JS_DECLARE_NATIVE_FUNCTION(since); JS_DECLARE_NATIVE_FUNCTION(to_string); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainTime/PlainTime.prototype.with.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainTime/PlainTime.prototype.with.js new file mode 100644 index 00000000000..0856941899e --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainTime/PlainTime.prototype.with.js @@ -0,0 +1,203 @@ +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 behavior", () => { + 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, "Object must be a partial Temporal object"); + }); + + test("argument can have a timeZone property as long as it's undefined", () => { + expect(() => { + new Temporal.PlainTime().with({ + timeZone: undefined, + }); + }).not.toThrowWithMessage(TypeError, "Object must be a partial Temporal object"); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainTime object", () => { + expect(() => { + Temporal.PlainTime.prototype.with.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainTime"); + }); + + test("argument is not an object", () => { + expect(() => { + new Temporal.PlainTime().with("foo"); + }).toThrowWithMessage(TypeError, "Object must be a partial Temporal object"); + expect(() => { + new Temporal.PlainTime().with(42); + }).toThrowWithMessage(TypeError, "Object must be a partial Temporal 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 time"); + expect(() => { + new Temporal.PlainTime().with({ foo: 1, bar: 2 }); + }).toThrowWithMessage(TypeError, "Invalid time"); + }); + + 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, + `Invalid value Infinity for time field '${property}'` + ); + expect(() => { + new Temporal.PlainTime().with({ [property]: -Infinity }); + }).toThrowWithMessage( + RangeError, + `Invalid value -Infinity for time field '${property}'` + ); + } + }); + + 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, "Object must be a partial Temporal object"); + expect(() => { + new Temporal.PlainTime().with({ + calendar: 1, + }); + }).toThrowWithMessage(TypeError, "Object must be a partial Temporal object"); + }); + + test("argument must not have a defined timeZone property", () => { + expect(() => { + new Temporal.PlainTime().with({ + timeZone: null, + }); + }).toThrowWithMessage(TypeError, "Object must be a partial Temporal object"); + expect(() => { + new Temporal.PlainTime().with({ + timeZone: 1, + }); + }).toThrowWithMessage(TypeError, "Object must be a partial Temporal object"); + }); + + 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, "Object must be a partial Temporal object"); + } + + for (const typeWithCalendar of REJECTED_CALENDAR_TYPES_TWO_ARGUMENTS) { + expect(() => { + new Temporal.PlainTime().with(new typeWithCalendar(1, 1)); + }).toThrowWithMessage(TypeError, "Object must be a partial Temporal object"); + } + }); +});