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

This commit is contained in:
Linus Groh 2021-11-13 17:38:00 +00:00
parent 656efe5d6c
commit dbe70e7c55
Notes: sideshowbarker 2024-07-18 01:11:17 +09:00
7 changed files with 245 additions and 0 deletions

View file

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

View file

@ -515,6 +515,32 @@ ThrowCompletionOr<Optional<String>> to_smallest_temporal_unit(GlobalObject& glob
return smallest_unit; 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 // 13.21 ToRelativeTemporalObject ( options ), https://tc39.es/proposal-temporal/#sec-temporal-torelativetemporalobject
ThrowCompletionOr<Value> to_relative_temporal_object(GlobalObject& global_object, Object const& options) ThrowCompletionOr<Value> to_relative_temporal_object(GlobalObject& global_object, Object const& options)
{ {

View file

@ -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<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<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<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<Value> to_relative_temporal_object(GlobalObject&, Object const& options);
ThrowCompletionOr<void> validate_temporal_unit_range(GlobalObject&, StringView largest_unit, StringView smallest_unit); ThrowCompletionOr<void> validate_temporal_unit_range(GlobalObject&, StringView largest_unit, StringView smallest_unit);
String larger_of_two_temporal_units(StringView, StringView); String larger_of_two_temporal_units(StringView, StringView);

View file

@ -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. // 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 // 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) { if (unit.is_one_of("year"sv, "month"sv, "week"sv) && !relative_to_object) {
// a. Throw a RangeError exception. // a. Throw a RangeError exception.

View file

@ -9,6 +9,7 @@
#include <LibJS/Runtime/Temporal/AbstractOperations.h> #include <LibJS/Runtime/Temporal/AbstractOperations.h>
#include <LibJS/Runtime/Temporal/Duration.h> #include <LibJS/Runtime/Temporal/Duration.h>
#include <LibJS/Runtime/Temporal/DurationPrototype.h> #include <LibJS/Runtime/Temporal/DurationPrototype.h>
#include <LibJS/Runtime/Temporal/ZonedDateTime.h>
#include <math.h> #include <math.h>
namespace JS::Temporal { 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.with, with, 1, attr);
define_native_function(vm.names.negated, negated, 0, attr); define_native_function(vm.names.negated, negated, 0, attr);
define_native_function(vm.names.abs, abs, 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.toString, to_string, 0, attr);
define_native_function(vm.names.toJSON, to_json, 0, attr); define_native_function(vm.names.toJSON, to_json, 0, attr);
define_native_function(vm.names.toLocaleString, to_locale_string, 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()))); 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 // 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) JS_DEFINE_NATIVE_FUNCTION(DurationPrototype::to_string)
{ {

View file

@ -35,6 +35,7 @@ private:
JS_DECLARE_NATIVE_FUNCTION(with); JS_DECLARE_NATIVE_FUNCTION(with);
JS_DECLARE_NATIVE_FUNCTION(negated); JS_DECLARE_NATIVE_FUNCTION(negated);
JS_DECLARE_NATIVE_FUNCTION(abs); JS_DECLARE_NATIVE_FUNCTION(abs);
JS_DECLARE_NATIVE_FUNCTION(total);
JS_DECLARE_NATIVE_FUNCTION(to_string); JS_DECLARE_NATIVE_FUNCTION(to_string);
JS_DECLARE_NATIVE_FUNCTION(to_json); JS_DECLARE_NATIVE_FUNCTION(to_json);
JS_DECLARE_NATIVE_FUNCTION(to_locale_string); JS_DECLARE_NATIVE_FUNCTION(to_locale_string);

View file

@ -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"
);
});
});