Browse Source

LibJS: Implement Temporal.ZonedDateTime.prototype.round

Luke Wilde 3 years ago
parent
commit
f6ab63993a

+ 1 - 0
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")                                                                         \

+ 83 - 0
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<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)
 {

+ 1 - 0
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);

+ 108 - 0
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"
+        );
+    });
+});