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.
This commit is contained in:
Timothy Flynn 2024-11-18 15:42:18 -05:00 committed by Tim Flynn
parent 5689621c2b
commit c715711f88
Notes: github-actions[bot] 2024-11-21 00:05:35 +00:00
3 changed files with 146 additions and 0 deletions

View file

@ -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<TypeError>(ErrorType::IsUndefined, "totalOf"sv);
GC::Ptr<Object> 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<Unit>();
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<RangeError>(ErrorType::TemporalInvalidLargestUnit, temporal_unit_to_string(largest_unit));
if (is_calendar_unit(unit))
return vm.throw_completion<RangeError>(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)
{

View file

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

View file

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