Browse Source

LibJS: Implement Temporal.Duration.prototype.total()

Linus Groh 3 years ago
parent
commit
dbe70e7c55

+ 1 - 0
Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h

@@ -456,6 +456,7 @@ namespace JS {
     P(toPlainTime)                           \
     P(toPlainYearMonth)                      \
     P(toString)                              \
+    P(total)                                 \
     P(toTemporalInstant)                     \
     P(toTimeString)                          \
     P(toUpperCase)                           \

+ 26 - 0
Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp

@@ -515,6 +515,32 @@ ThrowCompletionOr<Optional<String>> to_smallest_temporal_unit(GlobalObject& glob
     return smallest_unit;
 }
 
+// 13.19 ToTemporalDurationTotalUnit ( normalizedOptions ), https://tc39.es/proposal-temporal/#sec-temporal-totemporaldurationtotalunit
+ThrowCompletionOr<String> to_temporal_duration_total_unit(GlobalObject& global_object, Object const& normalized_options)
+{
+    auto& vm = global_object.vm();
+
+    // 1. Let unit be ? GetOption(normalizedOptions, "unit", « String », « "year", "years", "month", "months", "week", "weeks", "day", "days", "hour", "hours", "minute", "minutes", "second", "seconds", "millisecond", "milliseconds", "microsecond", "microseconds", "nanosecond", "nanoseconds" », undefined).
+    auto unit_value = TRY(get_option(global_object, normalized_options, vm.names.unit, { OptionType::String }, { "year"sv, "years"sv, "month"sv, "months"sv, "week"sv, "weeks"sv, "day"sv, "days"sv, "hour"sv, "hours"sv, "minute"sv, "minutes"sv, "second"sv, "seconds"sv, "millisecond"sv, "milliseconds"sv, "microsecond"sv, "microseconds"sv, "nanosecond"sv, "nanoseconds"sv }, js_undefined()));
+
+    // 2. If unit is undefined, then
+    if (unit_value.is_undefined()) {
+        // a. Throw a RangeError exception.
+        return vm.throw_completion<RangeError>(global_object, ErrorType::IsUndefined, "unit option value"sv);
+    }
+
+    auto unit = unit_value.as_string().string();
+
+    // 3. If unit is in the Plural column of Table 12, then
+    if (auto singular_unit = plural_to_singular_units.get(unit); singular_unit.has_value()) {
+        // a. Set unit to the corresponding Singular value of the same row.
+        unit = *singular_unit;
+    }
+
+    // 4. Return unit.
+    return unit;
+}
+
 // 13.21 ToRelativeTemporalObject ( options ), https://tc39.es/proposal-temporal/#sec-temporal-torelativetemporalobject
 ThrowCompletionOr<Value> to_relative_temporal_object(GlobalObject& global_object, Object const& options)
 {

+ 1 - 0
Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h

@@ -113,6 +113,7 @@ ThrowCompletionOr<u64> to_temporal_date_time_rounding_increment(GlobalObject&, O
 ThrowCompletionOr<SecondsStringPrecision> to_seconds_string_precision(GlobalObject&, Object const& normalized_options);
 ThrowCompletionOr<String> to_largest_temporal_unit(GlobalObject&, Object const& normalized_options, Vector<StringView> const& disallowed_units, String const& fallback, Optional<String> auto_value);
 ThrowCompletionOr<Optional<String>> to_smallest_temporal_unit(GlobalObject&, Object const& normalized_options, Vector<StringView> const& disallowed_units, Optional<String> fallback);
+ThrowCompletionOr<String> to_temporal_duration_total_unit(GlobalObject& global_object, Object const& normalized_options);
 ThrowCompletionOr<Value> to_relative_temporal_object(GlobalObject&, Object const& options);
 ThrowCompletionOr<void> validate_temporal_unit_range(GlobalObject&, StringView largest_unit, StringView smallest_unit);
 String larger_of_two_temporal_units(StringView, StringView);

+ 3 - 0
Userland/Libraries/LibJS/Runtime/Temporal/Duration.cpp

@@ -704,6 +704,9 @@ ThrowCompletionOr<RoundedDuration> round_duration(GlobalObject& global_object, d
 
     // 2. Let years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, and increment each be the mathematical values of themselves.
 
+    // FIXME: assuming "smallestUnit" as the option name here leads to confusing error messages in some cases:
+    //        > new Temporal.Duration().total({ unit: "month" })
+    //        Uncaught exception: [RangeError] month is not a valid value for option smallestUnit
     // 3. If unit is "year", "month", or "week", and relativeTo is undefined, then
     if (unit.is_one_of("year"sv, "month"sv, "week"sv) && !relative_to_object) {
         // a. Throw a RangeError exception.

+ 106 - 0
Userland/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp

@@ -9,6 +9,7 @@
 #include <LibJS/Runtime/Temporal/AbstractOperations.h>
 #include <LibJS/Runtime/Temporal/Duration.h>
 #include <LibJS/Runtime/Temporal/DurationPrototype.h>
+#include <LibJS/Runtime/Temporal/ZonedDateTime.h>
 #include <math.h>
 
 namespace JS::Temporal {
@@ -45,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.total, total, 1, attr);
     define_native_function(vm.names.toString, to_string, 0, attr);
     define_native_function(vm.names.toJSON, to_json, 0, attr);
     define_native_function(vm.names.toLocaleString, to_locale_string, 0, attr);
@@ -286,6 +288,110 @@ 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.21 Temporal.Duration.prototype.total ( options ), https://tc39.es/proposal-temporal/#sec-temporal.duration.prototype.total
+JS_DEFINE_NATIVE_FUNCTION(DurationPrototype::total)
+{
+    // 1. Let duration be the this value.
+    // 2. Perform ? RequireInternalSlot(duration, [[InitializedTemporalDuration]]).
+    auto* duration = TRY(typed_this_object(global_object));
+
+    // 3. If options is undefined, throw a TypeError exception.
+    if (vm.argument(0).is_undefined())
+        return vm.throw_completion<TypeError>(global_object, ErrorType::TemporalMissingOptionsObject);
+
+    // 4. Set options to ? GetOptionsObject(options).
+    auto* options = TRY(get_options_object(global_object, vm.argument(0)));
+
+    // 5. Let relativeTo be ? ToRelativeTemporalObject(options).
+    auto relative_to = TRY(to_relative_temporal_object(global_object, *options));
+
+    // 6. Let unit be ? ToTemporalDurationTotalUnit(options).
+    auto unit = TRY(to_temporal_duration_total_unit(global_object, *options));
+
+    // 7. Let unbalanceResult be ? UnbalanceDurationRelative(duration.[[Years]], duration.[[Months]], duration.[[Weeks]], duration.[[Days]], unit, relativeTo).
+    auto unbalance_result = TRY(unbalance_duration_relative(global_object, duration->years(), duration->months(), duration->weeks(), duration->days(), unit, relative_to));
+
+    // 8. Let intermediate be undefined.
+    ZonedDateTime* intermediate = nullptr;
+
+    // 9. If relativeTo has an [[InitializedTemporalZonedDateTime]] internal slot, then
+    if (relative_to.is_object() && is<ZonedDateTime>(relative_to.as_object())) {
+        // a. Set intermediate to ? MoveRelativeZonedDateTime(relativeTo, unbalanceResult.[[Years]], unbalanceResult.[[Months]], unbalanceResult.[[Weeks]], 0).
+        intermediate = TRY(move_relative_zoned_date_time(global_object, static_cast<ZonedDateTime&>(relative_to.as_object()), unbalance_result.years, unbalance_result.months, unbalance_result.weeks, 0));
+    }
+
+    // 10. Let balanceResult be ? BalanceDuration(unbalanceResult.[[Days]], duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]], unit, intermediate).
+    auto balance_result = TRY(balance_duration(global_object, unbalance_result.days, duration->hours(), duration->minutes(), duration->seconds(), duration->milliseconds(), duration->microseconds(), *js_bigint(vm, Crypto::SignedBigInteger::create_from(duration->nanoseconds())), unit, intermediate));
+
+    // 11. Let roundResult be ? RoundDuration(unbalanceResult.[[Years]], unbalanceResult.[[Months]], unbalanceResult.[[Weeks]], balanceResult.[[Days]], balanceResult.[[Hours]], balanceResult.[[Minutes]], balanceResult.[[Seconds]], balanceResult.[[Milliseconds]], balanceResult.[[Microseconds]], balanceResult.[[Nanoseconds]], 1, unit, "trunc", relativeTo).
+    auto round_result = TRY(round_duration(global_object, unbalance_result.years, unbalance_result.months, unbalance_result.weeks, balance_result.days, balance_result.hours, balance_result.minutes, balance_result.seconds, balance_result.milliseconds, balance_result.microseconds, balance_result.nanoseconds, 1, unit, "trunc"sv, relative_to.is_object() ? &relative_to.as_object() : nullptr));
+
+    double whole;
+
+    // 12. If unit is "year", then
+    if (unit == "year"sv) {
+        // a. Let whole be roundResult.[[Years]].
+        whole = round_result.years;
+    }
+
+    // 13. If unit is "month", then
+    if (unit == "month"sv) {
+        // a. Let whole be roundResult.[[Months]].
+        whole = round_result.months;
+    }
+
+    // 14. If unit is "week", then
+    if (unit == "week"sv) {
+        // a. Let whole be roundResult.[[Weeks]].
+        whole = round_result.weeks;
+    }
+
+    // 15. If unit is "day", then
+    if (unit == "day"sv) {
+        // a. Let whole be roundResult.[[Days]].
+        whole = round_result.days;
+    }
+
+    // 16. If unit is "hour", then
+    if (unit == "hour"sv) {
+        // a. Let whole be roundResult.[[Hours]].
+        whole = round_result.hours;
+    }
+
+    // 17. If unit is "minute", then
+    if (unit == "minute"sv) {
+        // a. Let whole be roundResult.[[Minutes]].
+        whole = round_result.minutes;
+    }
+
+    // 18. If unit is "second", then
+    if (unit == "second"sv) {
+        // a. Let whole be roundResult.[[Seconds]].
+        whole = round_result.seconds;
+    }
+
+    // 19. If unit is "millisecond", then
+    if (unit == "millisecond"sv) {
+        // a. Let whole be roundResult.[[Milliseconds]].
+        whole = round_result.milliseconds;
+    }
+
+    // 20. If unit is "microsecond", then
+    if (unit == "microsecond"sv) {
+        // a. Let whole be roundResult.[[Microseconds]].
+        whole = round_result.microseconds;
+    }
+
+    // 21. If unit is "nanosecond", then
+    if (unit == "nanosecond"sv) {
+        // a. Let whole be roundResult.[[Nanoseconds]].
+        whole = round_result.nanoseconds;
+    }
+
+    // 22. Return whole + roundResult.[[Remainder]].
+    return whole + round_result.remainder;
+}
+
 // 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
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(total);
     JS_DECLARE_NATIVE_FUNCTION(to_string);
     JS_DECLARE_NATIVE_FUNCTION(to_json);
     JS_DECLARE_NATIVE_FUNCTION(to_locale_string);

+ 107 - 0
Userland/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.total.js

@@ -0,0 +1,107 @@
+describe("correct behavior", () => {
+    test("basic functionality", () => {
+        {
+            const duration = new Temporal.Duration(0, 0, 0, 0, 1, 2, 3, 4, 5, 6);
+            const relativeTo = new Temporal.PlainDate(1970, 1, 1);
+            const values = [
+                [{ unit: "year", relativeTo }, 0.0001180556825534627],
+                [{ unit: "month", relativeTo }, 0.0013900104558714158],
+                [{ unit: "week", relativeTo }, 0.006155760590287699],
+                [{ unit: "day", relativeTo }, 0.04309032413201389],
+                [{ unit: "hour" }, 1.034167779168333],
+                [{ unit: "minute" }, 62.0500667501],
+                [{ unit: "second" }, 3723.00400500600017],
+                [{ unit: "millisecond" }, 3723004.005005999933928],
+                [{ unit: "microsecond" }, 3723004005.006000041961669],
+                [{ unit: "nanosecond" }, 3723004005006],
+            ];
+            for (const [arg, expected] of values) {
+                const matcher = Number.isInteger(expected) ? "toBe" : "toBeCloseTo";
+                expect(duration.total(arg))[matcher](expected);
+            }
+        }
+
+        {
+            const duration = new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
+            const relativeTo = new Temporal.PlainDate(1970, 1, 1);
+            const values = [
+                [{ unit: "year", relativeTo }, 1.2307194003046997],
+                [{ unit: "month", relativeTo }, 14.813309068103722],
+                [{ unit: "week", relativeTo }, 64.17322587303077],
+                [{ unit: "day", relativeTo }, 449.21258111121534],
+                [{ unit: "hour", relativeTo }, 10781.101946669169],
+                [{ unit: "minute", relativeTo }, 646866.1168001501],
+                [{ unit: "second", relativeTo }, 38811967.00800901],
+                [{ unit: "millisecond", relativeTo }, 38811967008.00901],
+                [{ unit: "microsecond", relativeTo }, 38811967008009.01],
+                [{ unit: "nanosecond", relativeTo }, 38811967008009010],
+            ];
+            for (const [arg, expected] of values) {
+                const matcher = Number.isInteger(expected) ? "toBe" : "toBeCloseTo";
+                expect(duration.total(arg))[matcher](expected);
+            }
+        }
+
+        {
+            const relativeTo = new Temporal.PlainDate(1970, 1, 1);
+            const units = [
+                "year",
+                "month",
+                "week",
+                "day",
+                "hour",
+                "minute",
+                "second",
+                "millisecond",
+                "microsecond",
+                "nanosecond",
+            ];
+            for (let i = 0; i < 10; ++i) {
+                const args = [0, 0, 0, 0, 0, 0, 0, 0, 0];
+                args[i] = 123;
+                const unit = units[i];
+                const duration = new Temporal.Duration(...args);
+                expect(duration.total({ unit, relativeTo })).toBe(123);
+            }
+        }
+    });
+});
+
+describe("errors", () => {
+    test("this value must be a Temporal.Duration object", () => {
+        expect(() => {
+            Temporal.Duration.prototype.total.call("foo");
+        }).toThrowWithMessage(TypeError, "Not an object of type Temporal.Duration");
+    });
+
+    test("missing options object", () => {
+        const duration = new Temporal.Duration();
+        expect(() => {
+            duration.total();
+        }).toThrowWithMessage(TypeError, "Required options object is missing or undefined");
+    });
+
+    test("missing unit option", () => {
+        const duration = new Temporal.Duration();
+        expect(() => {
+            duration.total({});
+        }).toThrowWithMessage(RangeError, "unit option value is undefined");
+    });
+
+    test("invalid unit option", () => {
+        const duration = new Temporal.Duration();
+        expect(() => {
+            duration.total({ unit: "foo" });
+        }).toThrowWithMessage(RangeError, "foo is not a valid value for option unit");
+    });
+
+    test("relativeTo is required when duration has calendar units", () => {
+        const duration = new Temporal.Duration(1);
+        expect(() => {
+            duration.total({ unit: "second" });
+        }).toThrowWithMessage(
+            RangeError,
+            "A starting point is required for balancing calendar units"
+        );
+    });
+});