diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.cpp index b5badab3fe..3f226482a9 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.cpp @@ -46,6 +46,7 @@ void PlainTimePrototype::initialize(GlobalObject& global_object) 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.round, round, 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.toZonedDateTime, to_zoned_date_time, 1, attr); @@ -267,6 +268,63 @@ JS_DEFINE_NATIVE_FUNCTION(PlainTimePrototype::with) return TRY(create_temporal_time(global_object, result.hour, result.minute, result.second, result.millisecond, result.microsecond, result.nanosecond)); } +// 4.3.15 Temporal.PlainTime.prototype.round ( options ), https://tc39.es/proposal-temporal/#sec-temporal.plaintime.prototype.round +JS_DEFINE_NATIVE_FUNCTION(PlainTimePrototype::round) +{ + // 1. Let temporalTime be the this value. + // 2. Perform ? RequireInternalSlot(temporalTime, [[InitializedTemporalTime]]). + auto* temporal_time = TRY(typed_this_object(global_object)); + + // 3. If options is undefined, then + if (vm.argument(0).is_undefined()) { + // a. Throw a TypeError exception. + return vm.throw_completion(global_object, ErrorType::TemporalMissingOptionsObject); + } + + // 4. Set options to ? GetOptionsObject(options). + auto* options = TRY(get_options_object(global_object, vm.argument(0))); + + // 5. Let smallestUnit be ? ToSmallestTemporalUnit(options, « "year", "month", "week", "day" », undefined). + auto smallest_unit_value = TRY(to_smallest_temporal_unit(global_object, *options, { "year"sv, "month"sv, "week"sv, "day"sv }, {})); + + // 6. If smallestUnit is undefined, throw a RangeError exception. + if (!smallest_unit_value.has_value()) + return vm.throw_completion(global_object, ErrorType::OptionIsNotValidValue, vm.names.undefined.as_string(), "smallestUnit"); + + // NOTE: At this point smallest_unit_value can only be a string + auto& smallest_unit = *smallest_unit_value; + + // 7. Let roundingMode be ? ToTemporalRoundingMode(options, "halfExpand"). + auto rounding_mode = TRY(to_temporal_rounding_mode(global_object, *options, "halfExpand")); + + double maximum; + + // 8. If smallestUnit is "hour", then + if (smallest_unit == "hour"sv) { + // a. Let maximum be 24. + maximum = 24; + } + // 9. Else if smallestUnit is "minute" or "second", then + else if (smallest_unit == "minute"sv || smallest_unit == "second"sv) { + // a. Let maximum be 60. + maximum = 60; + } + // 10. Else, + else { + // a. Let maximum be 1000. + maximum = 1000; + } + + // 11. Let roundingIncrement be ? ToTemporalRoundingIncrement(options, maximum, false). + auto rounding_increment = TRY(to_temporal_rounding_increment(global_object, *options, maximum, false)); + + // 12. Let result be ! RoundTime(temporalTime.[[ISOHour]], temporalTime.[[ISOMinute]], temporalTime.[[ISOSecond]], temporalTime.[[ISOMillisecond]], temporalTime.[[ISOMicrosecond]], temporalTime.[[ISONanosecond]], roundingIncrement, smallestUnit, roundingMode). + auto result = round_time(temporal_time->iso_hour(), temporal_time->iso_minute(), temporal_time->iso_second(), temporal_time->iso_millisecond(), temporal_time->iso_microsecond(), temporal_time->iso_nanosecond(), rounding_increment, smallest_unit, rounding_mode); + + // 13. Return ? CreateTemporalTime(result.[[Hour]], result.[[Minute]], result.[[Second]], result.[[Millisecond]], result.[[Microsecond]], result.[[Nanosecond]]). + return TRY(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 4f091c508f..60e0e3c31a 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.h @@ -30,6 +30,7 @@ private: JS_DECLARE_NATIVE_FUNCTION(add); JS_DECLARE_NATIVE_FUNCTION(subtract); JS_DECLARE_NATIVE_FUNCTION(with); + JS_DECLARE_NATIVE_FUNCTION(round); JS_DECLARE_NATIVE_FUNCTION(equals); JS_DECLARE_NATIVE_FUNCTION(to_plain_date_time); JS_DECLARE_NATIVE_FUNCTION(to_zoned_date_time); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Temporal/PlainTime/PlainTime.prototype.round.js b/Userland/Libraries/LibJS/Tests/builtins/Temporal/PlainTime/PlainTime.prototype.round.js new file mode 100644 index 0000000000..1300767abb --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Temporal/PlainTime/PlainTime.prototype.round.js @@ -0,0 +1,119 @@ +describe("correct behavior", () => { + test("length is 1", () => { + expect(Temporal.PlainTime.prototype.round).toHaveLength(1); + }); + + test("basic functionality", () => { + const plainTime = new Temporal.PlainTime(18, 15, 9, 100, 200, 300); + + const firstRoundedPlainTime = plainTime.round({ smallestUnit: "minute" }); + expect(firstRoundedPlainTime.hour).toBe(18); + expect(firstRoundedPlainTime.minute).toBe(15); + expect(firstRoundedPlainTime.second).toBe(0); + expect(firstRoundedPlainTime.millisecond).toBe(0); + expect(firstRoundedPlainTime.microsecond).toBe(0); + expect(firstRoundedPlainTime.nanosecond).toBe(0); + + const secondRoundedPlainTime = plainTime.round({ + smallestUnit: "minute", + roundingMode: "ceil", + }); + expect(secondRoundedPlainTime.hour).toBe(18); + expect(secondRoundedPlainTime.minute).toBe(16); + expect(secondRoundedPlainTime.second).toBe(0); + expect(secondRoundedPlainTime.millisecond).toBe(0); + expect(secondRoundedPlainTime.microsecond).toBe(0); + expect(secondRoundedPlainTime.nanosecond).toBe(0); + + const thirdRoundedPlainTime = plainTime.round({ + smallestUnit: "minute", + roundingMode: "ceil", + roundingIncrement: 30, + }); + expect(thirdRoundedPlainTime.hour).toBe(18); + expect(thirdRoundedPlainTime.minute).toBe(30); + expect(thirdRoundedPlainTime.second).toBe(0); + expect(thirdRoundedPlainTime.millisecond).toBe(0); + expect(thirdRoundedPlainTime.microsecond).toBe(0); + expect(thirdRoundedPlainTime.nanosecond).toBe(0); + + const fourthRoundedPlainTime = plainTime.round({ + smallestUnit: "minute", + roundingMode: "floor", + roundingIncrement: 30, + }); + expect(fourthRoundedPlainTime.hour).toBe(18); + expect(fourthRoundedPlainTime.minute).toBe(0); + expect(fourthRoundedPlainTime.second).toBe(0); + expect(fourthRoundedPlainTime.millisecond).toBe(0); + expect(fourthRoundedPlainTime.microsecond).toBe(0); + expect(fourthRoundedPlainTime.nanosecond).toBe(0); + + const fifthRoundedPlainTime = plainTime.round({ + smallestUnit: "hour", + roundingMode: "halfExpand", + roundingIncrement: 4, + }); + expect(fifthRoundedPlainTime.hour).toBe(20); + expect(fifthRoundedPlainTime.minute).toBe(0); + expect(fifthRoundedPlainTime.second).toBe(0); + expect(fifthRoundedPlainTime.millisecond).toBe(0); + expect(fifthRoundedPlainTime.microsecond).toBe(0); + expect(fifthRoundedPlainTime.nanosecond).toBe(0); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainTime object", () => { + expect(() => { + Temporal.PlainTime.prototype.round.call("foo", {}); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainTime"); + }); + + test("missing options object", () => { + expect(() => { + const plainTime = new Temporal.PlainTime(18, 15, 9, 100, 200, 300); + plainTime.round(); + }).toThrowWithMessage(TypeError, "Required options object is missing or undefined"); + }); + + test("invalid rounding mode", () => { + expect(() => { + const plainTime = new Temporal.PlainTime(18, 15, 9, 100, 200, 300); + plainTime.round({ smallestUnit: "second", roundingMode: "serenityOS" }); + }).toThrowWithMessage( + RangeError, + "serenityOS is not a valid value for option roundingMode" + ); + }); + + test("invalid smallest unit", () => { + expect(() => { + const plainTime = new Temporal.PlainTime(18, 15, 9, 100, 200, 300); + plainTime.round({ smallestUnit: "serenityOS" }); + }).toThrowWithMessage( + RangeError, + "serenityOS is not a valid value for option smallestUnit" + ); + }); + + test("increment may not be NaN", () => { + expect(() => { + const plainTime = new Temporal.PlainTime(18, 15, 9, 100, 200, 300); + plainTime.round({ smallestUnit: "second", roundingIncrement: NaN }); + }).toThrowWithMessage(RangeError, "NaN is not a valid value for option roundingIncrement"); + }); + + test("increment may not be smaller than 1 or larger than maximum", () => { + const plainTime = new Temporal.PlainTime(18, 15, 9, 100, 200, 300); + expect(() => { + plainTime.round({ smallestUnit: "second", roundingIncrement: -1 }); + }).toThrowWithMessage(RangeError, "-1 is not a valid value for option roundingIncrement"); + expect(() => { + plainTime.round({ smallestUnit: "second", roundingIncrement: 0 }); + }).toThrowWithMessage(RangeError, "0 is not a valid value for option roundingIncrement"); + expect(() => { + plainTime.round({ smallestUnit: "second", roundingIncrement: Infinity }); + }).toThrowWithMessage(RangeError, "inf is not a valid value for option roundingIncrement"); + }); +});