From 4ef21614e9bcffd553c0060a0166a0219e00c5cb Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Mon, 25 Nov 2024 10:17:28 -0500 Subject: [PATCH] LibJS: Implement stringification Temporal.ZonedDateTime prototypes --- .../Runtime/Temporal/AbstractOperations.cpp | 34 +++++ .../Runtime/Temporal/AbstractOperations.h | 15 ++- .../LibJS/Runtime/Temporal/ZonedDateTime.cpp | 57 ++++++++ .../LibJS/Runtime/Temporal/ZonedDateTime.h | 1 + .../Temporal/ZonedDateTimePrototype.cpp | 69 ++++++++++ .../Runtime/Temporal/ZonedDateTimePrototype.h | 3 + .../ZonedDateTime.prototype.toJSON.js | 19 +++ .../ZonedDateTime.prototype.toLocaleString.js | 19 +++ .../ZonedDateTime.prototype.toString.js | 123 ++++++++++++++++++ 9 files changed, 339 insertions(+), 1 deletion(-) create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/ZonedDateTime/ZonedDateTime.prototype.toJSON.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/ZonedDateTime/ZonedDateTime.prototype.toLocaleString.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/ZonedDateTime/ZonedDateTime.prototype.toString.js diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp index fe7850367fb..799ddba7a8f 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp @@ -211,6 +211,40 @@ ThrowCompletionOr get_temporal_show_calendar_name_option(VM& vm, O return ShowCalendar::Auto; } +// 13.11 GetTemporalShowTimeZoneNameOption ( options ), https://tc39.es/proposal-temporal/#sec-temporal-gettemporalshowtimezonenameoption +ThrowCompletionOr get_temporal_show_time_zone_name_option(VM& vm, Object const& options) +{ + // 1. Let stringValue be ? GetOption(options, "timeZoneName", STRING, « "auto", "never", "critical" », "auto"). + auto string_value = TRY(get_option(vm, options, vm.names.timeZoneName, OptionType::String, { "auto"sv, "never"sv, "critical"sv }, "auto"sv)); + auto string_view = string_value.as_string().utf8_string_view(); + + // 2. If stringValue is "never", return NEVER. + if (string_view == "never"sv) + return ShowTimeZoneName::Never; + + // 3. If stringValue is "critical", return CRITICAL. + if (string_view == "critical"sv) + return ShowTimeZoneName::Critical; + + // 4. Return AUTO. + return ShowTimeZoneName::Auto; +} + +// 13.12 GetTemporalShowOffsetOption ( options ), https://tc39.es/proposal-temporal/#sec-temporal-gettemporalshowoffsetoption +ThrowCompletionOr get_temporal_show_offset_option(VM& vm, Object const& options) +{ + // 1. Let stringValue be ? GetOption(options, "offset", STRING, « "auto", "never" », "auto"). + auto string_value = TRY(get_option(vm, options, vm.names.offset, OptionType::String, { "auto"sv, "never"sv }, "auto"sv)); + auto string_view = string_value.as_string().utf8_string_view(); + + // 2. If stringValue is "never", return never. + if (string_view == "never"sv) + return ShowOffset::Never; + + // 3. Return auto. + return ShowOffset::Auto; +} + // 13.14 ValidateTemporalRoundingIncrement ( increment, dividend, inclusive ), https://tc39.es/proposal-temporal/#sec-validatetemporalroundingincrement ThrowCompletionOr validate_temporal_rounding_increment(VM& vm, u64 increment, u64 dividend, bool inclusive) { diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h index 5f9083f9656..988d8ed7012 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h @@ -64,6 +64,17 @@ enum class ShowCalendar { Critical, }; +enum class ShowOffset { + Auto, + Never, +}; + +enum class ShowTimeZoneName { + Auto, + Never, + Critical, +}; + enum class TimeStyle { Separated, Unseparated, @@ -161,6 +172,8 @@ ThrowCompletionOr get_temporal_overflow_option(VM&, Object const& opti ThrowCompletionOr get_temporal_disambiguation_option(VM&, Object const& options); RoundingMode negate_rounding_mode(RoundingMode); ThrowCompletionOr get_temporal_offset_option(VM&, Object const& options, OffsetOption fallback); +ThrowCompletionOr get_temporal_show_time_zone_name_option(VM&, Object const& options); +ThrowCompletionOr get_temporal_show_offset_option(VM&, Object const& options); ThrowCompletionOr get_temporal_show_calendar_name_option(VM&, Object const& options); ThrowCompletionOr validate_temporal_rounding_increment(VM&, u64 increment, u64 dividend, bool inclusive); ThrowCompletionOr get_temporal_fractional_second_digits_option(VM&, Object const& options); @@ -184,7 +197,7 @@ Crypto::SignedBigInteger round_number_to_increment_as_if_positive(Crypto::Signed ThrowCompletionOr parse_iso_date_time(VM&, StringView iso_string, ReadonlySpan allowed_formats); ThrowCompletionOr parse_temporal_calendar_string(VM&, String const&); ThrowCompletionOr> parse_temporal_duration_string(VM&, StringView iso_string); -ThrowCompletionOr parse_temporal_time_zone_string(VM& vm, StringView time_zone_string); +ThrowCompletionOr parse_temporal_time_zone_string(VM&, StringView time_zone_string); ThrowCompletionOr to_month_code(VM&, Value argument); ThrowCompletionOr to_offset_string(VM&, Value argument); CalendarFields iso_date_to_fields(StringView calendar, ISODate const&, DateType); diff --git a/Libraries/LibJS/Runtime/Temporal/ZonedDateTime.cpp b/Libraries/LibJS/Runtime/Temporal/ZonedDateTime.cpp index 5dd7bde51ca..c2394f406b5 100644 --- a/Libraries/LibJS/Runtime/Temporal/ZonedDateTime.cpp +++ b/Libraries/LibJS/Runtime/Temporal/ZonedDateTime.cpp @@ -319,6 +319,63 @@ ThrowCompletionOr> create_temporal_zoned_date_time(VM& vm return object; } +// 6.5.4 TemporalZonedDateTimeToString ( zonedDateTime, precision, showCalendar, showTimeZone, showOffset [ , increment [ , unit [ , roundingMode ] ] ] ), https://tc39.es/proposal-temporal/#sec-temporal-temporalzoneddatetimetostring +String temporal_zoned_date_time_to_string(ZonedDateTime const& zoned_date_time, SecondsStringPrecision::Precision precision, ShowCalendar show_calendar, ShowTimeZoneName show_time_zone, ShowOffset show_offset, u64 increment, Unit unit, RoundingMode rounding_mode) +{ + // 1. If increment is not present, set increment to 1. + // 2. If unit is not present, set unit to NANOSECOND. + // 3. If roundingMode is not present, set roundingMode to TRUNC. + + // 4. Let epochNs be zonedDateTime.[[EpochNanoseconds]]. + // 5. Set epochNs to RoundTemporalInstant(epochNs, increment, unit, roundingMode). + auto epoch_nanoseconds = round_temporal_instant(zoned_date_time.epoch_nanoseconds()->big_integer(), increment, unit, rounding_mode); + + // 6. Let timeZone be zonedDateTime.[[TimeZone]]. + auto const& time_zone = zoned_date_time.time_zone(); + + // 7. Let offsetNanoseconds be GetOffsetNanosecondsFor(timeZone, epochNs). + auto offset_nanoseconds = get_offset_nanoseconds_for(time_zone, epoch_nanoseconds); + + // 8. Let isoDateTime be GetISODateTimeFor(timeZone, epochNs). + auto iso_date_time = get_iso_date_time_for(time_zone, epoch_nanoseconds); + + // 9. Let dateTimeString be ISODateTimeToString(isoDateTime, "iso8601", precision, NEVER). + auto date_time_string = iso_date_time_to_string(iso_date_time, "iso8601"sv, precision, ShowCalendar::Never); + + String offset_string; + String time_zone_string; + + // 10. If showOffset is NEVER, then + if (show_offset == ShowOffset::Never) { + // a. Let offsetString be the empty String. + } + // 11. Else, + else { + // a. Let offsetString be FormatDateTimeUTCOffsetRounded(offsetNanoseconds). + offset_string = format_date_time_utc_offset_rounded(offset_nanoseconds); + } + + // 12. If showTimeZone is NEVER, then + if (show_time_zone == ShowTimeZoneName::Never) { + // a. Let timeZoneString be the empty String. + } + // 13. Else, + else { + // a. If showTimeZone is critical, let flag be "!"; else let flag be the empty String. + auto flag = show_time_zone == ShowTimeZoneName::Critical ? "!"sv : ""sv; + + // b. Let timeZoneString be the string-concatenation of the code unit 0x005B (LEFT SQUARE BRACKET), flag, + // timeZone, and the code unit 0x005D (RIGHT SQUARE BRACKET). + time_zone_string = MUST(String::formatted("[{}{}]", flag, time_zone)); + } + + // 14. Let calendarString be FormatCalendarAnnotation(zonedDateTime.[[Calendar]], showCalendar). + auto calendar_string = format_calendar_annotation(zoned_date_time.calendar(), show_calendar); + + // 15. Return the string-concatenation of dateTimeString, offsetString, timeZoneString, and calendarString. + return MUST(String::formatted("{}{}{}{}", date_time_string, offset_string, time_zone_string, calendar_string)); +} + // 6.5.5 AddZonedDateTime ( epochNanoseconds, timeZone, calendar, duration, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-addzoneddatetime ThrowCompletionOr add_zoned_date_time(VM& vm, Crypto::SignedBigInteger const& epoch_nanoseconds, StringView time_zone, StringView calendar, InternalDuration const& duration, Overflow overflow) { diff --git a/Libraries/LibJS/Runtime/Temporal/ZonedDateTime.h b/Libraries/LibJS/Runtime/Temporal/ZonedDateTime.h index bd50ea1234f..27370714e09 100644 --- a/Libraries/LibJS/Runtime/Temporal/ZonedDateTime.h +++ b/Libraries/LibJS/Runtime/Temporal/ZonedDateTime.h @@ -51,6 +51,7 @@ enum class MatchBehavior { ThrowCompletionOr interpret_iso_date_time_offset(VM&, ISODate, Variant const&, OffsetBehavior, double offset_nanoseconds, StringView time_zone, Disambiguation, OffsetOption, MatchBehavior); ThrowCompletionOr> to_temporal_zoned_date_time(VM&, Value item, Value options = js_undefined()); ThrowCompletionOr> create_temporal_zoned_date_time(VM&, BigInt const& epoch_nanoseconds, String time_zone, String calendar, GC::Ptr new_target = {}); +String temporal_zoned_date_time_to_string(ZonedDateTime const&, SecondsStringPrecision::Precision, ShowCalendar, ShowTimeZoneName, ShowOffset, u64 increment = 1, Unit = Unit::Nanosecond, RoundingMode = RoundingMode::Trunc); ThrowCompletionOr add_zoned_date_time(VM&, Crypto::SignedBigInteger const& epoch_nanoseconds, StringView time_zone, StringView calendar, InternalDuration const&, Overflow); ThrowCompletionOr difference_zoned_date_time(VM&, Crypto::SignedBigInteger const& nanoseconds1, Crypto::SignedBigInteger const& nanoseconds2, StringView time_zone, StringView calendar, Unit largest_unit); ThrowCompletionOr difference_zoned_date_time_with_rounding(VM&, Crypto::SignedBigInteger const& nanoseconds1, Crypto::SignedBigInteger const& nanoseconds2, StringView time_zone, StringView calendar, Unit largest_unit, u64 rounding_increment, Unit smallest_unit, RoundingMode); diff --git a/Libraries/LibJS/Runtime/Temporal/ZonedDateTimePrototype.cpp b/Libraries/LibJS/Runtime/Temporal/ZonedDateTimePrototype.cpp index 75088f4f5f0..c06b9f10b7f 100644 --- a/Libraries/LibJS/Runtime/Temporal/ZonedDateTimePrototype.cpp +++ b/Libraries/LibJS/Runtime/Temporal/ZonedDateTimePrototype.cpp @@ -62,6 +62,9 @@ void ZonedDateTimePrototype::initialize(Realm& realm) define_native_accessor(realm, vm.names.offset, offset_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); define_native_function(realm, vm.names.valueOf, value_of, 0, attr); } @@ -339,6 +342,72 @@ JS_DEFINE_NATIVE_FUNCTION(ZonedDateTimePrototype::offset_getter) return PrimitiveString::create(vm, format_utc_offset_nanoseconds(offset_nanoseconds)); } +// 6.3.41 Temporal.ZonedDateTime.prototype.toString ( [ options ] ), https://tc39.es/proposal-temporal/#sec-temporal.zoneddatetime.prototype.tostring +JS_DEFINE_NATIVE_FUNCTION(ZonedDateTimePrototype::to_string) +{ + // 1. Let zonedDateTime be the this value. + // 2. Perform ? RequireInternalSlot(zonedDateTime, [[InitializedTemporalZonedDateTime]]). + auto zoned_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", GetTemporalShowOffsetOption reads "offset", 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 showOffset be ? GetTemporalShowOffsetOption(resolvedOptions). + auto show_offset = TRY(get_temporal_show_offset_option(vm, resolved_options)); + + // 8. Let roundingMode be ? GetRoundingModeOption(resolvedOptions, TRUNC). + auto rounding_mode = TRY(get_rounding_mode_option(vm, resolved_options, RoundingMode::Trunc)); + + // 9. 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 {})); + + // 10. 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); + + // 11. Let showTimeZone be ? GetTemporalShowTimeZoneNameOption(resolvedOptions). + auto show_time_zone = TRY(get_temporal_show_time_zone_name_option(vm, resolved_options)); + + // 12. Let precision be ToSecondsStringPrecisionRecord(smallestUnit, digits). + auto precision = to_seconds_string_precision_record(smallest_unit, digits); + + // 13. Return TemporalZonedDateTimeToString(zonedDateTime, precision.[[Precision]], showCalendar, showTimeZone, showOffset, precision.[[Increment]], precision.[[Unit]], roundingMode). + return PrimitiveString::create(vm, temporal_zoned_date_time_to_string(zoned_date_time, precision.precision, show_calendar, show_time_zone, show_offset, precision.increment, precision.unit, rounding_mode)); +} + +// 6.3.42 Temporal.ZonedDateTime.prototype.toLocaleString ( [ locales [ , options ] ] ), https://tc39.es/proposal-temporal/#sec-temporal.zoneddatetime.prototype.tolocalestring +// NOTE: This is the minimum toLocaleString implementation for engines without ECMA-402. +JS_DEFINE_NATIVE_FUNCTION(ZonedDateTimePrototype::to_locale_string) +{ + // 1. Let zonedDateTime be the this value. + // 2. Perform ? RequireInternalSlot(zonedDateTime, [[InitializedTemporalZonedDateTime]]). + auto zoned_date_time = TRY(typed_this_object(vm)); + + // 3. Return TemporalZonedDateTimeToString(zonedDateTime, AUTO, AUTO, AUTO, AUTO). + return PrimitiveString::create(vm, temporal_zoned_date_time_to_string(zoned_date_time, Auto {}, ShowCalendar::Auto, ShowTimeZoneName::Auto, ShowOffset::Auto)); +} + +// 6.3.43 Temporal.ZonedDateTime.prototype.toJSON ( ), https://tc39.es/proposal-temporal/#sec-temporal.zoneddatetime.prototype.tojson +JS_DEFINE_NATIVE_FUNCTION(ZonedDateTimePrototype::to_json) +{ + // 1. Let zonedDateTime be the this value. + // 2. Perform ? RequireInternalSlot(zonedDateTime, [[InitializedTemporalZonedDateTime]]). + auto zoned_date_time = TRY(typed_this_object(vm)); + + // 3. Return TemporalZonedDateTimeToString(zonedDateTime, AUTO, AUTO, AUTO, AUTO). + return PrimitiveString::create(vm, temporal_zoned_date_time_to_string(zoned_date_time, Auto {}, ShowCalendar::Auto, ShowTimeZoneName::Auto, ShowOffset::Auto)); +} + // 6.3.44 Temporal.ZonedDateTime.prototype.valueOf ( ), https://tc39.es/proposal-temporal/#sec-temporal.zoneddatetime.prototype.valueof JS_DEFINE_NATIVE_FUNCTION(ZonedDateTimePrototype::value_of) { diff --git a/Libraries/LibJS/Runtime/Temporal/ZonedDateTimePrototype.h b/Libraries/LibJS/Runtime/Temporal/ZonedDateTimePrototype.h index e7ea723336e..a47c32c28c8 100644 --- a/Libraries/LibJS/Runtime/Temporal/ZonedDateTimePrototype.h +++ b/Libraries/LibJS/Runtime/Temporal/ZonedDateTimePrototype.h @@ -51,6 +51,9 @@ private: JS_DECLARE_NATIVE_FUNCTION(in_leap_year_getter); JS_DECLARE_NATIVE_FUNCTION(offset_nanoseconds_getter); JS_DECLARE_NATIVE_FUNCTION(offset_getter); + JS_DECLARE_NATIVE_FUNCTION(to_string); + JS_DECLARE_NATIVE_FUNCTION(to_locale_string); + JS_DECLARE_NATIVE_FUNCTION(to_json); JS_DECLARE_NATIVE_FUNCTION(value_of); }; diff --git a/Libraries/LibJS/Tests/builtins/Temporal/ZonedDateTime/ZonedDateTime.prototype.toJSON.js b/Libraries/LibJS/Tests/builtins/Temporal/ZonedDateTime/ZonedDateTime.prototype.toJSON.js new file mode 100644 index 00000000000..e30a328f0f5 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/ZonedDateTime/ZonedDateTime.prototype.toJSON.js @@ -0,0 +1,19 @@ +describe("correct behavior", () => { + test("length is 0", () => { + expect(Temporal.ZonedDateTime.prototype.toJSON).toHaveLength(0); + }); + + test("basic functionality", () => { + const plainDateTime = new Temporal.PlainDateTime(2021, 11, 3, 1, 33, 5, 100, 200, 300); + const zonedDateTime = plainDateTime.toZonedDateTime("UTC"); + expect(zonedDateTime.toJSON()).toBe("2021-11-03T01:33:05.1002003+00:00[UTC]"); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.ZonedDateTime object", () => { + expect(() => { + Temporal.ZonedDateTime.prototype.toJSON.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.ZonedDateTime"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/ZonedDateTime/ZonedDateTime.prototype.toLocaleString.js b/Libraries/LibJS/Tests/builtins/Temporal/ZonedDateTime/ZonedDateTime.prototype.toLocaleString.js new file mode 100644 index 00000000000..cd0d8d1829a --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/ZonedDateTime/ZonedDateTime.prototype.toLocaleString.js @@ -0,0 +1,19 @@ +describe("correct behavior", () => { + test("length is 0", () => { + expect(Temporal.ZonedDateTime.prototype.toLocaleString).toHaveLength(0); + }); + + test("basic functionality", () => { + const plainDateTime = new Temporal.PlainDateTime(2021, 11, 3, 1, 33, 5, 100, 200, 300); + const zonedDateTime = plainDateTime.toZonedDateTime("UTC"); + expect(zonedDateTime.toLocaleString()).toBe("2021-11-03T01:33:05.1002003+00:00[UTC]"); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.ZonedDateTime object", () => { + expect(() => { + Temporal.ZonedDateTime.prototype.toLocaleString.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.ZonedDateTime"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/ZonedDateTime/ZonedDateTime.prototype.toString.js b/Libraries/LibJS/Tests/builtins/Temporal/ZonedDateTime/ZonedDateTime.prototype.toString.js new file mode 100644 index 00000000000..2160e5cad60 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/ZonedDateTime/ZonedDateTime.prototype.toString.js @@ -0,0 +1,123 @@ +describe("correct behavior", () => { + test("length is 0", () => { + expect(Temporal.ZonedDateTime.prototype.toString).toHaveLength(0); + }); + + test("basic functionality", () => { + const plainDateTime = new Temporal.PlainDateTime(2021, 11, 3, 1, 33, 5, 100, 200, 300); + const zonedDateTime = plainDateTime.toZonedDateTime("UTC"); + expect(zonedDateTime.toString()).toBe("2021-11-03T01:33:05.1002003+00:00[UTC]"); + }); + + test("negative epoch nanoseconds", () => { + const zonedDateTime = new Temporal.ZonedDateTime(-999_999_999n, "UTC"); + expect(zonedDateTime.toString()).toBe("1969-12-31T23:59:59.000000001+00:00[UTC]"); + }); + + test("fractionalSecondDigits option", () => { + const plainDateTime = new Temporal.PlainDateTime(2021, 11, 3, 1, 33, 5, 100, 200, 300); + const zonedDateTime = plainDateTime.toZonedDateTime("UTC"); + const values = [ + ["auto", "2021-11-03T01:33:05.1002003+00:00[UTC]"], + [0, "2021-11-03T01:33:05+00:00[UTC]"], + [1, "2021-11-03T01:33:05.1+00:00[UTC]"], + [2, "2021-11-03T01:33:05.10+00:00[UTC]"], + [3, "2021-11-03T01:33:05.100+00:00[UTC]"], + [4, "2021-11-03T01:33:05.1002+00:00[UTC]"], + [5, "2021-11-03T01:33:05.10020+00:00[UTC]"], + [6, "2021-11-03T01:33:05.100200+00:00[UTC]"], + [7, "2021-11-03T01:33:05.1002003+00:00[UTC]"], + [8, "2021-11-03T01:33:05.10020030+00:00[UTC]"], + [9, "2021-11-03T01:33:05.100200300+00:00[UTC]"], + ]; + + for (const [fractionalSecondDigits, expected] of values) { + const options = { fractionalSecondDigits }; + expect(zonedDateTime.toString(options)).toBe(expected); + } + + // Ignored when smallestUnit is given + expect(zonedDateTime.toString({ smallestUnit: "minute", fractionalSecondDigits: 9 })).toBe( + "2021-11-03T01:33+00:00[UTC]" + ); + }); + + test("smallestUnit option", () => { + const plainDateTime = new Temporal.PlainDateTime(2021, 11, 3, 1, 33, 5, 100, 200, 300); + const zonedDateTime = plainDateTime.toZonedDateTime("UTC"); + const values = [ + ["minute", "2021-11-03T01:33+00:00[UTC]"], + ["second", "2021-11-03T01:33:05+00:00[UTC]"], + ["millisecond", "2021-11-03T01:33:05.100+00:00[UTC]"], + ["microsecond", "2021-11-03T01:33:05.100200+00:00[UTC]"], + ["nanosecond", "2021-11-03T01:33:05.100200300+00:00[UTC]"], + ]; + + for (const [smallestUnit, expected] of values) { + const singularOptions = { smallestUnit }; + const pluralOptions = { smallestUnit: `${smallestUnit}s` }; + expect(zonedDateTime.toString(singularOptions)).toBe(expected); + expect(zonedDateTime.toString(pluralOptions)).toBe(expected); + } + }); + + test("timeZoneName option", () => { + const plainDateTime = new Temporal.PlainDateTime(2021, 11, 3, 1, 33, 5, 100, 200, 300); + const zonedDateTime = plainDateTime.toZonedDateTime("UTC"); + const values = [ + ["auto", "2021-11-03T01:33:05.1002003+00:00[UTC]"], + ["never", "2021-11-03T01:33:05.1002003+00:00"], + ["critical", "2021-11-03T01:33:05.1002003+00:00[!UTC]"], + ]; + + for (const [timeZoneName, expected] of values) { + const options = { timeZoneName }; + expect(zonedDateTime.toString(options)).toBe(expected); + } + }); + + test("offset option", () => { + const plainDateTime = new Temporal.PlainDateTime(2021, 11, 3, 1, 33, 5, 100, 200, 300); + const zonedDateTime = plainDateTime.toZonedDateTime("UTC"); + const values = [ + ["auto", "2021-11-03T01:33:05.1002003+00:00[UTC]"], + ["never", "2021-11-03T01:33:05.1002003[UTC]"], + ]; + + for (const [offset, expected] of values) { + const options = { offset }; + expect(zonedDateTime.toString(options)).toBe(expected); + } + }); + + test("calendarName option", () => { + const plainDateTime = new Temporal.PlainDateTime(2022, 11, 2, 19, 4, 35, 100, 200, 300); + const zonedDateTime = plainDateTime.toZonedDateTime("UTC"); + const values = [ + ["auto", "2022-11-02T19:04:35.1002003+00:00[UTC]"], + ["always", "2022-11-02T19:04:35.1002003+00:00[UTC][u-ca=iso8601]"], + ["never", "2022-11-02T19:04:35.1002003+00:00[UTC]"], + ["critical", "2022-11-02T19:04:35.1002003+00:00[UTC][!u-ca=iso8601]"], + ]; + + for (const [calendarName, expected] of values) { + const options = { calendarName }; + expect(zonedDateTime.toString(options)).toBe(expected); + } + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.ZonedDateTime object", () => { + expect(() => { + Temporal.ZonedDateTime.prototype.toString.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.ZonedDateTime"); + }); + + test("calendarName option must be one of 'auto', 'always', 'never', 'critical'", () => { + const zonedDateTime = new Temporal.ZonedDateTime(0n, "UTC"); + expect(() => { + zonedDateTime.toString({ calendarName: "foo" }); + }).toThrowWithMessage(RangeError, "foo is not a valid value for option calendarName"); + }); +});