diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index 20105e7089..fadc7bfca6 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -246,6 +246,7 @@ M(TemporalInvalidZonedDateTimeString, "Invalid zoned date time string '{}'") \ M(TemporalMissingOptionsObject, "Required options object is missing or undefined") \ M(TemporalMissingStartingPoint, "A starting point is required for balancing {}") \ + M(TemporalMissingUnits, "One or both of smallestUnit or largestUnit is required") \ 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") \ diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp index 6e2ae46a40..fe6ffcd734 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp @@ -46,6 +46,7 @@ void DurationPrototype::initialize(GlobalObject& global_object) define_native_function(vm.names.with, with, 1, attr); define_native_function(vm.names.negated, negated, 0, attr); define_native_function(vm.names.abs, abs, 0, attr); + define_native_function(vm.names.round, round, 1, attr); define_native_function(vm.names.total, total, 1, attr); define_native_function(vm.names.toString, to_string, 0, attr); define_native_function(vm.names.toJSON, to_json, 0, attr); @@ -288,6 +289,130 @@ JS_DEFINE_NATIVE_FUNCTION(DurationPrototype::abs) return TRY(create_temporal_duration(global_object, fabs(duration->years()), fabs(duration->months()), fabs(duration->weeks()), fabs(duration->days()), fabs(duration->hours()), fabs(duration->minutes()), fabs(duration->seconds()), fabs(duration->milliseconds()), fabs(duration->microseconds()), fabs(duration->nanoseconds()))); } +// 7.3.20 Temporal.Duration.prototype.round ( roundTo ), https://tc39.es/proposal-temporal/#sec-temporal.duration.prototype.round +JS_DEFINE_NATIVE_FUNCTION(DurationPrototype::round) +{ + // 1. Let duration be the this value. + // 2. Perform ? RequireInternalSlot(duration, [[InitializedTemporalDuration]]). + auto* duration = TRY(typed_this_object(global_object)); + + // 3. If roundTo is undefined, then + if (vm.argument(0).is_undefined()) { + // a. Throw a TypeError exception. + return vm.throw_completion(global_object, ErrorType::TemporalMissingOptionsObject); + } + + Object* round_to; + + // 4. If Type(roundTo) is String, then + if (vm.argument(0).is_string()) { + // a. Let paramString be roundTo. + + // b. Set roundTo to ! OrdinaryObjectCreate(null). + round_to = Object::create(global_object, nullptr); + + // FIXME: "_smallestUnit_" is a spec bug, see https://github.com/tc39/proposal-temporal/pull/1931 + // c. Perform ! CreateDataPropertyOrThrow(roundTo, "_smallestUnit_", paramString). + MUST(round_to->create_data_property_or_throw(vm.names.smallestUnit, vm.argument(0))); + } + // 5. Else, + else { + // a. Set roundTo to ? GetOptionsObject(roundTo). + round_to = TRY(get_options_object(global_object, vm.argument(0))); + } + + // 6. Let smallestUnitPresent be true. + bool smallest_unit_present = true; + + // 7. Let largestUnitPresent be true. + bool largest_unit_present = true; + + // 8. Let smallestUnit be ? ToSmallestTemporalUnit(roundTo, « », undefined). + auto smallest_unit = TRY(to_smallest_temporal_unit(global_object, *round_to, {}, {})); + + // 9. If smallestUnit is undefined, then + if (!smallest_unit.has_value()) { + // a. Set smallestUnitPresent to false. + smallest_unit_present = false; + + // b. Set smallestUnit to "nanosecond". + smallest_unit = "nanosecond"sv; + } + + // 10. Let defaultLargestUnit be ! DefaultTemporalLargestUnit(duration.[[Years]], duration.[[Months]], duration.[[Weeks]], duration.[[Days]], duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]]). + auto default_largest_unit = default_temporal_largest_unit(duration->years(), duration->months(), duration->weeks(), duration->days(), duration->hours(), duration->minutes(), duration->seconds(), duration->milliseconds(), duration->microseconds()); + + // 11. Set defaultLargestUnit to ! LargerOfTwoTemporalUnits(defaultLargestUnit, smallestUnit). + default_largest_unit = larger_of_two_temporal_units(default_largest_unit, *smallest_unit); + + // 12. Let largestUnit be ? ToLargestTemporalUnit(roundTo, « », undefined). + auto largest_unit = TRY(to_largest_temporal_unit(global_object, *round_to, {}, {})); + + // 13. If largestUnit is undefined, then + if (!largest_unit.has_value()) { + // a. Set largestUnitPresent to false. + largest_unit_present = false; + + // b. Set largestUnit to defaultLargestUnit. + largest_unit = default_largest_unit; + } + // 14. Else if largestUnit is "auto", then + else if (*largest_unit == "auto"sv) { + // a. Set largestUnit to defaultLargestUnit. + largest_unit = default_largest_unit; + } + + // 15. If smallestUnitPresent is false and largestUnitPresent is false, then + if (!smallest_unit_present && !largest_unit_present) { + // a. Throw a RangeError exception. + return vm.throw_completion(global_object, ErrorType::TemporalMissingUnits); + } + + // 16. Perform ? ValidateTemporalUnitRange(largestUnit, smallestUnit). + TRY(validate_temporal_unit_range(global_object, *largest_unit, *smallest_unit)); + + // 17. Let roundingMode be ? ToTemporalRoundingMode(roundTo, "halfExpand"). + auto rounding_mode = TRY(to_temporal_rounding_mode(global_object, *round_to, "halfExpand"sv)); + + // 18. Let maximum be ! MaximumTemporalDurationRoundingIncrement(smallestUnit). + auto maximum = maximum_temporal_duration_rounding_increment(*smallest_unit); + + // 19. Let roundingIncrement be ? ToTemporalRoundingIncrement(roundTo, maximum, false). + auto rounding_increment = TRY(to_temporal_rounding_increment(global_object, *round_to, maximum.has_value() ? *maximum : Optional {}, false)); + + // 20. Let relativeTo be ? ToRelativeTemporalObject(roundTo). + auto relative_to = TRY(to_relative_temporal_object(global_object, *round_to)); + + // 21. Let unbalanceResult be ? UnbalanceDurationRelative(duration.[[Years]], duration.[[Months]], duration.[[Weeks]], duration.[[Days]], largestUnit, relativeTo). + auto unbalance_result = TRY(unbalance_duration_relative(global_object, duration->years(), duration->months(), duration->weeks(), duration->days(), *largest_unit, relative_to)); + + // 22. Let roundResult be ? RoundDuration(unbalanceResult.[[Years]], unbalanceResult.[[Months]], unbalanceResult.[[Weeks]], unbalanceResult.[[Days]], duration.[[Hours]], duration.[[Minutes]], duration[[Seconds]], duration[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]], roundingIncrement, smallestUnit, roundingMode, relativeTo). + auto round_result = TRY(round_duration(global_object, unbalance_result.years, unbalance_result.months, unbalance_result.weeks, unbalance_result.days, duration->hours(), duration->minutes(), duration->seconds(), duration->milliseconds(), duration->microseconds(), duration->nanoseconds(), rounding_increment, *smallest_unit, rounding_mode, relative_to.is_object() ? &relative_to.as_object() : nullptr)); + + // 23. Let adjustResult be ? AdjustRoundedDurationDays(roundResult.[[Years]], roundResult.[[Months]], roundResult.[[Weeks]], roundResult.[[Days]], roundResult.[[Hours]], roundResult.[[Minutes]], roundResult.[[Seconds]], roundResult.[[Milliseconds]], roundResult.[[Microseconds]], roundResult.[[Nanoseconds]], roundingIncrement, smallestUnit, roundingMode, relativeTo). + auto adjust_result = TRY(adjust_rounded_duration_days(global_object, round_result.years, round_result.months, round_result.weeks, round_result.days, round_result.hours, round_result.minutes, round_result.seconds, round_result.milliseconds, round_result.microseconds, round_result.nanoseconds, rounding_increment, *smallest_unit, rounding_mode, relative_to.is_object() ? &relative_to.as_object() : nullptr)); + + // 24. Let balanceResult be ? BalanceDurationRelative(adjustResult.[[Years]], adjustResult.[[Months]], adjustResult.[[Weeks]], adjustResult.[[Days]], largestUnit, relativeTo). + auto balance_result = TRY(balance_duration_relative(global_object, adjust_result.years, adjust_result.months, adjust_result.weeks, adjust_result.days, *largest_unit, relative_to)); + + // 25. If relativeTo has an [[InitializedTemporalZonedDateTime]] internal slot, then + // TODO: The spec doesn't check the type of relativeTo here and relativeTo can be undefined. + if (relative_to.is_object() && is(relative_to.as_object())) { + auto& relative_to_zoned_date_time = static_cast(relative_to.as_object()); + + // a. Set relativeTo to ? MoveRelativeZonedDateTime(relativeTo, balanceResult.[[Years]], balanceResult.[[Months]], balanceResult.[[Weeks]], 0). + relative_to = TRY(move_relative_zoned_date_time(global_object, relative_to_zoned_date_time, balance_result.years, balance_result.months, balance_result.weeks, 0)); + } + + // 26. Let result be ? BalanceDuration(balanceResult.[[Days]], adjustResult.[[Hours]], adjustResult.[[Minutes]], adjustResult.[[Seconds]], adjustResult.[[Milliseconds]], adjustResult.[[Microseconds]], adjustResult.[[Nanoseconds]], largestUnit, relativeTo). + // FIXME: Narrowing conversion from 'double' to 'i64' + auto* adjust_result_nanoseconds = js_bigint(vm, Crypto::SignedBigInteger::create_from(adjust_result.nanoseconds)); + auto result = TRY(balance_duration(global_object, balance_result.days, adjust_result.hours, adjust_result.minutes, adjust_result.seconds, adjust_result.milliseconds, adjust_result.microseconds, *adjust_result_nanoseconds, *largest_unit, relative_to.is_object() ? &relative_to.as_object() : nullptr)); + + // 27. Return ? CreateTemporalDuration(balanceResult.[[Years]], balanceResult.[[Months]], balanceResult.[[Weeks]], result.[[Days]], result.[[Hours]], result.[[Minutes]], result.[[Seconds]], result.[[Milliseconds]], result.[[Microseconds]], result.[[Nanoseconds]]). + return TRY(create_temporal_duration(global_object, balance_result.years, balance_result.months, balance_result.weeks, result.days, result.hours, result.minutes, result.seconds, result.milliseconds, result.microseconds, result.nanoseconds)); +} + // 7.3.21 Temporal.Duration.prototype.total ( totalOf ), https://tc39.es/proposal-temporal/#sec-temporal.duration.prototype.total JS_DEFINE_NATIVE_FUNCTION(DurationPrototype::total) { diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/DurationPrototype.h b/Userland/Libraries/LibJS/Runtime/Temporal/DurationPrototype.h index 51354d0848..94d2304cf7 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/DurationPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/DurationPrototype.h @@ -35,6 +35,7 @@ private: JS_DECLARE_NATIVE_FUNCTION(with); JS_DECLARE_NATIVE_FUNCTION(negated); JS_DECLARE_NATIVE_FUNCTION(abs); + JS_DECLARE_NATIVE_FUNCTION(round); JS_DECLARE_NATIVE_FUNCTION(total); JS_DECLARE_NATIVE_FUNCTION(to_string); JS_DECLARE_NATIVE_FUNCTION(to_json); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.round.js b/Userland/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.round.js new file mode 100644 index 0000000000..c9df9501e8 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.round.js @@ -0,0 +1,164 @@ +describe("correct behavior", () => { + test("length is 1", () => { + expect(Temporal.Duration.prototype.round).toHaveLength(1); + }); + + test("basic functionality", () => { + const duration = new Temporal.Duration(0, 0, 0, 21, 7, 10, 100, 200, 300, 400); + const values = [ + ["nanosecond", "P21DT7H11M40.2003004S"], + ["microsecond", "P21DT7H11M40.2003S"], + ["millisecond", "P21DT7H11M40.2S"], + ["second", "P21DT7H11M40S"], + ["minute", "P21DT7H12M"], + ["hour", "P21DT7H"], + ["day", "P21D"], + ]; + + for (const [smallestUnit, durationString] of values) { + const singularRoundedDuration = duration.round({ smallestUnit }); + const pluralRoundedDuration = duration.round({ smallestUnit: `${smallestUnit}s` }); + + // Passing in a string is treated as though { smallestUnit: "" } was passed in. + const singularRoundedDurationWithString = duration.round(smallestUnit); + const pluralRoundedDurationWithString = duration.round(`${smallestUnit}s`); + + expect(singularRoundedDuration.toString()).toBe(durationString); + expect(singularRoundedDurationWithString.toString()).toBe(durationString); + expect(pluralRoundedDuration.toString()).toBe(durationString); + expect(pluralRoundedDurationWithString.toString()).toBe(durationString); + } + }); + + test("largestUnit option", () => { + const duration = new Temporal.Duration(0, 0, 0, 21, 7, 10, 100, 200, 300, 400); + + // Using strings is not sufficient here, for example, the nanosecond case will produce "PT1840300.2003004S" which is 1840300 s, 200 ms, 300 us, 400 ns + const values = [ + ["nanosecond", { nanoseconds: 1840300200300400 }], + ["microsecond", { microseconds: 1840300200300, nanoseconds: 400 }], + ["millisecond", { milliseconds: 1840300200, microseconds: 300, nanoseconds: 400 }], + [ + "second", + { seconds: 1840300, milliseconds: 200, microseconds: 300, nanoseconds: 400 }, + ], + [ + "minute", + { + minutes: 30671, + seconds: 40, + milliseconds: 200, + microseconds: 300, + nanoseconds: 400, + }, + ], + [ + "hour", + { + hours: 511, + minutes: 11, + seconds: 40, + milliseconds: 200, + microseconds: 300, + nanoseconds: 400, + }, + ], + [ + "day", + { + days: 21, + hours: 7, + minutes: 11, + seconds: 40, + milliseconds: 200, + microseconds: 300, + nanoseconds: 400, + }, + ], + ]; + + for (const [largestUnit, durationLike] of values) { + const singularRoundedDuration = duration.round({ largestUnit }); + const pluralRoundedDuration = duration.round({ largestUnit: `${largestUnit}s` }); + + const propertiesToCheck = Object.keys(durationLike); + + for (const property of propertiesToCheck) { + expect(singularRoundedDuration[property]).toBe(durationLike[property]); + expect(pluralRoundedDuration[property]).toBe(durationLike[property]); + } + } + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.Duration object", () => { + expect(() => { + Temporal.Duration.prototype.round.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.Duration"); + }); + + test("missing options object", () => { + const duration = new Temporal.Duration(1); + expect(() => { + duration.round(); + }).toThrowWithMessage(TypeError, "Required options object is missing or undefined"); + }); + + test("invalid rounding mode", () => { + const duration = new Temporal.Duration(1); + expect(() => { + duration.round({ smallestUnit: "second", roundingMode: "serenityOS" }); + }).toThrowWithMessage( + RangeError, + "serenityOS is not a valid value for option roundingMode" + ); + }); + + test("invalid smallest unit", () => { + const duration = new Temporal.Duration(1); + expect(() => { + duration.round({ smallestUnit: "serenityOS" }); + }).toThrowWithMessage( + RangeError, + "serenityOS is not a valid value for option smallestUnit" + ); + }); + + test("increment may not be NaN", () => { + const duration = new Temporal.Duration(1); + expect(() => { + duration.round({ smallestUnit: "second", roundingIncrement: NaN }); + }).toThrowWithMessage(RangeError, "NaN is not a valid value for option roundingIncrement"); + }); + + test("increment may smaller than 1 or larger than maximum", () => { + const duration = new Temporal.Duration(1); + expect(() => { + duration.round({ smallestUnit: "second", roundingIncrement: -1 }); + }).toThrowWithMessage(RangeError, "-1 is not a valid value for option roundingIncrement"); + expect(() => { + duration.round({ smallestUnit: "second", roundingIncrement: 0 }); + }).toThrowWithMessage(RangeError, "0 is not a valid value for option roundingIncrement"); + expect(() => { + duration.round({ smallestUnit: "second", roundingIncrement: Infinity }); + }).toThrowWithMessage(RangeError, "inf is not a valid value for option roundingIncrement"); + }); + + test("must provide one or both of smallestUnit or largestUnit", () => { + const duration = new Temporal.Duration(1); + expect(() => { + duration.round({}); + }).toThrowWithMessage(RangeError, "One or both of smallestUnit or largestUnit is required"); + }); + + test("relativeTo is required when duration has calendar units", () => { + const duration = new Temporal.Duration(1); + expect(() => { + duration.round({ largestUnit: "second" }); + }).toThrowWithMessage( + RangeError, + "A starting point is required for balancing calendar units" + ); + }); +});