LibJS: Implement Temporal.ZonedDateTime.prototype.round

This commit is contained in:
Timothy Flynn 2024-11-25 13:02:33 -05:00 committed by Andreas Kling
parent eadd0c40c9
commit f2ab9e1aa9
Notes: github-actions[bot] 2024-11-26 10:03:33 +00:00
3 changed files with 240 additions and 0 deletions

View file

@ -10,6 +10,7 @@
#include <LibJS/Runtime/Temporal/Duration.h>
#include <LibJS/Runtime/Temporal/Instant.h>
#include <LibJS/Runtime/Temporal/PlainDate.h>
#include <LibJS/Runtime/Temporal/PlainDateTime.h>
#include <LibJS/Runtime/Temporal/TimeZone.h>
#include <LibJS/Runtime/Temporal/ZonedDateTimePrototype.h>
@ -66,6 +67,7 @@ void ZonedDateTimePrototype::initialize(Realm& realm)
define_native_function(realm, vm.names.subtract, subtract, 1, attr);
define_native_function(realm, vm.names.until, until, 1, attr);
define_native_function(realm, vm.names.since, since, 1, attr);
define_native_function(realm, vm.names.round, round, 1, attr);
define_native_function(realm, vm.names.equals, equals, 1, attr);
define_native_function(realm, vm.names.toString, to_string, 0, attr);
define_native_function(realm, vm.names.toLocaleString, to_locale_string, 0, attr);
@ -403,6 +405,149 @@ JS_DEFINE_NATIVE_FUNCTION(ZonedDateTimePrototype::since)
return TRY(difference_temporal_zoned_date_time(vm, DurationOperation::Since, zoned_date_time, other, options));
}
// 6.3.39 Temporal.ZonedDateTime.prototype.round ( roundTo ), https://tc39.es/proposal-temporal/#sec-temporal.zoneddatetime.prototype.round
JS_DEFINE_NATIVE_FUNCTION(ZonedDateTimePrototype::round)
{
auto& realm = *vm.current_realm();
auto round_to_value = vm.argument(0);
// 1. Let zonedDateTime be the this value.
// 2. Perform ? RequireInternalSlot(zonedDateTime, [[InitializedTemporalZonedDateTime]]).
auto zoned_date_time = TRY(typed_this_object(vm));
// 3. If roundTo is undefined, then
if (round_to_value.is_undefined()) {
// a. Throw a TypeError exception.
return vm.throw_completion<TypeError>(ErrorType::TemporalMissingOptionsObject);
}
GC::Ptr<Object> round_to;
// 4. If roundTo is a String, then
if (round_to_value.is_string()) {
// a. Let paramString be roundTo.
auto param_string = round_to_value;
// b. Set roundTo to OrdinaryObjectCreate(null).
round_to = Object::create(realm, nullptr);
// c. Perform ! CreateDataPropertyOrThrow(roundTo, "smallestUnit", paramString).
MUST(round_to->create_data_property_or_throw(vm.names.smallestUnit, param_string));
}
// 5. Else,
else {
// a. Set roundTo to ? GetOptionsObject(roundTo).
round_to = TRY(get_options_object(vm, round_to_value));
}
// 6. NOTE: The following steps read options and perform independent validation in alphabetical order
// (GetRoundingIncrementOption reads "roundingIncrement" and GetRoundingModeOption reads "roundingMode").
// 7. Let roundingIncrement be ? GetRoundingIncrementOption(roundTo).
auto rounding_increment = TRY(get_rounding_increment_option(vm, *round_to));
// 8. Let roundingMode be ? GetRoundingModeOption(roundTo, HALF-EXPAND).
auto rounding_mode = TRY(get_rounding_mode_option(vm, *round_to, RoundingMode::HalfExpand));
// 9. Let smallestUnit be ? GetTemporalUnitValuedOption(roundTo, "smallestUnit", TIME, REQUIRED, « DAY »).
auto smallest_unit = TRY(get_temporal_unit_valued_option(vm, *round_to, vm.names.smallestUnit, UnitGroup::Time, Required {}, { { Unit::Day } }));
auto smallest_unit_value = smallest_unit.get<Unit>();
RoundingIncrement maximum { 0 };
auto inclusive = false;
// 10. If smallestUnit is DAY, then
if (smallest_unit_value == Unit::Day) {
// a. Let maximum be 1.
maximum = 1;
// b. Let inclusive be true.
inclusive = true;
}
// 11. Else,
else {
// a. Let maximum be MaximumTemporalDurationRoundingIncrement(smallestUnit).
maximum = maximum_temporal_duration_rounding_increment(smallest_unit_value);
// b. Assert: maximum is not UNSET.
VERIFY(!maximum.has<Unset>());
// c. Let inclusive be false.
inclusive = false;
}
// 12. Perform ? ValidateTemporalRoundingIncrement(roundingIncrement, maximum, inclusive).
TRY(validate_temporal_rounding_increment(vm, rounding_increment, maximum.get<u64>(), inclusive));
// 13. If smallestUnit is NANOSECOND and roundingIncrement = 1, then
if (smallest_unit_value == Unit::Nanosecond && rounding_increment == 1) {
// a. Return ! CreateTemporalZonedDateTime(zonedDateTime.[[EpochNanoseconds]], zonedDateTime.[[TimeZone]], zonedDateTime.[[Calendar]]).
return MUST(create_temporal_zoned_date_time(vm, zoned_date_time->epoch_nanoseconds(), zoned_date_time->time_zone(), zoned_date_time->calendar()));
}
// 14. Let thisNs be zonedDateTime.[[EpochNanoseconds]].
auto const& this_nanoseconds = zoned_date_time->epoch_nanoseconds()->big_integer();
// 15. Let timeZone be zonedDateTime.[[TimeZone]].
auto const& time_zone = zoned_date_time->time_zone();
// 16. Let calendar be zonedDateTime.[[Calendar]].
auto const& calendar = zoned_date_time->calendar();
// 17. Let isoDateTime be GetISODateTimeFor(timeZone, thisNs).
auto iso_date_time = get_iso_date_time_for(time_zone, this_nanoseconds);
Crypto::SignedBigInteger epoch_nanoseconds;
// 18. If smallestUnit is day, then
if (smallest_unit_value == Unit::Day) {
// a. Let dateStart be isoDateTime.[[ISODate]].
auto date_start = iso_date_time.iso_date;
// b. Let dateEnd be BalanceISODate(dateStart.[[Year]], dateStart.[[Month]], dateStart.[[Day]] + 1).
auto date_end = balance_iso_date(date_start.year, date_start.month, static_cast<double>(date_start.day) + 1);
// c. Let startNs be ? GetStartOfDay(timeZone, dateStart).
auto start_nanoseconds = TRY(get_start_of_day(vm, time_zone, date_start));
// d. Assert: thisNs ≥ startNs.
VERIFY(this_nanoseconds >= start_nanoseconds);
// e. Let endNs be ? GetStartOfDay(timeZone, dateEnd).
auto end_nanoseconds = TRY(get_start_of_day(vm, time_zone, date_end));
// f. Assert: thisNs < endNs.
VERIFY(this_nanoseconds < end_nanoseconds);
// g. Let dayLengthNs be (endNs - startNs).
auto day_length_nanoseconds = end_nanoseconds.minus(start_nanoseconds);
// h. Let dayProgressNs be TimeDurationFromEpochNanosecondsDifference(thisNs, startNs).
auto day_progress_nanoseconds = time_duration_from_epoch_nanoseconds_difference(this_nanoseconds, start_nanoseconds);
// i. Let roundedDayNs be ! RoundTimeDurationToIncrement(dayProgressNs, dayLengthNs, roundingMode).
auto rounded_day_nanoseconds = MUST(round_time_duration_to_increment(vm, day_progress_nanoseconds, day_length_nanoseconds.unsigned_value(), rounding_mode));
// j. Let epochNanoseconds be AddTimeDurationToEpochNanoseconds(startNs, roundedDayNs).
epoch_nanoseconds = add_time_duration_to_epoch_nanoseconds(start_nanoseconds, rounded_day_nanoseconds);
}
// 19. Else,
else {
// a. Let roundResult be RoundISODateTime(isoDateTime, roundingIncrement, smallestUnit, roundingMode).
auto round_result = round_iso_date_time(iso_date_time, rounding_increment, smallest_unit_value, rounding_mode);
// b. Let offsetNanoseconds be GetOffsetNanosecondsFor(timeZone, thisNs).
auto offset_nanoseconds = get_offset_nanoseconds_for(time_zone, this_nanoseconds);
// c. Let epochNanoseconds be ? InterpretISODateTimeOffset(roundResult.[[ISODate]], roundResult.[[Time]], OPTION, offsetNanoseconds, timeZone, COMPATIBLE, PREFER, MATCH-EXACTLY).
epoch_nanoseconds = TRY(interpret_iso_date_time_offset(vm, round_result.iso_date, round_result.time, OffsetBehavior::Option, offset_nanoseconds, time_zone, Disambiguation::Compatible, OffsetOption::Prefer, MatchBehavior::MatchExactly));
}
// 20. Return ! CreateTemporalZonedDateTime(epochNanoseconds, timeZone, calendar).
return MUST(create_temporal_zoned_date_time(vm, BigInt::create(vm, move(epoch_nanoseconds)), time_zone, calendar));
}
// 6.3.40 Temporal.ZonedDateTime.prototype.equals ( other ), https://tc39.es/proposal-temporal/#sec-temporal.zoneddatetime.prototype.equals
JS_DEFINE_NATIVE_FUNCTION(ZonedDateTimePrototype::equals)
{

View file

@ -55,6 +55,7 @@ private:
JS_DECLARE_NATIVE_FUNCTION(subtract);
JS_DECLARE_NATIVE_FUNCTION(until);
JS_DECLARE_NATIVE_FUNCTION(since);
JS_DECLARE_NATIVE_FUNCTION(round);
JS_DECLARE_NATIVE_FUNCTION(equals);
JS_DECLARE_NATIVE_FUNCTION(to_string);
JS_DECLARE_NATIVE_FUNCTION(to_locale_string);

View file

@ -0,0 +1,94 @@
describe("correct behavior", () => {
test("length is 1", () => {
expect(Temporal.ZonedDateTime.prototype.round).toHaveLength(1);
});
test("basic functionality", () => {
const zonedDateTime = new Temporal.ZonedDateTime(1111111111111n, "UTC");
expect(zonedDateTime.round({ smallestUnit: "second" }).epochNanoseconds).toBe(
1111000000000n
);
expect(
zonedDateTime.round({ smallestUnit: "second", roundingMode: "ceil" }).epochNanoseconds
).toBe(1112000000000n);
expect(
zonedDateTime.round({
smallestUnit: "minute",
roundingIncrement: 30,
roundingMode: "floor",
}).epochNanoseconds
).toBe(0n);
expect(
zonedDateTime.round({
smallestUnit: "minute",
roundingIncrement: 30,
roundingMode: "halfExpand",
}).epochNanoseconds
).toBe(1800000000000n);
});
test("string argument is implicitly converted to options object", () => {
const zonedDateTime = new Temporal.ZonedDateTime(1111111111111n, "UTC");
expect(
zonedDateTime.round("second").equals(zonedDateTime.round({ smallestUnit: "second" }))
).toBeTrue();
});
});
describe("errors", () => {
test("this value must be a Temporal.ZonedDateTime object", () => {
expect(() => {
Temporal.ZonedDateTime.prototype.round.call("foo");
}).toThrowWithMessage(TypeError, "Not an object of type Temporal.ZonedDateTime");
});
test("missing options object", () => {
const zonedDateTime = new Temporal.ZonedDateTime(1n, "UTC");
expect(() => {
zonedDateTime.round();
}).toThrowWithMessage(TypeError, "Required options object is missing or undefined");
});
test("invalid rounding mode", () => {
const zonedDateTime = new Temporal.ZonedDateTime(1n, "UTC");
expect(() => {
zonedDateTime.round({ smallestUnit: "second", roundingMode: "serenityOS" });
}).toThrowWithMessage(
RangeError,
"serenityOS is not a valid value for option roundingMode"
);
});
test("invalid smallest unit", () => {
const zonedDateTime = new Temporal.ZonedDateTime(1n, "UTC");
expect(() => {
zonedDateTime.round({ smallestUnit: "serenityOS" });
}).toThrowWithMessage(
RangeError,
"serenityOS is not a valid value for option smallestUnit"
);
});
test("increment may not be NaN", () => {
const zonedDateTime = new Temporal.ZonedDateTime(1n, "UTC");
expect(() => {
zonedDateTime.round({ smallestUnit: "second", roundingIncrement: NaN });
}).toThrowWithMessage(RangeError, "NaN is not a valid value for option roundingIncrement");
});
test("increment may smaller than 1 or larger than maximum", () => {
const zonedDateTime = new Temporal.ZonedDateTime(1n, "UTC");
expect(() => {
zonedDateTime.round({ smallestUnit: "second", roundingIncrement: -1 });
}).toThrowWithMessage(RangeError, "-1 is not a valid value for option roundingIncrement");
expect(() => {
zonedDateTime.round({ smallestUnit: "second", roundingIncrement: 0 });
}).toThrowWithMessage(RangeError, "0 is not a valid value for option roundingIncrement");
expect(() => {
zonedDateTime.round({ smallestUnit: "second", roundingIncrement: Infinity });
}).toThrowWithMessage(
RangeError,
"Infinity is not a valid value for option roundingIncrement"
);
});
});