From dbe70e7c55b2d24f57c6aea40da0e57f6a20f8b6 Mon Sep 17 00:00:00 2001 From: Linus Groh Date: Sat, 13 Nov 2021 17:38:00 +0000 Subject: [PATCH] LibJS: Implement Temporal.Duration.prototype.total() --- .../LibJS/Runtime/CommonPropertyNames.h | 1 + .../Runtime/Temporal/AbstractOperations.cpp | 26 +++++ .../Runtime/Temporal/AbstractOperations.h | 1 + .../LibJS/Runtime/Temporal/Duration.cpp | 3 + .../Runtime/Temporal/DurationPrototype.cpp | 106 +++++++++++++++++ .../Runtime/Temporal/DurationPrototype.h | 1 + .../Duration/Duration.prototype.total.js | 107 ++++++++++++++++++ 7 files changed, 245 insertions(+) create mode 100644 Userland/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.total.js diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index 713272628de..ce154efa6ac 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/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) \ diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp index 28bb9b544d0..ba8215907fd 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp @@ -515,6 +515,32 @@ ThrowCompletionOr> to_smallest_temporal_unit(GlobalObject& glob return smallest_unit; } +// 13.19 ToTemporalDurationTotalUnit ( normalizedOptions ), https://tc39.es/proposal-temporal/#sec-temporal-totemporaldurationtotalunit +ThrowCompletionOr 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(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 to_relative_temporal_object(GlobalObject& global_object, Object const& options) { diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h index 2b607bced42..dd4812d4bf4 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h @@ -113,6 +113,7 @@ ThrowCompletionOr to_temporal_date_time_rounding_increment(GlobalObject&, O ThrowCompletionOr to_seconds_string_precision(GlobalObject&, Object const& normalized_options); ThrowCompletionOr to_largest_temporal_unit(GlobalObject&, Object const& normalized_options, Vector const& disallowed_units, String const& fallback, Optional auto_value); ThrowCompletionOr> to_smallest_temporal_unit(GlobalObject&, Object const& normalized_options, Vector const& disallowed_units, Optional fallback); +ThrowCompletionOr to_temporal_duration_total_unit(GlobalObject& global_object, Object const& normalized_options); ThrowCompletionOr to_relative_temporal_object(GlobalObject&, Object const& options); ThrowCompletionOr validate_temporal_unit_range(GlobalObject&, StringView largest_unit, StringView smallest_unit); String larger_of_two_temporal_units(StringView, StringView); diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/Duration.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/Duration.cpp index 6b9e1cf2792..25d0ef2748e 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/Duration.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/Duration.cpp @@ -704,6 +704,9 @@ ThrowCompletionOr 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. diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp index 02c91a6963d..ef997a809f8 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include 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(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(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(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) { diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/DurationPrototype.h b/Userland/Libraries/LibJS/Runtime/Temporal/DurationPrototype.h index ef030254d52..51354d0848a 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(total); JS_DECLARE_NATIVE_FUNCTION(to_string); JS_DECLARE_NATIVE_FUNCTION(to_json); JS_DECLARE_NATIVE_FUNCTION(to_locale_string); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.total.js b/Userland/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.total.js new file mode 100644 index 00000000000..acce649369f --- /dev/null +++ b/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" + ); + }); +});