From f6ab63993a2ef733e5b2394af7cceaca0ef6489c Mon Sep 17 00:00:00 2001 From: Luke Wilde Date: Sat, 13 Nov 2021 19:35:38 +0000 Subject: [PATCH] LibJS: Implement Temporal.ZonedDateTime.prototype.round --- Userland/Libraries/LibJS/Runtime/ErrorTypes.h | 1 + .../Temporal/ZonedDateTimePrototype.cpp | 83 ++++++++++++++ .../Runtime/Temporal/ZonedDateTimePrototype.h | 1 + .../ZonedDateTime.prototype.round.js | 108 ++++++++++++++++++ 4 files changed, 193 insertions(+) create mode 100644 Userland/Libraries/LibJS/Tests/builtins/Temporal/ZonedDateTime/ZonedDateTime.prototype.round.js diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index 22def7516d..32d2da7c62 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -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") \ diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/ZonedDateTimePrototype.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/ZonedDateTimePrototype.cpp index 453ab8328c..ca80f79bc4 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/ZonedDateTimePrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/ZonedDateTimePrototype.cpp @@ -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(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(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(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) { diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/ZonedDateTimePrototype.h b/Userland/Libraries/LibJS/Runtime/Temporal/ZonedDateTimePrototype.h index 2dd410992a..be65dee5fd 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/ZonedDateTimePrototype.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/ZonedDateTimePrototype.h @@ -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); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Temporal/ZonedDateTime/ZonedDateTime.prototype.round.js b/Userland/Libraries/LibJS/Tests/builtins/Temporal/ZonedDateTime/ZonedDateTime.prototype.round.js new file mode 100644 index 0000000000..e608855927 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Temporal/ZonedDateTime/ZonedDateTime.prototype.round.js @@ -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" + ); + }); +});