From c715711f88180d2c11409268c346d48595d0ed6c Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Mon, 18 Nov 2024 15:42:18 -0500 Subject: [PATCH] LibJS: Implement Temporal.Duration.prototype.total Until we have re-implemented Temporal.PlainDate/ZonedDateTime, some of Temporal.Duration.prototype.total (and its invoked AOs) are left unimplemented. --- .../Runtime/Temporal/DurationPrototype.cpp | 90 +++++++++++++++++++ .../Runtime/Temporal/DurationPrototype.h | 1 + .../Duration/Duration.prototype.total.js | 55 ++++++++++++ 3 files changed, 146 insertions(+) create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.total.js diff --git a/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp b/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp index 1c9e1bf5b47..4447cb4fa7c 100644 --- a/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp +++ b/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp @@ -42,6 +42,7 @@ void DurationPrototype::initialize(Realm& realm) 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.total, total, 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); @@ -411,6 +412,95 @@ JS_DEFINE_NATIVE_FUNCTION(DurationPrototype::round) return TRY(temporal_duration_from_internal(vm, internal_duration, largest_unit_value)); } +// 7.3.21 Temporal.Duration.prototype.total ( totalOf ), https://tc39.es/proposal-temporal/#sec-temporal.duration.prototype.total +JS_DEFINE_NATIVE_FUNCTION(DurationPrototype::total) +{ + auto& realm = *vm.current_realm(); + + auto total_of_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 totalOf is undefined, throw a TypeError exception. + if (total_of_value.is_undefined()) + return vm.throw_completion(ErrorType::IsUndefined, "totalOf"sv); + + GC::Ptr total_of; + + // 4. If totalOf is a String, then + if (total_of_value.is_string()) { + // a. Let paramString be totalOf. + auto param_string = total_of_value; + + // b. Set totalOf to OrdinaryObjectCreate(null). + total_of = Object::create(realm, nullptr); + + // c. Perform ! CreateDataPropertyOrThrow(totalOf, "unit", paramString). + MUST(total_of->create_data_property_or_throw(vm.names.unit, param_string)); + } + // 5. Else, + else { + // a. Set totalOf to ? GetOptionsObject(totalOf). + total_of = TRY(get_options_object(vm, total_of_value)); + } + + // 6. NOTE: The following steps read options and perform independent validation in alphabetical order + // (GetTemporalRelativeToOption reads "relativeTo"). + + // 7. Let relativeToRecord be ? GetTemporalRelativeToOption(totalOf). + // 8. Let zonedRelativeTo be relativeToRecord.[[ZonedRelativeTo]]. + // 9. Let plainRelativeTo be relativeToRecord.[[PlainRelativeTo]]. + auto [zoned_relative_to, plain_relative_to] = TRY(get_temporal_relative_to_option(vm, *total_of)); + + // 10. Let unit be ? GetTemporalUnitValuedOption(totalOf, "unit", DATETIME, REQUIRED). + auto unit = TRY(get_temporal_unit_valued_option(vm, *total_of, vm.names.unit, UnitGroup::DateTime, Required {})).get(); + + double total = 0; + + // 11. If zonedRelativeTo is not undefined, then + if (zoned_relative_to) { + // FIXME: a. Let internalDuration be ToInternalDurationRecord(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. Let total be ? DifferenceZonedDateTimeWithTotal(relativeEpochNs, targetEpochNs, timeZone, calendar, unit). + } + // 12. Else if plainRelativeTo is not undefined, then + else if (plain_relative_to) { + // FIXME: a. Let internalDuration be ToInternalDurationRecordWith24HourDays(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. Let total be ? DifferencePlainDateTimeWithTotal(isoDateTime, targetDateTime, calendar, unit). + } + // 13. Else, + else { + // a. Let largestUnit be DefaultTemporalLargestUnit(duration). + auto largest_unit = default_temporal_largest_unit(duration); + + // b. If IsCalendarUnit(largestUnit) is true, or IsCalendarUnit(unit) is true, throw a RangeError exception. + if (is_calendar_unit(largest_unit)) + return vm.throw_completion(ErrorType::TemporalInvalidLargestUnit, temporal_unit_to_string(largest_unit)); + if (is_calendar_unit(unit)) + return vm.throw_completion(ErrorType::TemporalInvalidLargestUnit, temporal_unit_to_string(unit)); + + // c. Let internalDuration be ToInternalDurationRecordWith24HourDays(duration). + auto internal_duration = to_internal_duration_record_with_24_hour_days(vm, duration); + + // d. Let total be TotalTimeDuration(internalDuration.[[Time]], unit). + total = total_time_duration(internal_duration.time, unit); + } + + // 14. Return 𝔽(total). + return total; +} + // 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/Libraries/LibJS/Runtime/Temporal/DurationPrototype.h b/Libraries/LibJS/Runtime/Temporal/DurationPrototype.h index 2070d5ea6c3..7faa80248e8 100644 --- a/Libraries/LibJS/Runtime/Temporal/DurationPrototype.h +++ b/Libraries/LibJS/Runtime/Temporal/DurationPrototype.h @@ -36,6 +36,7 @@ private: JS_DECLARE_NATIVE_FUNCTION(add); JS_DECLARE_NATIVE_FUNCTION(subtract); JS_DECLARE_NATIVE_FUNCTION(round); + 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/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.total.js b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.total.js new file mode 100644 index 00000000000..887bf57c712 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.total.js @@ -0,0 +1,55 @@ +describe("correct behavior", () => { + test("basic functionality", () => { + { + const duration = new Temporal.Duration(0, 0, 0, 0, 1, 2, 3, 4, 5, 6); + const values = [ + [{ 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); + } + } + }); +}); + +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, "totalOf is undefined"); + }); + + test("missing unit option", () => { + const duration = new Temporal.Duration(); + expect(() => { + duration.total({}); + }).toThrowWithMessage(RangeError, "undefined is not a valid value for option unit"); + }); + + 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, "Largest unit must not be year"); + }); +});