mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-04-22 12:35:14 +00:00
LibJS: Implement Temporal.ZonedDateTime.prototype.round
This commit is contained in:
parent
eadd0c40c9
commit
f2ab9e1aa9
Notes:
github-actions[bot]
2024-11-26 10:03:33 +00:00
Author: https://github.com/trflynn89 Commit: https://github.com/LadybirdBrowser/ladybird/commit/f2ab9e1aa97 Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/2577 Reviewed-by: https://github.com/shannonbooth ✅
3 changed files with 240 additions and 0 deletions
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue