Quellcode durchsuchen

LibJS: Implement Temporal.Duration.prototype.round

Until we have re-implemented Temporal.PlainDate/ZonedDateTime, some of
Temporal.Duration.prototype.round (and its invoked AOs) are left
unimplemented.
Timothy Flynn vor 8 Monaten
Ursprung
Commit
5689621c2b

+ 62 - 0
Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp

@@ -43,6 +43,39 @@ StringView temporal_unit_to_string(Unit unit)
     return temporal_units[to_underlying(unit)].singular_property_name;
 }
 
+// 13.14 ValidateTemporalRoundingIncrement ( increment, dividend, inclusive ), https://tc39.es/proposal-temporal/#sec-validatetemporalroundingincrement
+ThrowCompletionOr<void> validate_temporal_rounding_increment(VM& vm, u64 increment, u64 dividend, bool inclusive)
+{
+    u64 maximum = 0;
+
+    // 1. If inclusive is true, then
+    if (inclusive) {
+        // a. Let maximum be dividend.
+        maximum = dividend;
+    }
+    // 2. Else,
+    else {
+        // a. Assert: dividend > 1.
+        VERIFY(dividend > 1);
+
+        // b. Let maximum be dividend - 1.
+        maximum = dividend - 1;
+    }
+
+    // 3. If increment > maximum, throw a RangeError exception.
+    if (increment > maximum)
+        return vm.throw_completion<RangeError>(ErrorType::OptionIsNotValidValue, increment, "roundingIncrement");
+
+    // 5. If dividend modulo increment ≠ 0, then
+    if (modulo(dividend, increment) != 0) {
+        // a. Throw a RangeError exception.
+        return vm.throw_completion<RangeError>(ErrorType::OptionIsNotValidValue, increment, "roundingIncrement");
+    }
+
+    // 6. Return UNUSED.
+    return {};
+}
+
 // 13.15 GetTemporalFractionalSecondDigitsOption ( options ), https://tc39.es/proposal-temporal/#sec-temporal-gettemporalfractionalseconddigitsoption
 ThrowCompletionOr<Precision> get_temporal_fractional_second_digits_option(VM& vm, Object const& options)
 {
@@ -324,6 +357,14 @@ UnitCategory temporal_unit_category(Unit unit)
     return temporal_units[to_underlying(unit)].category;
 }
 
