LibJS: Implement stringification Temporal.ZonedDateTime prototypes

This commit is contained in:
Timothy Flynn 2024-11-25 10:17:28 -05:00 committed by Andreas Kling
parent c0150acc5e
commit 4ef21614e9
Notes: github-actions[bot] 2024-11-26 10:03:54 +00:00
9 changed files with 339 additions and 1 deletions

View file

@ -211,6 +211,40 @@ ThrowCompletionOr<ShowCalendar> 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<ShowTimeZoneName> 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<ShowOffset> 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<void> validate_temporal_rounding_increment(VM& vm, u64 increment, u64 dividend, bool inclusive)
{

View file

@ -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<Overflow> get_temporal_overflow_option(VM&, Object const& opti
ThrowCompletionOr<Disambiguation> get_temporal_disambiguation_option(VM&, Object const& options);
RoundingMode negate_rounding_mode(RoundingMode);
ThrowCompletionOr<OffsetOption> get_temporal_offset_option(VM&, Object const& options, OffsetOption fallback);
ThrowCompletionOr<ShowTimeZoneName> get_temporal_show_time_zone_name_option(VM&, Object const& options);
ThrowCompletionOr<ShowOffset> get_temporal_show_offset_option(VM&, Object const& options);
ThrowCompletionOr<ShowCalendar> get_temporal_show_calendar_name_option(VM&, Object const& options);
ThrowCompletionOr<void> validate_temporal_rounding_increment(VM&, u64 increment, u64 dividend, bool inclusive);
ThrowCompletionOr<Precision> 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<ParsedISODateTime> parse_iso_date_time(VM&, StringView iso_string, ReadonlySpan<Production> allowed_formats);
ThrowCompletionOr<String> parse_temporal_calendar_string(VM&, String const&);
ThrowCompletionOr<GC::Ref<Duration>> parse_temporal_duration_string(VM&, StringView iso_string);
ThrowCompletionOr<TimeZone> parse_temporal_time_zone_string(VM& vm, StringView time_zone_string);
ThrowCompletionOr<TimeZone> parse_temporal_time_zone_string(VM&, StringView time_zone_string);
ThrowCompletionOr<String> to_month_code(VM&, Value argument);
ThrowCompletionOr<String> to_offset_string(VM&, Value argument);
CalendarFields iso_date_to_fields(StringView calendar, ISODate const&, DateType);

View file

@ -319,6 +319,63 @@ ThrowCompletionOr<GC::Ref<ZonedDateTime>> 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<Crypto::SignedBigInteger> add_zoned_date_time(VM& vm, Crypto::SignedBigInteger const& epoch_nanoseconds, StringView time_zone, StringView calendar, InternalDuration const& duration, Overflow overflow)
{

View file

@ -51,6 +51,7 @@ enum class MatchBehavior {
ThrowCompletionOr<Crypto::SignedBigInteger> interpret_iso_date_time_offset(VM&, ISODate, Variant<ParsedISODateTime::StartOfDay, Time> const&, OffsetBehavior, double offset_nanoseconds, StringView time_zone, Disambiguation, OffsetOption, MatchBehavior);
ThrowCompletionOr<GC::Ref<ZonedDateTime>> to_temporal_zoned_date_time(VM&, Value item, Value options = js_undefined());
ThrowCompletionOr<GC::Ref<ZonedDateTime>> create_temporal_zoned_date_time(VM&, BigInt const& epoch_nanoseconds, String time_zone, String calendar, GC::Ptr<FunctionObject> 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<Crypto::SignedBigInteger> add_zoned_date_time(VM&, Crypto::SignedBigInteger const& epoch_nanoseconds, StringView time_zone, StringView calendar, InternalDuration const&, Overflow);
ThrowCompletionOr<InternalDuration> difference_zoned_date_time(VM&, Crypto::SignedBigInteger const& nanoseconds1, Crypto::SignedBigInteger const& nanoseconds2, StringView time_zone, StringView calendar, Unit largest_unit);
ThrowCompletionOr<InternalDuration> 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);

View file

@ -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 == Unit::Hour)
return vm.throw_completion<RangeError>(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)
{

View file

@ -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);
};

View file

@ -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");
});
});

View file

@ -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");
});
});

View file

@ -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");
});
});