From d314fcce7acc30a79ee4c58b1d547bd46def56d3 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Sat, 23 Nov 2024 18:20:39 -0500 Subject: [PATCH] LibJS: Implement stringification Temporal.PlainDateTime prototypes --- .../LibJS/Runtime/Temporal/PlainDateTime.cpp | 42 +++++++++ .../LibJS/Runtime/Temporal/PlainDateTime.h | 2 + .../Temporal/PlainDateTimePrototype.cpp | 73 ++++++++++++++++ .../Runtime/Temporal/PlainDateTimePrototype.h | 3 + .../PlainDateTime.prototype.toJSON.js | 18 ++++ .../PlainDateTime.prototype.toLocaleString.js | 18 ++++ .../PlainDateTime.prototype.toString.js | 85 +++++++++++++++++++ 7 files changed, 241 insertions(+) create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainDateTime/PlainDateTime.prototype.toJSON.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainDateTime/PlainDateTime.prototype.toLocaleString.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainDateTime/PlainDateTime.prototype.toString.js diff --git a/Libraries/LibJS/Runtime/Temporal/PlainDateTime.cpp b/Libraries/LibJS/Runtime/Temporal/PlainDateTime.cpp index b314249bacb..f4335b72738 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainDateTime.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainDateTime.cpp @@ -216,6 +216,32 @@ ThrowCompletionOr> create_temporal_date_time(VM& vm, ISOD return object; } +// 5.5.9 ISODateTimeToString ( isoDateTime, calendar, precision, showCalendar ), https://tc39.es/proposal-temporal/#sec-temporal-isodatetimetostring +String iso_date_time_to_string(ISODateTime const& iso_date_time, StringView calendar, SecondsStringPrecision::Precision precision, ShowCalendar show_calendar) +{ + // 1. Let yearString be PadISOYear(isoDateTime.[[ISODate]].[[Year]]). + auto year_string = pad_iso_year(iso_date_time.iso_date.year); + + // 2. Let monthString be ToZeroPaddedDecimalString(isoDateTime.[[ISODate]].[[Month]], 2). + auto month = iso_date_time.iso_date.month; + + // 3. Let dayString be ToZeroPaddedDecimalString(isoDateTime.[[ISODate]].[[Day]], 2). + auto day = iso_date_time.iso_date.day; + + // 4. Let subSecondNanoseconds be isoDateTime.[[Time]].[[Millisecond]] × 10**6 + isoDateTime.[[Time]].[[Microsecond]] × 10**3 + isoDateTime.[[Time]].[[Nanosecond]]. + auto sub_second_nanoseconds = (static_cast(iso_date_time.time.millisecond) * 1'000'000) + (static_cast(iso_date_time.time.microsecond) * 1000) + static_cast(iso_date_time.time.nanosecond); + + // 5. Let timeString be FormatTimeString(isoDateTime.[[Time]].[[Hour]], isoDateTime.[[Time]].[[Minute]], isoDateTime.[[Time]].[[Second]], subSecondNanoseconds, precision). + auto time_string = format_time_string(iso_date_time.time.hour, iso_date_time.time.minute, iso_date_time.time.second, sub_second_nanoseconds, precision); + + // 6. Let calendarString be FormatCalendarAnnotation(calendar, showCalendar). + auto calendar_string = format_calendar_annotation(calendar, show_calendar); + + // 7. Return the string-concatenation of yearString, the code unit 0x002D (HYPHEN-MINUS), monthString, the code unit 0x002D (HYPHEN-MINUS), + // dayString, 0x0054 (LATIN CAPITAL LETTER T), timeString, and calendarString. + return MUST(String::formatted("{}-{:02}-{:02}T{}{}", year_string, month, day, time_string, calendar_string)); +} + // 5.5.10 CompareISODateTime ( isoDateTime1, isoDateTime2 ), https://tc39.es/proposal-temporal/#sec-temporal-compareisodatetime i8 compare_iso_date_time(ISODateTime const& iso_date_time1, ISODateTime const& iso_date_time2) { @@ -230,6 +256,22 @@ i8 compare_iso_date_time(ISODateTime const& iso_date_time1, ISODateTime const& i return compare_time_record(iso_date_time1.time, iso_date_time2.time); } +// 5.5.11 RoundISODateTime ( isoDateTime, increment, unit, roundingMode ), https://tc39.es/proposal-temporal/#sec-temporal-roundisodatetime +ISODateTime round_iso_date_time(ISODateTime const& iso_date_time, u64 increment, Unit unit, RoundingMode rounding_mode) +{ + // 1. Assert: ISODateTimeWithinLimits(isoDateTime) is true. + VERIFY(iso_date_time_within_limits(iso_date_time)); + + // 2. Let roundedTime be RoundTime(isoDateTime.[[Time]], increment, unit, roundingMode). + auto rounded_time = round_time(iso_date_time.time, increment, unit, rounding_mode); + + // 3. Let balanceResult be BalanceISODate(isoDateTime.[[ISODate]].[[Year]], isoDateTime.[[ISODate]].[[Month]], isoDateTime.[[ISODate]].[[Day]] + roundedTime.[[Days]]). + auto balance_result = balance_iso_date(iso_date_time.iso_date.year, iso_date_time.iso_date.month, iso_date_time.iso_date.day + rounded_time.days); + + // 4. Return CombineISODateAndTimeRecord(balanceResult, roundedTime). + return combine_iso_date_and_time_record(balance_result, rounded_time); +} + // 5.5.12 DifferenceISODateTime ( isoDateTime1, isoDateTime2, calendar, largestUnit ), https://tc39.es/proposal-temporal/#sec-temporal-differenceisodatetime ThrowCompletionOr difference_iso_date_time(VM& vm, ISODateTime const& iso_date_time1, ISODateTime const& iso_date_time2, StringView calendar, Unit largest_unit) { diff --git a/Libraries/LibJS/Runtime/Temporal/PlainDateTime.h b/Libraries/LibJS/Runtime/Temporal/PlainDateTime.h index f9b964d66e9..7e90c750b48 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainDateTime.h +++ b/Libraries/LibJS/Runtime/Temporal/PlainDateTime.h @@ -38,7 +38,9 @@ ThrowCompletionOr interpret_temporal_date_time_fields(VM&, StringVi ThrowCompletionOr> to_temporal_date_time(VM&, Value item, Value options = js_undefined()); ISODateTime balance_iso_date_time(double year, double month, double day, double hour, double minute, double second, double millisecond, double microsecond, double nanosecond); ThrowCompletionOr> create_temporal_date_time(VM&, ISODateTime const&, String calendar, GC::Ptr new_target = {}); +String iso_date_time_to_string(ISODateTime const&, StringView calendar, SecondsStringPrecision::Precision, ShowCalendar); i8 compare_iso_date_time(ISODateTime const&, ISODateTime const&); +ISODateTime round_iso_date_time(ISODateTime const&, u64 increment, Unit, RoundingMode); ThrowCompletionOr difference_iso_date_time(VM&, ISODateTime const&, ISODateTime const&, StringView calendar, Unit largest_unit); ThrowCompletionOr difference_plain_date_time_with_rounding(VM&, ISODateTime const&, ISODateTime const&, StringView calendar, Unit largest_unit, u64 rounding_increment, Unit smallest_unit, RoundingMode); ThrowCompletionOr difference_plain_date_time_with_total(VM&, ISODateTime const&, ISODateTime const&, StringView calendar, Unit); diff --git a/Libraries/LibJS/Runtime/Temporal/PlainDateTimePrototype.cpp b/Libraries/LibJS/Runtime/Temporal/PlainDateTimePrototype.cpp index e1a62ea93a4..a8393e888ee 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainDateTimePrototype.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainDateTimePrototype.cpp @@ -6,6 +6,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include @@ -50,6 +51,11 @@ void PlainDateTimePrototype::initialize(Realm& realm) define_native_accessor(realm, vm.names.daysInYear, days_in_year_getter, {}, Attribute::Configurable); define_native_accessor(realm, vm.names.monthsInYear, months_in_year_getter, {}, Attribute::Configurable); define_native_accessor(realm, vm.names.inLeapYear, in_leap_year_getter, {}, Attribute::Configurable); + + u8 attr = Attribute::Writable | Attribute::Configurable; + define_native_function(realm, vm.names.toString, to_string, 0, attr); + define_native_function(realm, vm.names.toLocaleString, to_locale_string, 0, attr); + define_native_function(realm, vm.names.toJSON, to_json, 0, attr); } // 5.3.3 get Temporal.PlainDateTime.prototype.calendarId, https://tc39.es/proposal-temporal/#sec-get-temporal.plaindatetime.prototype.calendarid @@ -207,4 +213,71 @@ JS_DEFINE_NATIVE_FUNCTION(PlainDateTimePrototype::year_of_week_getter) return *result; } +// 5.3.34 Temporal.PlainDateTime.prototype.toString ( [ options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plaindatetime.prototype.tostring +JS_DEFINE_NATIVE_FUNCTION(PlainDateTimePrototype::to_string) +{ + // 1. Let dateTime be the this value. + // 2. Perform ? RequireInternalSlot(dateTime, [[InitializedTemporalDateTime]]). + auto date_time = TRY(typed_this_object(vm)); + + // 3. Let resolvedOptions be ? GetOptionsObject(options). + auto resolved_options = TRY(get_options_object(vm, vm.argument(0))); + + // 4. NOTE: The following steps read options and perform independent validation in alphabetical order + // (GetTemporalShowCalendarNameOption reads "calendarName", GetTemporalFractionalSecondDigitsOption reads + // "fractionalSecondDigits", and GetRoundingModeOption reads "roundingMode"). + + // 5. Let showCalendar be ? GetTemporalShowCalendarNameOption(resolvedOptions). + auto show_calendar = TRY(get_temporal_show_calendar_name_option(vm, resolved_options)); + + // 6. Let digits be ? GetTemporalFractionalSecondDigitsOption(resolvedOptions). + auto digits = TRY(get_temporal_fractional_second_digits_option(vm, resolved_options)); + + // 7. Let roundingMode be ? GetRoundingModeOption(resolvedOptions, TRUNC). + auto rounding_mode = TRY(get_rounding_mode_option(vm, resolved_options, RoundingMode::Trunc)); + + // 8. Let smallestUnit be ? GetTemporalUnitValuedOption(resolvedOptions, "smallestUnit", TIME, UNSET). + auto smallest_unit = TRY(get_temporal_unit_valued_option(vm, resolved_options, vm.names.smallestUnit, UnitGroup::Time, Unset {})); + + // 9. If smallestUnit is HOUR, throw a RangeError exception. + if (auto const* unit = smallest_unit.get_pointer(); unit && *unit == Unit::Hour) + return vm.throw_completion(ErrorType::OptionIsNotValidValue, temporal_unit_to_string(*unit), vm.names.smallestUnit); + + // 10. Let precision be ToSecondsStringPrecisionRecord(smallestUnit, digits). + auto precision = to_seconds_string_precision_record(smallest_unit, digits); + + // 11. Let result be RoundISODateTime(dateTime.[[ISODateTime]], precision.[[Increment]], precision.[[Unit]], roundingMode). + auto result = round_iso_date_time(date_time->iso_date_time(), precision.increment, precision.unit, rounding_mode); + + // 12. If ISODateTimeWithinLimits(result) is false, throw a RangeError exception. + if (!iso_date_time_within_limits(result)) + return vm.throw_completion(ErrorType::TemporalInvalidPlainDateTime); + + // 13. Return ISODateTimeToString(result, dateTime.[[Calendar]], precision.[[Precision]], showCalendar). + return PrimitiveString::create(vm, iso_date_time_to_string(result, date_time->calendar(), precision.precision, show_calendar)); +} + +// 5.3.35 Temporal.PlainDateTime.prototype.toLocaleString ( [ locales [ , options ] ] ), https://tc39.es/proposal-temporal/#sec-temporal.plaindatetime.prototype.tolocalestring +// NOTE: This is the minimum toLocaleString implementation for engines without ECMA-402. +JS_DEFINE_NATIVE_FUNCTION(PlainDateTimePrototype::to_locale_string) +{ + // 1. Let dateTime be the this value. + // 2. Perform ? RequireInternalSlot(dateTime, [[InitializedTemporalDateTime]]). + auto date_time = TRY(typed_this_object(vm)); + + // 3. Return ISODateTimeToString(dateTime.[[ISODateTime]], dateTime.[[Calendar]], AUTO, AUTO). + return PrimitiveString::create(vm, iso_date_time_to_string(date_time->iso_date_time(), date_time->calendar(), Auto {}, ShowCalendar::Auto)); +} + +// 5.3.36 Temporal.PlainDateTime.prototype.toJSON ( ), https://tc39.es/proposal-temporal/#sec-temporal.plaindatetime.prototype.tojson +JS_DEFINE_NATIVE_FUNCTION(PlainDateTimePrototype::to_json) +{ + // 1. Let dateTime be the this value. + // 2. Perform ? RequireInternalSlot(dateTime, [[InitializedTemporalDateTime]]). + auto date_time = TRY(typed_this_object(vm)); + + // 3. Return ISODateTimeToString(dateTime.[[ISODateTime]], dateTime.[[Calendar]], AUTO, AUTO). + return PrimitiveString::create(vm, iso_date_time_to_string(date_time->iso_date_time(), date_time->calendar(), Auto {}, ShowCalendar::Auto)); +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainDateTimePrototype.h b/Libraries/LibJS/Runtime/Temporal/PlainDateTimePrototype.h index 5c23f998450..00a6725c7c1 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainDateTimePrototype.h +++ b/Libraries/LibJS/Runtime/Temporal/PlainDateTimePrototype.h @@ -45,6 +45,9 @@ private: JS_DECLARE_NATIVE_FUNCTION(days_in_year_getter); JS_DECLARE_NATIVE_FUNCTION(months_in_year_getter); JS_DECLARE_NATIVE_FUNCTION(in_leap_year_getter); + JS_DECLARE_NATIVE_FUNCTION(to_string); + JS_DECLARE_NATIVE_FUNCTION(to_locale_string); + JS_DECLARE_NATIVE_FUNCTION(to_json); }; } diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainDateTime/PlainDateTime.prototype.toJSON.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainDateTime/PlainDateTime.prototype.toJSON.js new file mode 100644 index 00000000000..b0ce506a78d --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainDateTime/PlainDateTime.prototype.toJSON.js @@ -0,0 +1,18 @@ +describe("correct behavior", () => { + test("length is 0", () => { + expect(Temporal.PlainDateTime.prototype.toJSON).toHaveLength(0); + }); + + test("basic functionality", () => { + const plainDateTime = new Temporal.PlainDateTime(2021, 11, 3, 1, 33, 5, 100, 200, 300); + expect(plainDateTime.toJSON()).toBe("2021-11-03T01:33:05.1002003"); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainDateTime object", () => { + expect(() => { + Temporal.PlainDateTime.prototype.toJSON.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainDateTime"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainDateTime/PlainDateTime.prototype.toLocaleString.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainDateTime/PlainDateTime.prototype.toLocaleString.js new file mode 100644 index 00000000000..1f35fa767fe --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainDateTime/PlainDateTime.prototype.toLocaleString.js @@ -0,0 +1,18 @@ +describe("correct behavior", () => { + test("length is 0", () => { + expect(Temporal.PlainDateTime.prototype.toLocaleString).toHaveLength(0); + }); + + test("basic functionality", () => { + const plainDateTime = new Temporal.PlainDateTime(2021, 11, 3, 1, 33, 5, 100, 200, 300); + expect(plainDateTime.toLocaleString()).toBe("2021-11-03T01:33:05.1002003"); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainDateTime object", () => { + expect(() => { + Temporal.PlainDateTime.prototype.toLocaleString.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainDateTime"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainDateTime/PlainDateTime.prototype.toString.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainDateTime/PlainDateTime.prototype.toString.js new file mode 100644 index 00000000000..c43ff6d4436 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainDateTime/PlainDateTime.prototype.toString.js @@ -0,0 +1,85 @@ +describe("correct behavior", () => { + test("length is 0", () => { + expect(Temporal.PlainDateTime.prototype.toString).toHaveLength(0); + }); + + test("basic functionality", () => { + const plainDateTime = new Temporal.PlainDateTime(2021, 11, 3, 1, 33, 5, 100, 200, 300); + expect(plainDateTime.toString()).toBe("2021-11-03T01:33:05.1002003"); + }); + + test("fractionalSecondDigits option", () => { + const plainDateTime = new Temporal.PlainDateTime(2021, 11, 3, 1, 33, 5, 100, 200, 300); + const values = [ + ["auto", "2021-11-03T01:33:05.1002003"], + [0, "2021-11-03T01:33:05"], + [1, "2021-11-03T01:33:05.1"], + [2, "2021-11-03T01:33:05.10"], + [3, "2021-11-03T01:33:05.100"], + [4, "2021-11-03T01:33:05.1002"], + [5, "2021-11-03T01:33:05.10020"], + [6, "2021-11-03T01:33:05.100200"], + [7, "2021-11-03T01:33:05.1002003"], + [8, "2021-11-03T01:33:05.10020030"], + [9, "2021-11-03T01:33:05.100200300"], + ]; + + for (const [fractionalSecondDigits, expected] of values) { + const options = { fractionalSecondDigits }; + expect(plainDateTime.toString(options)).toBe(expected); + } + + // Ignored when smallestUnit is given + expect(plainDateTime.toString({ smallestUnit: "minute", fractionalSecondDigits: 9 })).toBe( + "2021-11-03T01:33" + ); + }); + + test("smallestUnit option", () => { + const plainDateTime = new Temporal.PlainDateTime(2021, 11, 3, 1, 33, 5, 100, 200, 300); + const values = [ + ["minute", "2021-11-03T01:33"], + ["second", "2021-11-03T01:33:05"], + ["millisecond", "2021-11-03T01:33:05.100"], + ["microsecond", "2021-11-03T01:33:05.100200"], + ["nanosecond", "2021-11-03T01:33:05.100200300"], + ]; + + for (const [smallestUnit, expected] of values) { + const singularOptions = { smallestUnit }; + const pluralOptions = { smallestUnit: `${smallestUnit}s` }; + expect(plainDateTime.toString(singularOptions)).toBe(expected); + expect(plainDateTime.toString(pluralOptions)).toBe(expected); + } + }); + + test("calendarName option", () => { + const plainDateTime = new Temporal.PlainDateTime(2022, 11, 2, 19, 4, 35, 100, 200, 300); + const values = [ + ["auto", "2022-11-02T19:04:35.1002003"], + ["always", "2022-11-02T19:04:35.1002003[u-ca=iso8601]"], + ["never", "2022-11-02T19:04:35.1002003"], + ["critical", "2022-11-02T19:04:35.1002003[!u-ca=iso8601]"], + ]; + + for (const [calendarName, expected] of values) { + const options = { calendarName }; + expect(plainDateTime.toString(options)).toBe(expected); + } + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainDateTime object", () => { + expect(() => { + Temporal.PlainDateTime.prototype.toString.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainDateTime"); + }); + + test("calendarName option must be one of 'auto', 'always', 'never', 'critical'", () => { + const plainDateTime = new Temporal.PlainDateTime(2022, 11, 2, 19, 5, 40, 100, 200, 300); + expect(() => { + plainDateTime.toString({ calendarName: "foo" }); + }).toThrowWithMessage(RangeError, "foo is not a valid value for option calendarName"); + }); +});