+// 13.22 MaximumTemporalDurationRoundingIncrement ( unit ), https://tc39.es/proposal-temporal/#sec-temporal-maximumtemporaldurationroundingincrement
+RoundingIncrement maximum_temporal_duration_rounding_increment(Unit unit)
+{
+    // 1. Return the value from the "Maximum duration rounding increment" column of the row of Table 21 in which unit is
+    //    in the "Value" column.
+    return temporal_units[to_underlying(unit)].maximum_duration_rounding_increment;
+}
+
 // AD-HOC
 Crypto::UnsignedBigInteger const& temporal_unit_length_in_nanoseconds(Unit unit)
 {
@@ -930,4 +971,25 @@ ThrowCompletionOr<RoundingMode> get_rounding_mode_option(VM& vm, Object const& o
     return static_cast<RoundingMode>(allowed_strings.first_index_of(string_value.as_string().utf8_string_view()).value());
 }
 
+// 14.4.1.4 GetRoundingIncrementOption ( options ), https://tc39.es/proposal-temporal/#sec-temporal-getroundingincrementoption
+ThrowCompletionOr<u64> get_rounding_increment_option(VM& vm, Object const& options)
+{
+    // 1. Let value be ? Get(options, "roundingIncrement").
+    auto value = TRY(options.get(vm.names.roundingIncrement));
+
+    // 2. If value is undefined, return 1𝔽.
+    if (value.is_undefined())
+        return 1;
+
+    // 3. Let integerIncrement be ? ToIntegerWithTruncation(value).
+    auto integer_increment = TRY(to_integer_with_truncation(vm, value, ErrorType::OptionIsNotValidValue, value, "roundingIncrement"sv));
+
+    // 4. If integerIncrement < 1 or integerIncrement > 10**9, throw a RangeError exception.
+    if (integer_increment < 1 || integer_increment > 1'000'000'000u)
+        return vm.throw_completion<RangeError>(ErrorType::OptionIsNotValidValue, value, "roundingIncrement");
+
+    // 5. Return integerIncrement.
+    return static_cast<u64>(integer_increment);
+}
+
 }

+ 3 - 0
Libraries/LibJS/Runtime/Temporal/AbstractOperations.h

@@ -103,6 +103,7 @@ struct RelativeTo {
     GC::Ptr<JS::Object> zoned_relative_to; // [[ZonedRelativeTo]]
 };
 
+ThrowCompletionOr<void> validate_temporal_rounding_increment(VM&, u64 increment, u64 dividend, bool inclusive);
 ThrowCompletionOr<Precision> get_temporal_fractional_second_digits_option(VM&, Object const& options);
 SecondsStringPrecision to_seconds_string_precision_record(UnitValue, Precision);
 ThrowCompletionOr<UnitValue> get_temporal_unit_valued_option(VM&, Object const& options, PropertyKey const&, UnitGroup, UnitDefault const&, ReadonlySpan<UnitValue> extra_values = {});
@@ -110,6 +111,7 @@ ThrowCompletionOr<RelativeTo> get_temporal_relative_to_option(VM&, Object const&
 Unit larger_of_two_temporal_units(Unit, Unit);
 bool is_calendar_unit(Unit);
 UnitCategory temporal_unit_category(Unit);
+RoundingIncrement maximum_temporal_duration_rounding_increment(Unit);
 Crypto::UnsignedBigInteger const& temporal_unit_length_in_nanoseconds(Unit);
 String format_fractional_seconds(u64, Precision);
 UnsignedRoundingMode get_unsigned_rounding_mode(RoundingMode, Sign);
@@ -183,5 +185,6 @@ ThrowCompletionOr<Value> get_option(VM& vm, Object const& options, PropertyKey c
 }
 
 ThrowCompletionOr<RoundingMode> get_rounding_mode_option(VM&, Object const& options, RoundingMode fallback);
+ThrowCompletionOr<u64> get_rounding_increment_option(VM&, Object const& options);
 
 }

+ 16 - 0
Libraries/LibJS/Runtime/Temporal/Duration.cpp

@@ -9,6 +9,7 @@
 
 #include <AK/Math.h>
 #include <AK/NumericLimits.h>
+#include <LibCrypto/BigFraction/BigFraction.h>
 #include <LibJS/Runtime/AbstractOperations.h>
 #include <LibJS/Runtime/Intrinsics.h>
 #include <LibJS/Runtime/Realm.h>
@@ -740,6 +741,21 @@ ThrowCompletionOr<TimeDuration> round_time_duration(VM& vm, TimeDuration const&
     return TRY(round_time_duration_to_increment(vm, time_duration, divisor.multiplied_by(increment), rounding_mode));
 }
 
+// 7.5.31 TotalTimeDuration ( timeDuration, unit ), https://tc39.es/proposal-temporal/#sec-temporal-totaltimeduration
+double total_time_duration(TimeDuration const& time_duration, Unit unit)
+{
+    // 1. Let divisor be the value in the "Length in Nanoseconds" column of the row of Table 21 whose "Value" column contains unit.
+    auto const& divisor = temporal_unit_length_in_nanoseconds(unit);
+
+    // 2. NOTE: The following step cannot be implemented directly using floating-point arithmetic when 𝔽(timeDuration) is
+    //    not a safe integer. The division can be implemented in C++ with the __float128 type if the compiler supports it,
+    //    or with software emulation such as in the SoftFP library.
+
+    // 3. Return timeDuration / divisor.
+    auto result = Crypto::BigFraction { time_duration } / Crypto::BigFraction { Crypto::SignedBigInteger { divisor } };
+    return result.to_double();
+}
+
 // 7.5.39 TemporalDurationToString ( duration, precision ), https://tc39.es/proposal-temporal/#sec-temporal-temporaldurationtostring
 String temporal_duration_to_string(Duration const& duration, Precision precision)
 {

+ 1 - 0
Libraries/LibJS/Runtime/Temporal/Duration.h

@@ -121,6 +121,7 @@ i8 compare_time_duration(TimeDuration const&, TimeDuration const&);
 ThrowCompletionOr<TimeDuration> round_time_duration_to_increment(VM&, TimeDuration const&, Crypto::UnsignedBigInteger const& increment, RoundingMode);
 i8 time_duration_sign(TimeDuration const&);
 ThrowCompletionOr<TimeDuration> round_time_duration(VM&, TimeDuration const&, Crypto::UnsignedBigInteger const& increment, Unit, RoundingMode);
+double total_time_duration(TimeDuration const&, Unit);
 String temporal_duration_to_string(Duration const&, Precision);
 ThrowCompletionOr<GC::Ref<Duration>> add_durations(VM&, ArithmeticOperation, Duration const&, Value);
 

+ 194 - 0
Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp

@@ -41,6 +41,7 @@ void DurationPrototype::initialize(Realm& realm)
     define_native_function(realm, vm.names.abs, abs, 0, attr);
     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.round, round, 1, attr);
     define_native_function(realm, vm.names.toString, to_string, 0, attr);
     define_native_function(realm, vm.names.toJSON, to_json, 0, attr);
     define_native_function(realm, vm.names.toLocaleString, to_locale_string, 0, attr);
@@ -217,6 +218,199 @@ JS_DEFINE_NATIVE_FUNCTION(DurationPrototype::subtract)
     return TRY(add_durations(vm, ArithmeticOperation::Subtract, duration, other));
 }
 
