1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-31 19:38:12 +00:00

LibJS: Implement Temporal.ZonedDateTime.prototype.round

This commit is contained in:
Luke Wilde 2021-11-13 19:35:38 +00:00 committed by Linus Groh
parent 3450def494
commit f6ab63993a
4 changed files with 193 additions and 0 deletions

View file

@ -230,6 +230,7 @@
M(TemporalObjectMustNotHave, "Object must not have a defined {} property") \
M(TemporalPropertyMustBeFinite, "Property must not be Infinity") \
M(TemporalPropertyMustBePositiveInteger, "Property must be a positive integer") \
M(TemporalZonedDateTimeRoundZeroLengthDay, "Cannot round a ZonedDateTime in a calendar that has zero-length days") \
M(ThisHasNotBeenInitialized, "|this| has not been initialized") \
M(ThisIsAlreadyInitialized, "|this| is already initialized") \
M(ToObjectNullOrUndefined, "ToObject on null or undefined") \

View file

@ -72,6 +72,7 @@ void ZonedDateTimePrototype::initialize(GlobalObject& global_object)
define_native_function(vm.names.withCalendar, with_calendar, 1, attr);
define_native_function(vm.names.add, add, 1, attr);
define_native_function(vm.names.subtract, subtract, 1, attr);
define_native_function(vm.names.round, round, 1, attr);
define_native_function(vm.names.equals, equals, 1, attr);
define_native_function(vm.names.toString, to_string, 0, attr);
define_native_function(vm.names.toLocaleString, to_locale_string, 0, attr);
@ -941,6 +942,88 @@ JS_DEFINE_NATIVE_FUNCTION(ZonedDateTimePrototype::subtract)
return MUST(create_temporal_zoned_date_time(global_object, *epoch_nanoseconds, time_zone, calendar));
}
// 6.3.39 Temporal.ZonedDateTime.prototype.round ( options ), https://tc39.es/proposal-temporal/#sec-temporal.zoneddatetime.prototype.round
JS_DEFINE_NATIVE_FUNCTION(ZonedDateTimePrototype::round)
{
// 1. Let zonedDateTime be the this value.
// 2. Perform ? RequireInternalSlot(zonedDateTime, [[InitializedTemporalZonedDateTime]]).
auto* zoned_date_time = TRY(typed_this_object(global_object));
// 3. If options is undefined, then
if (vm.argument(0).is_undefined()) {
// a. Throw a TypeError exception.
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 smallestUnit be ? ToSmallestTemporalUnit(options, « "year", "month", "week" », undefined).
auto smallest_unit_value = TRY(to_smallest_temporal_unit(global_object, *options, { "year"sv, "month"sv, "week"sv }, {}));
// 6. If smallestUnit is undefined, throw a RangeError exception.
if (!smallest_unit_value.has_value())
return vm.throw_completion<RangeError>(global_object, ErrorType::OptionIsNotValidValue, vm.names.undefined.as_string(), "smallestUnit");
// NOTE: At this point smallest_unit_value can only be a string
auto& smallest_unit = *smallest_unit_value;
// 7. Let roundingMode be ? ToTemporalRoundingMode(options, "halfExpand").
auto rounding_mode = TRY(to_temporal_rounding_mode(global_object, *options, "halfExpand"sv));
// 8. Let roundingIncrement be ? ToTemporalDateTimeRoundingIncrement(options, smallestUnit).
auto rounding_increment = TRY(to_temporal_date_time_rounding_increment(global_object, *options, smallest_unit));
// 9. Let timeZone be zonedDateTime.[[TimeZone]].
auto& time_zone = zoned_date_time->time_zone();
// 10. Let instant be ! CreateTemporalInstant(zonedDateTime.[[Nanoseconds]]).
auto* instant = MUST(create_temporal_instant(global_object, zoned_date_time->nanoseconds()));
// 11. Let calendar be zonedDateTime.[[Calendar]].
auto& calendar = zoned_date_time->calendar();
// 12. Let temporalDateTime be ? BuiltinTimeZoneGetPlainDateTimeFor(timeZone, instant, calendar).
auto* temporal_date_time = TRY(builtin_time_zone_get_plain_date_time_for(global_object, &time_zone, *instant, calendar));
// 13. Let isoCalendar be ! GetISO8601Calendar().
auto* iso_calendar = get_iso8601_calendar(global_object);
// 14. Let dtStart be ? CreateTemporalDateTime(temporalDateTime.[[ISOYear]], temporalDateTime.[[ISOMonth]], temporalDateTime.[[ISODay]], 0, 0, 0, 0, 0, 0, isoCalendar).
auto* dt_start = TRY(create_temporal_date_time(global_object, temporal_date_time->iso_year(), temporal_date_time->iso_month(), temporal_date_time->iso_day(), 0, 0, 0, 0, 0, 0, *iso_calendar));
// 15. Let instantStart be ? BuiltinTimeZoneGetInstantFor(timeZone, dtStart, "compatible").
auto* instant_start = TRY(builtin_time_zone_get_instant_for(global_object, &time_zone, *dt_start, "compatible"sv));
// 16. Let startNs be instantStart.[[Nanoseconds]].
auto& start_ns = instant_start->nanoseconds();
// 17. Let endNs be ? AddZonedDateTime(startNs, timeZone, zonedDateTime.[[Calendar]], 0, 0, 0, 1, 0, 0, 0, 0, 0, 0).
// TODO: Shouldn't `zonedDateTime.[[Calendar]]` be `calendar` for consistency?
auto* end_ns = TRY(add_zoned_date_time(global_object, start_ns, &time_zone, zoned_date_time->calendar(), 0, 0, 0, 1, 0, 0, 0, 0, 0, 0));
// 18. Let dayLengthNs be (endNs startNs).
auto day_length_ns = end_ns->big_integer().minus(start_ns.big_integer()).to_double();
// 19. If dayLengthNs is 0, then
if (day_length_ns == 0) {
// a. Throw a RangeError exception.
return vm.throw_completion<RangeError>(global_object, ErrorType::TemporalZonedDateTimeRoundZeroLengthDay);
}
// 20. Let roundResult be ! RoundISODateTime(temporalDateTime.[[ISOYear]], temporalDateTime.[[ISOMonth]], temporalDateTime.[[ISODay]], temporalDateTime.[[ISOHour]], temporalDateTime.[[ISOMinute]], temporalDateTime.[[ISOSecond]], temporalDateTime.[[ISOMillisecond]], temporalDateTime.[[ISOMicrosecond]], temporalDateTime.[[ISONanosecond]], roundingIncrement, smallestUnit, roundingMode, dayLengthNs).
auto round_result = round_iso_date_time(temporal_date_time->iso_year(), temporal_date_time->iso_month(), temporal_date_time->iso_day(), temporal_date_time->iso_hour(), temporal_date_time->iso_minute(), temporal_date_time->iso_second(), temporal_date_time->iso_millisecond(), temporal_date_time->iso_microsecond(), temporal_date_time->iso_nanosecond(), rounding_increment, smallest_unit, rounding_mode, day_length_ns);
// 21. Let offsetNanoseconds be ? GetOffsetNanosecondsFor(timeZone, instant).
auto offset_nanoseconds = TRY(get_offset_nanoseconds_for(global_object, &time_zone, *instant));
// 22. Let epochNanoseconds be ? InterpretISODateTimeOffset(roundResult.[[Year]], roundResult.[[Month]], roundResult.[[Day]], roundResult.[[Hour]], roundResult.[[Minute]], roundResult.[[Second]], roundResult.[[Millisecond]], roundResult.[[Microsecond]], roundResult.[[Nanosecond]], option, offsetNanoseconds, timeZone, "compatible", "prefer", match exactly).
auto* epoch_nanoseconds = TRY(interpret_iso_date_time_offset(global_object, round_result.year, round_result.month, round_result.day, round_result.hour, round_result.minute, round_result.second, round_result.millisecond, round_result.microsecond, round_result.nanosecond, OffsetBehavior::Option, offset_nanoseconds, &time_zone, "compatible"sv, "prefer"sv, MatchBehavior::MatchExactly));
// 23. Return ! CreateTemporalZonedDateTime(epochNanoseconds, timeZone, calendar).
return MUST(create_temporal_zoned_date_time(global_object, *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

@ -56,6 +56,7 @@ private:
JS_DECLARE_NATIVE_FUNCTION(with_calendar);
JS_DECLARE_NATIVE_FUNCTION(add);
JS_DECLARE_NATIVE_FUNCTION(subtract);
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,108 @@
describe("correct behavior", () => {
test("length is 1", () => {
expect(Temporal.ZonedDateTime.prototype.round).toHaveLength(1);
});
test("basic functionality", () => {
const zonedDateTime = new Temporal.ZonedDateTime(
1111111111111n,
new Temporal.TimeZone("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);
});
});
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, new Temporal.TimeZone("UTC"));
expect(() => {
zonedDateTime.round();
}).toThrowWithMessage(TypeError, "Required options object is missing or undefined");
});
test("invalid rounding mode", () => {
const zonedDateTime = new Temporal.ZonedDateTime(1n, new Temporal.TimeZone("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, new Temporal.TimeZone("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, new Temporal.TimeZone("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, new Temporal.TimeZone("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, "inf is not a valid value for option roundingIncrement");
});
test("calendar with zero-length days", () => {
const calendar = {
dateAdd(date) {
return date;
},
};
const zonedDateTime = new Temporal.ZonedDateTime(
1n,
new Temporal.TimeZone("UTC"),
calendar
);
expect(() => {
zonedDateTime.round({ smallestUnit: "second" });
}).toThrowWithMessage(
RangeError,
"Cannot round a ZonedDateTime in a calendar that has zero-length days"
);
});
});