From d1a5254e416e56be89a27ce5da9097deb4ab5e02 Mon Sep 17 00:00:00 2001 From: Luke Wilde Date: Wed, 3 Nov 2021 18:22:54 +0000 Subject: [PATCH] LibJS: Implement Temporal.PlainDateTime.prototype.round --- .../Runtime/Temporal/AbstractOperations.cpp | 33 +++++ .../Runtime/Temporal/AbstractOperations.h | 1 + .../Temporal/PlainDateTimePrototype.cpp | 41 ++++++ .../Runtime/Temporal/PlainDateTimePrototype.h | 1 + .../PlainDateTime.prototype.round.js | 139 ++++++++++++++++++ 5 files changed, 215 insertions(+) create mode 100644 Userland/Libraries/LibJS/Tests/builtins/Temporal/PlainDateTime/PlainDateTime.prototype.round.js diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp index c41634c2f7..953bc89f39 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp @@ -284,6 +284,39 @@ ThrowCompletionOr to_temporal_rounding_increment(GlobalObject& global_objec return floored_increment; } +// 13.15 ToTemporalDateTimeRoundingIncrement ( normalizedOptions, smallestUnit ), https://tc39.es/proposal-temporal/#sec-temporal-totemporaldatetimeroundingincrement +ThrowCompletionOr to_temporal_date_time_rounding_increment(GlobalObject& global_object, Object const& normalized_options, StringView smallest_unit) +{ + double maximum; + + // 1. If smallestUnit is "day", then + if (smallest_unit == "day"sv) { + // a. Let maximum be 1. + maximum = 1; + } + // 2. Else if smallestUnit is "hour", then + else if (smallest_unit == "hour"sv) { + // a. Let maximum be 24. + maximum = 24; + } + // 3. Else if smallestUnit is "minute" or "second", then + else if (smallest_unit.is_one_of("minute"sv, "second"sv)) { + // a. Let maximum be 60. + maximum = 60; + } + // 4. Else, + else { + // a. Assert: smallestUnit is "millisecond", "microsecond", or "nanosecond". + VERIFY(smallest_unit.is_one_of("millisecond"sv, "microsecond"sv, "nanosecond"sv)); + + // b. Let maximum be 1000. + maximum = 1000; + } + + // 5. Return ? ToTemporalRoundingIncrement(normalizedOptions, maximum, false). + return to_temporal_rounding_increment(global_object, normalized_options, maximum, false); +} + // 13.16 ToSecondsStringPrecision ( normalizedOptions ), https://tc39.es/proposal-temporal/#sec-temporal-tosecondsstringprecision ThrowCompletionOr to_seconds_string_precision(GlobalObject& global_object, Object const& normalized_options) { diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h index bf6f9939fd..b9ff753fcd 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h @@ -94,6 +94,7 @@ ThrowCompletionOr to_temporal_disambiguation(GlobalObject&, Object const ThrowCompletionOr to_temporal_rounding_mode(GlobalObject&, Object const& normalized_options, String const& fallback); ThrowCompletionOr to_show_calendar_option(GlobalObject&, Object const& normalized_options); ThrowCompletionOr to_temporal_rounding_increment(GlobalObject&, Object const& normalized_options, Optional dividend, bool inclusive); +ThrowCompletionOr to_temporal_date_time_rounding_increment(GlobalObject&, Object const& normalized_options, StringView smallest_unit); ThrowCompletionOr to_seconds_string_precision(GlobalObject&, Object const& normalized_options); ThrowCompletionOr to_largest_temporal_unit(GlobalObject&, Object const& normalized_options, Vector const& disallowed_units, String const& fallback, Optional auto_value); ThrowCompletionOr> to_smallest_temporal_unit(GlobalObject&, Object const& normalized_options, Vector const& disallowed_units, Optional fallback); diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDateTimePrototype.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDateTimePrototype.cpp index 600b5da1c8..9401e548a8 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDateTimePrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDateTimePrototype.cpp @@ -1,5 +1,6 @@ /* * Copyright (c) 2021, Linus Groh + * Copyright (c) 2021, Luke Wilde * * SPDX-License-Identifier: BSD-2-Clause */ @@ -62,6 +63,7 @@ void PlainDateTimePrototype::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); @@ -451,6 +453,45 @@ JS_DEFINE_NATIVE_FUNCTION(PlainDateTimePrototype::subtract) return TRY(create_temporal_date_time(global_object, result.year, result.month, result.day, result.hour, result.minute, result.second, result.millisecond, result.microsecond, result.nanosecond, date_time->calendar())); } +// 5.3.30 Temporal.PlainDateTime.prototype.round ( options ), https://tc39.es/proposal-temporal/#sec-temporal.plaindatetime.prototype.round +JS_DEFINE_NATIVE_FUNCTION(PlainDateTimePrototype::round) +{ + // 1. Let dateTime be the this value. + // 2. Perform ? RequireInternalSlot(dateTime, [[InitializedTemporalDateTime]]). + auto* 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")); + + // 8. Let roundingIncrement be ? ToTemporalDateTimeRoundingIncrement(options, smallestUnit). + auto rounding_increment = TRY(to_temporal_date_time_rounding_increment(global_object, *options, smallest_unit)); + + // 9. Let result be ! RoundISODateTime(dateTime.[[ISOYear]], dateTime.[[ISOMonth]], dateTime.[[ISODay]], dateTime.[[ISOHour]], dateTime.[[ISOMinute]], dateTime.[[ISOSecond]], dateTime.[[ISOMillisecond]], dateTime.[[ISOMicrosecond]], dateTime.[[ISONanosecond]], roundingIncrement, smallestUnit, roundingMode). + auto result = round_iso_date_time(date_time->iso_year(), date_time->iso_month(), date_time->iso_day(), date_time->iso_hour(), date_time->iso_minute(), date_time->iso_second(), date_time->iso_millisecond(), date_time->iso_microsecond(), date_time->iso_nanosecond(), rounding_increment, smallest_unit, rounding_mode); + + // 10. Return ? CreateTemporalDateTime(result.[[Year]], result.[[Month]], result.[[Day]], result.[[Hour]], result.[[Minute]], result.[[Second]], result.[[Millisecond]], result.[[Microsecond]], result.[[Nanosecond]], dateTime.[[Calendar]]). + return TRY(create_temporal_date_time(global_object, result.year, result.month, result.day, result.hour, result.minute, result.second, result.millisecond, result.microsecond, result.nanosecond, date_time->calendar())); +} + // 5.3.31 Temporal.PlainDateTime.prototype.equals ( other ), https://tc39.es/proposal-temporal/#sec-temporal.plaindatetime.prototype.equals JS_DEFINE_NATIVE_FUNCTION(PlainDateTimePrototype::equals) { diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDateTimePrototype.h b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDateTimePrototype.h index 42a9743355..123f9d6186 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDateTimePrototype.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDateTimePrototype.h @@ -46,6 +46,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/PlainDateTime/PlainDateTime.prototype.round.js b/Userland/Libraries/LibJS/Tests/builtins/Temporal/PlainDateTime/PlainDateTime.prototype.round.js new file mode 100644 index 0000000000..50f4397f19 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Temporal/PlainDateTime/PlainDateTime.prototype.round.js @@ -0,0 +1,139 @@ +describe("correct behavior", () => { + test("length is 1", () => { + expect(Temporal.PlainDateTime.prototype.round).toHaveLength(1); + }); + + test("basic functionality", () => { + const plainDateTime = new Temporal.PlainDateTime(2021, 11, 3, 18, 8, 10, 100, 200, 300); + + const firstRoundedPlainDateTime = plainDateTime.round({ smallestUnit: "minute" }); + expect(firstRoundedPlainDateTime.year).toBe(2021); + expect(firstRoundedPlainDateTime.month).toBe(11); + expect(firstRoundedPlainDateTime.monthCode).toBe("M11"); + expect(firstRoundedPlainDateTime.day).toBe(3); + expect(firstRoundedPlainDateTime.hour).toBe(18); + expect(firstRoundedPlainDateTime.minute).toBe(8); + expect(firstRoundedPlainDateTime.second).toBe(0); + expect(firstRoundedPlainDateTime.millisecond).toBe(0); + expect(firstRoundedPlainDateTime.microsecond).toBe(0); + expect(firstRoundedPlainDateTime.nanosecond).toBe(0); + + const secondRoundedPlainDateTime = plainDateTime.round({ + smallestUnit: "minute", + roundingMode: "ceil", + }); + expect(secondRoundedPlainDateTime.year).toBe(2021); + expect(secondRoundedPlainDateTime.month).toBe(11); + expect(secondRoundedPlainDateTime.monthCode).toBe("M11"); + expect(secondRoundedPlainDateTime.day).toBe(3); + expect(secondRoundedPlainDateTime.hour).toBe(18); + expect(secondRoundedPlainDateTime.minute).toBe(9); + expect(secondRoundedPlainDateTime.second).toBe(0); + expect(secondRoundedPlainDateTime.millisecond).toBe(0); + expect(secondRoundedPlainDateTime.microsecond).toBe(0); + expect(secondRoundedPlainDateTime.nanosecond).toBe(0); + + const thirdRoundedPlainDateTime = plainDateTime.round({ + smallestUnit: "minute", + roundingMode: "ceil", + roundingIncrement: 30, + }); + expect(thirdRoundedPlainDateTime.year).toBe(2021); + expect(thirdRoundedPlainDateTime.month).toBe(11); + expect(thirdRoundedPlainDateTime.monthCode).toBe("M11"); + expect(thirdRoundedPlainDateTime.day).toBe(3); + expect(thirdRoundedPlainDateTime.hour).toBe(18); + expect(thirdRoundedPlainDateTime.minute).toBe(30); + expect(thirdRoundedPlainDateTime.second).toBe(0); + expect(thirdRoundedPlainDateTime.millisecond).toBe(0); + expect(thirdRoundedPlainDateTime.microsecond).toBe(0); + expect(thirdRoundedPlainDateTime.nanosecond).toBe(0); + + const fourthRoundedPlainDateTime = plainDateTime.round({ + smallestUnit: "minute", + roundingMode: "floor", + roundingIncrement: 30, + }); + expect(fourthRoundedPlainDateTime.year).toBe(2021); + expect(fourthRoundedPlainDateTime.month).toBe(11); + expect(fourthRoundedPlainDateTime.monthCode).toBe("M11"); + expect(fourthRoundedPlainDateTime.day).toBe(3); + expect(fourthRoundedPlainDateTime.hour).toBe(18); + expect(fourthRoundedPlainDateTime.minute).toBe(0); + expect(fourthRoundedPlainDateTime.second).toBe(0); + expect(fourthRoundedPlainDateTime.millisecond).toBe(0); + expect(fourthRoundedPlainDateTime.microsecond).toBe(0); + expect(fourthRoundedPlainDateTime.nanosecond).toBe(0); + + const fifthRoundedPlainDateTime = plainDateTime.round({ + smallestUnit: "hour", + roundingMode: "halfExpand", + roundingIncrement: 4, + }); + expect(fifthRoundedPlainDateTime.year).toBe(2021); + expect(fifthRoundedPlainDateTime.month).toBe(11); + expect(fifthRoundedPlainDateTime.monthCode).toBe("M11"); + expect(fifthRoundedPlainDateTime.day).toBe(3); + expect(fifthRoundedPlainDateTime.hour).toBe(20); + expect(fifthRoundedPlainDateTime.minute).toBe(0); + expect(fifthRoundedPlainDateTime.second).toBe(0); + expect(fifthRoundedPlainDateTime.millisecond).toBe(0); + expect(fifthRoundedPlainDateTime.microsecond).toBe(0); + expect(fifthRoundedPlainDateTime.nanosecond).toBe(0); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainDateTime object", () => { + expect(() => { + Temporal.PlainDateTime.prototype.round.call("foo", {}); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainDateTime"); + }); + + test("missing options object", () => { + expect(() => { + const plainDateTime = new Temporal.PlainDateTime(2021, 11, 3, 18, 8, 10, 100, 200, 300); + plainDateTime.round(); + }).toThrowWithMessage(TypeError, "Required options object is missing or undefined"); + }); + + test("invalid rounding mode", () => { + expect(() => { + const plainDateTime = new Temporal.PlainDateTime(2021, 11, 3, 18, 8, 10, 100, 200, 300); + plainDateTime.round({ smallestUnit: "second", roundingMode: "serenityOS" }); + }).toThrowWithMessage( + RangeError, + "serenityOS is not a valid value for option roundingMode" + ); + }); + + test("invalid smallest unit", () => { + expect(() => { + const plainDateTime = new Temporal.PlainDateTime(2021, 11, 3, 18, 8, 10, 100, 200, 300); + plainDateTime.round({ smallestUnit: "serenityOS" }); + }).toThrowWithMessage( + RangeError, + "serenityOS is not a valid value for option smallestUnit" + ); + }); + + test("increment may not be NaN", () => { + expect(() => { + const plainDateTime = new Temporal.PlainDateTime(2021, 11, 3, 18, 8, 10, 100, 200, 300); + plainDateTime.round({ smallestUnit: "second", roundingIncrement: NaN }); + }).toThrowWithMessage(RangeError, "NaN is not a valid value for option roundingIncrement"); + }); + + test("increment may not be smaller than 1 or larger than maximum", () => { + const plainDateTime = new Temporal.PlainDateTime(2021, 11, 3, 18, 8, 10, 100, 200, 300); + expect(() => { + plainDateTime.round({ smallestUnit: "second", roundingIncrement: -1 }); + }).toThrowWithMessage(RangeError, "-1 is not a valid value for option roundingIncrement"); + expect(() => { + plainDateTime.round({ smallestUnit: "second", roundingIncrement: 0 }); + }).toThrowWithMessage(RangeError, "0 is not a valid value for option roundingIncrement"); + expect(() => { + plainDateTime.round({ smallestUnit: "second", roundingIncrement: Infinity }); + }).toThrowWithMessage(RangeError, "inf is not a valid value for option roundingIncrement"); + }); +});