+// 7.3.20 Temporal.Duration.prototype.round ( roundTo ), https://tc39.es/proposal-temporal/#sec-temporal.duration.prototype.round
+JS_DEFINE_NATIVE_FUNCTION(DurationPrototype::round)
+{
+    auto& realm = *vm.current_realm();
+
+    auto round_to_value = vm.argument(0);
+
+    // 1. Let duration be the this value.
+    // 2. Perform ? RequireInternalSlot(duration, [[InitializedTemporalDuration]]).
+    auto duration = TRY(typed_this_object(vm));
+
+    // 3. If roundTo is undefined, then
+    if (round_to_value.is_undefined()) {
+        // a. Throw a TypeError exception.
+        return vm.throw_completion<TypeError>(ErrorType::TemporalMissingOptionsObject);
+    }
+
+    GC::Ptr<Object> round_to;
+
+    // 4. If roundTo is a String, then
+    if (round_to_value.is_string()) {
+        // a. Let paramString be roundTo.
+        auto param_string = round_to_value;
+
+        // b. Set roundTo to OrdinaryObjectCreate(null).
+        round_to = Object::create(realm, nullptr);
+
+        // c. Perform ! CreateDataPropertyOrThrow(roundTo, "smallestUnit", paramString).
+        MUST(round_to->create_data_property_or_throw(vm.names.smallestUnit, param_string));
+    }
+    // 5. Else,
+    else {
+        // a. Set roundTo to ? GetOptionsObject(roundTo).
+        round_to = TRY(get_options_object(vm, round_to_value));
+    }
+
+    // 6. Let smallestUnitPresent be true.
+    bool smallest_unit_present = true;
+
+    // 7. Let largestUnitPresent be true.
+    bool largest_unit_present = true;
+
+    // 8. NOTE: The following steps read options and perform independent validation in alphabetical order
+    //    (GetTemporalRelativeToOption reads "relativeTo", GetRoundingIncrementOption reads "roundingIncrement" and
+    //    GetRoundingModeOption reads "roundingMode").
+
+    // 9. Let largestUnit be ? GetTemporalUnitValuedOption(roundTo, "largestUnit", DATETIME, UNSET, « auto »).
+    auto largest_unit = TRY(get_temporal_unit_valued_option(vm, *round_to, vm.names.largestUnit, UnitGroup::DateTime, Unset {}, { { Auto {} } }));
+
+    // 10. Let relativeToRecord be ? GetTemporalRelativeToOption(roundTo).
+    // 11. Let zonedRelativeTo be relativeToRecord.[[ZonedRelativeTo]].
+    // 12. Let plainRelativeTo be relativeToRecord.[[PlainRelativeTo]].
+    auto [zoned_relative_to, plain_relative_to] = TRY(get_temporal_relative_to_option(vm, *round_to));
+
+    // 13. Let roundingIncrement be ? GetRoundingIncrementOption(roundTo).
+    auto rounding_increment = TRY(get_rounding_increment_option(vm, *round_to));
+
+    // 14. Let roundingMode be ? GetRoundingModeOption(roundTo, HALF-EXPAND).
+    auto rounding_mode = TRY(get_rounding_mode_option(vm, *round_to, RoundingMode::HalfExpand));
+
+    // 15. Let smallestUnit be ? GetTemporalUnitValuedOption(roundTo, "smallestUnit", DATETIME, UNSET).
+    auto smallest_unit = TRY(get_temporal_unit_valued_option(vm, *round_to, vm.names.smallestUnit, UnitGroup::DateTime, Unset {}));
+
+    // 16. If smallestUnit is UNSET, then
+    if (smallest_unit.has<Unset>()) {
+        // a. Set smallestUnitPresent to false.
+        smallest_unit_present = false;
+
+        // b. Set smallestUnit to NANOSECOND.
+        smallest_unit = Unit::Nanosecond;
+    }
+
+    auto smallest_unit_value = smallest_unit.get<Unit>();
+
+    // 17. Let existingLargestUnit be DefaultTemporalLargestUnit(duration).
+    auto existing_largest_unit = default_temporal_largest_unit(duration);
+
+    // 18. Let defaultLargestUnit be LargerOfTwoTemporalUnits(existingLargestUnit, smallestUnit).
+    auto default_largest_unit = larger_of_two_temporal_units(existing_largest_unit, smallest_unit_value);
+
+    // 19. If largestUnit is UNSET, then
+    if (largest_unit.has<Unset>()) {
+        // a. Set largestUnitPresent to false.
+        largest_unit_present = false;
+
+        // b. Set largestUnit to defaultLargestUnit.
+        largest_unit = default_largest_unit;
+    }
+    // 20. Else if largestUnit is AUTO, then
+    else if (largest_unit.has<Auto>()) {
+        // a. Set largestUnit to defaultLargestUnit.
+        largest_unit = default_largest_unit;
+    }
+
+    // 21. 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<RangeError>(ErrorType::TemporalMissingUnits);
+    }
+
+    auto largest_unit_value = largest_unit.get<Unit>();
+
+    // 22. If LargerOfTwoTemporalUnits(largestUnit, smallestUnit) is not largestUnit, throw a RangeError exception.
+    if (larger_of_two_temporal_units(largest_unit_value, smallest_unit_value) != largest_unit_value)
+        return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidUnitRange, temporal_unit_to_string(smallest_unit_value), temporal_unit_to_string(largest_unit_value));
+
+    // 23. Let maximum be MaximumTemporalDurationRoundingIncrement(smallestUnit).
+    auto maximum = maximum_temporal_duration_rounding_increment(smallest_unit_value);
+
+    // 24. If maximum is not UNSET, perform ? ValidateTemporalRoundingIncrement(roundingIncrement, maximum, false).
+    if (!maximum.has<Unset>())
+        TRY(validate_temporal_rounding_increment(vm, rounding_increment, maximum.get<u64>(), false));
+
+    // 25. If roundingIncrement > 1, and largestUnit is not smallestUnit, and TemporalUnitCategory(smallestUnit) is DATE,
+    //     throw a RangeError exception.
+    if (rounding_increment > 1 && largest_unit_value != smallest_unit_value && temporal_unit_category(smallest_unit_value) == UnitCategory::Date)
+        return vm.throw_completion<RangeError>(ErrorType::OptionIsNotValidValue, rounding_increment, "roundingIncrement");
+
+    // 26. If zonedRelativeTo is not undefined, then
+    if (zoned_relative_to) {
+        // a. Let internalDuration be ToInternalDurationRecord(duration).
+        auto internal_duration = to_internal_duration_record(vm, duration);
+
+        // FIXME: b. Let timeZone be zonedRelativeTo.[[TimeZone]].
+        // FIXME: c. Let calendar be zonedRelativeTo.[[Calendar]].
+        // FIXME: d. Let relativeEpochNs be zonedRelativeTo.[[EpochNanoseconds]].
+        // FIXME: e. Let targetEpochNs be ? AddZonedDateTime(relativeEpochNs, timeZone, calendar, internalDuration, constrain).
+        // FIXME: f. Set internalDuration to ? DifferenceZonedDateTimeWithRounding(relativeEpochNs, targetEpochNs, timeZone, calendar, largestUnit, roundingIncrement, smallestUnit, roundingMode).
+
+        // g. If TemporalUnitCategory(largestUnit) is date, set largestUnit to hour.
+        if (temporal_unit_category(largest_unit_value) == UnitCategory::Date)
+            largest_unit_value = Unit::Hour;
+
+        // h. Return ? TemporalDurationFromInternal(internalDuration, largestUnit).
+        return TRY(temporal_duration_from_internal(vm, internal_duration, largest_unit_value));
+    }
+
+    // 27. If plainRelativeTo is not undefined, then
+    if (plain_relative_to) {
+        // a. Let internalDuration be ToInternalDurationRecordWith24HourDays(duration).
+        auto internal_duration = to_internal_duration_record_with_24_hour_days(vm, duration);
+
+        // FIXME: b. Let targetTime be AddTime(MidnightTimeRecord(), internalDuration.[[Time]]).
+        // FIXME: c. Let calendar be plainRelativeTo.[[Calendar]].
+        // FIXME: d. Let dateDuration be ! AdjustDateDurationRecord(internalDuration.[[Date]], targetTime.[[Days]]).
+        // FIXME: e. Let targetDate be ? CalendarDateAdd(calendar, plainRelativeTo.[[ISODate]], dateDuration, constrain).
+        // FIXME: f. Let isoDateTime be CombineISODateAndTimeRecord(plainRelativeTo.[[ISODate]], MidnightTimeRecord()).
+        // FIXME: g. Let targetDateTime be CombineISODateAndTimeRecord(targetDate, targetTime).
+        // FIXME: h. Set internalDuration to ? DifferencePlainDateTimeWithRounding(isoDateTime, targetDateTime, calendar, largestUnit, roundingIncrement, smallestUnit, roundingMode).
+
+        // i. Return ? TemporalDurationFromInternal(internalDuration, largestUnit).
+        return TRY(temporal_duration_from_internal(vm, internal_duration, largest_unit_value));
+    }
+
+    // 28. If IsCalendarUnit(existingLargestUnit) is true, or IsCalendarUnit(largestUnit) is true, throw a RangeError exception.
+    if (is_calendar_unit(existing_largest_unit))
+        return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidLargestUnit, temporal_unit_to_string(existing_largest_unit));
+    if (is_calendar_unit(largest_unit_value))
+        return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidLargestUnit, temporal_unit_to_string(largest_unit_value));
+
+    // 29. Assert: IsCalendarUnit(smallestUnit) is false.
+    VERIFY(!is_calendar_unit(smallest_unit_value));
+
+    // 30. Let internalDuration be ToInternalDurationRecordWith24HourDays(duration).
+    auto internal_duration = to_internal_duration_record_with_24_hour_days(vm, duration);
+
+    // 31. If smallestUnit is DAY, then
+    if (smallest_unit_value == Unit::Day) {
+        // a. Let fractionalDays be TotalTimeDuration(internalDuration.[[Time]], DAY).
+        auto fractional_days = total_time_duration(internal_duration.time, Unit::Day);
+
+        // b. Let days be RoundNumberToIncrement(fractionalDays, roundingIncrement, roundingMode).
+        auto days = round_number_to_increment(fractional_days, rounding_increment, rounding_mode);
+
+        // c. Let dateDuration be ? CreateDateDurationRecord(0, 0, 0, days).
+        auto date_duration = TRY(create_date_duration_record(vm, 0, 0, 0, days));
+
+        // d. Set internalDuration to ! CombineDateAndTimeDuration(dateDuration, 0).
+        internal_duration = MUST(combine_date_and_time_duration(vm, date_duration, TimeDuration { 0 }));
+    }
+    // 32. Else,
+    else {
+        // a. Let timeDuration be ? RoundTimeDuration(internalDuration.[[Time]], roundingIncrement, smallestUnit, roundingMode).
+        auto time_duration = TRY(round_time_duration(vm, internal_duration.time, Crypto::UnsignedBigInteger { rounding_increment }, smallest_unit_value, rounding_mode));
+
+        // b. Set internalDuration to ! CombineDateAndTimeDuration(ZeroDateDuration(), timeDuration).
+        internal_duration = MUST(combine_date_and_time_duration(vm, zero_date_duration(vm), move(time_duration)));
+    }
+
+    // 33. Return ? TemporalDurationFromInternal(internalDuration, largestUnit).
+    return TRY(temporal_duration_from_internal(vm, internal_duration, largest_unit_value));
+}
+
 // 7.3.22 Temporal.Duration.prototype.toString ( [ options ] ), https://tc39.es/proposal-temporal/#sec-temporal.duration.prototype.tostring
 JS_DEFINE_NATIVE_FUNCTION(DurationPrototype::to_string)
 {

+ 1 - 0
Libraries/LibJS/Runtime/Temporal/DurationPrototype.h

@@ -35,6 +35,7 @@ private:
     JS_DECLARE_NATIVE_FUNCTION(abs);
     JS_DECLARE_NATIVE_FUNCTION(add);
     JS_DECLARE_NATIVE_FUNCTION(subtract);
+    JS_DECLARE_NATIVE_FUNCTION(round);
     JS_DECLARE_NATIVE_FUNCTION(to_string);
     JS_DECLARE_NATIVE_FUNCTION(to_json);
     JS_DECLARE_NATIVE_FUNCTION(to_locale_string);

+ 173 - 0
Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.round.js

@@ -0,0 +1,173 @@
+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: "<string value>" } 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,
+            "Infinity 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, "Largest unit must not be year");
+    });
+
+    // Spec Issue: https://github.com/tc39/proposal-temporal/issues/2124
+    // Spec Fix: https://github.com/tc39/proposal-temporal/commit/66f7464aaec64d3cd21fb2ec37f6502743b9a730
+    test("balancing calendar units with largestUnit set to 'year' and relativeTo unset throws instead of crashing", () => {
+        const duration = new Temporal.Duration(1);
+        expect(() => {
+            duration.round({ largestUnit: "year" });
+        }).toThrowWithMessage(RangeError, "Largest unit must not be year");
+    });
+});