From 91e25ca5888004fa5a45acd60d643b6ee44ad489 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Wed, 20 Nov 2024 20:06:42 -0500 Subject: [PATCH] LibJS: Use Temporal's ISO8601 parser to parse UTC offset strings We now have the Temporal facilities to implement the Date AOs which parse UTC offset strings using the ISO8601 parser. This patch updates those AOs and their callers in accordance with the Temporal spec. --- Libraries/LibJS/Runtime/Date.cpp | 276 +++++------------- Libraries/LibJS/Runtime/Date.h | 17 +- Libraries/LibJS/Runtime/DatePrototype.cpp | 71 ++--- .../Intl/DateTimeFormatConstructor.cpp | 25 +- .../Runtime/Temporal/AbstractOperations.cpp | 5 +- Libraries/LibJS/Runtime/Temporal/TimeZone.cpp | 3 +- 6 files changed, 113 insertions(+), 284 deletions(-) diff --git a/Libraries/LibJS/Runtime/Date.cpp b/Libraries/LibJS/Runtime/Date.cpp index 8a2a45d359b..48e571a5c60 100644 --- a/Libraries/LibJS/Runtime/Date.cpp +++ b/Libraries/LibJS/Runtime/Date.cpp @@ -1,20 +1,19 @@ /* * Copyright (c) 2020-2023, Linus Groh - * Copyright (c) 2022-2024, Tim Flynn + * Copyright (c) 2022-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ -#include -#include #include -#include #include #include #include #include #include #include +#include +#include #include namespace JS { @@ -457,7 +456,7 @@ String system_time_zone_identifier() // time zone identifier or an offset time zone identifier. auto system_time_zone_string = Unicode::current_time_zone(); - if (!is_time_zone_offset_string(system_time_zone_string)) { + if (!is_offset_time_zone_identifier(system_time_zone_string)) { auto time_zone_identifier = Intl::get_available_named_time_zone_identifier(system_time_zone_string); if (!time_zone_identifier.has_value()) return "UTC"_string; @@ -476,46 +475,55 @@ void clear_system_time_zone_cache() } // 21.4.1.25 LocalTime ( t ), https://tc39.es/ecma262/#sec-localtime +// 14.5.6 LocalTime ( t ), https://tc39.es/proposal-temporal/#sec-localtime double local_time(double time) { // 1. Let systemTimeZoneIdentifier be SystemTimeZoneIdentifier(). auto system_time_zone_identifier = JS::system_time_zone_identifier(); + // 2. Let parseResult be ! ParseTimeZoneIdentifier(systemTimeZoneIdentifier). + auto parse_result = Temporal::parse_time_zone_identifier(system_time_zone_identifier); + double offset_nanoseconds { 0 }; - // 2. If IsTimeZoneOffsetString(systemTimeZoneIdentifier) is true, then - if (is_time_zone_offset_string(system_time_zone_identifier)) { - // a. Let offsetNs be ParseTimeZoneOffsetString(systemTimeZoneIdentifier). - offset_nanoseconds = parse_time_zone_offset_string(system_time_zone_identifier); + // 3. If parseResult.[[OffsetMinutes]] is not EMPTY, then + if (parse_result.offset_minutes.has_value()) { + // a. Let offsetNs be parseResult.[[OffsetMinutes]] × (60 × 10**9). + offset_nanoseconds = static_cast(*parse_result.offset_minutes) * 60'000'000'000; } - // 3. Else, + // 4. Else, else { // a. Let offsetNs be GetNamedTimeZoneOffsetNanoseconds(systemTimeZoneIdentifier, ℤ(ℝ(t) × 10^6)). auto offset = get_named_time_zone_offset_milliseconds(system_time_zone_identifier, time); offset_nanoseconds = static_cast(offset.offset.to_nanoseconds()); } - // 4. Let offsetMs be truncate(offsetNs / 10^6). + // 5. Let offsetMs be truncate(offsetNs / 10^6). auto offset_milliseconds = trunc(offset_nanoseconds / 1e6); - // 5. Return t + 𝔽(offsetMs). + // 6. Return t + 𝔽(offsetMs). return time + offset_milliseconds; } // 21.4.1.26 UTC ( t ), https://tc39.es/ecma262/#sec-utc-t +// 14.5.7 UTC ( t ), https://tc39.es/proposal-temporal/#sec-localtime +// FIXME: Update the rest of this AO for Temporal once we have the required Temporal objects. double utc_time(double time) { // 1. Let systemTimeZoneIdentifier be SystemTimeZoneIdentifier(). auto system_time_zone_identifier = JS::system_time_zone_identifier(); + // 2. Let parseResult be ! ParseTimeZoneIdentifier(systemTimeZoneIdentifier). + auto parse_result = Temporal::parse_time_zone_identifier(system_time_zone_identifier); + double offset_nanoseconds { 0 }; - // 2. If IsTimeZoneOffsetString(systemTimeZoneIdentifier) is true, then - if (is_time_zone_offset_string(system_time_zone_identifier)) { - // a. Let offsetNs be ParseTimeZoneOffsetString(systemTimeZoneIdentifier). - offset_nanoseconds = parse_time_zone_offset_string(system_time_zone_identifier); + // 3. If parseResult.[[OffsetMinutes]] is not EMPTY, then + if (parse_result.offset_minutes.has_value()) { + // a. Let offsetNs be parseResult.[[OffsetMinutes]] × (60 × 10**9). + offset_nanoseconds = static_cast(*parse_result.offset_minutes) * 60'000'000'000; } - // 3. Else, + // 4. Else, else { // a. Let possibleInstants be GetNamedTimeZoneEpochNanoseconds(systemTimeZoneIdentifier, ℝ(YearFromTime(t)), ℝ(MonthFromTime(t)) + 1, ℝ(DateFromTime(t)), ℝ(HourFromTime(t)), ℝ(MinFromTime(t)), ℝ(SecFromTime(t)), ℝ(msFromTime(t)), 0, 0). auto possible_instants = get_named_time_zone_epoch_nanoseconds(system_time_zone_identifier, year_from_time(time), month_from_time(time) + 1, date_from_time(time), hour_from_time(time), min_from_time(time), sec_from_time(time), ms_from_time(time), 0, 0); @@ -544,10 +552,10 @@ double utc_time(double time) offset_nanoseconds = static_cast(offset.offset.to_nanoseconds()); } - // 4. Let offsetMs be truncate(offsetNs / 10^6). + // 5. Let offsetMs be truncate(offsetNs / 10^6). auto offset_milliseconds = trunc(offset_nanoseconds / 1e6); - // 5. Return t - 𝔽(offsetMs). + // 6. Return t - 𝔽(offsetMs). return time - offset_milliseconds; } @@ -636,171 +644,12 @@ double time_clip(double time) return to_integer_or_infinity(time); } -// 21.4.1.33 Time Zone Offset String Format, https://tc39.es/ecma262/#sec-time-zone-offset-strings -Optional parse_utc_offset(StringView offset_string) -{ - GenericLexer lexer { offset_string }; - UTCOffset parse_result; - - // https://tc39.es/ecma262/#prod-ASCIISign - auto parse_ascii_sign = [&]() { - // ASCIISign ::: one of - // + - - if (lexer.next_is(is_any_of("+-"sv))) { - parse_result.sign = lexer.consume(); - return true; - } - - return false; - }; - - auto parse_two_digits = [&](size_t max_value) -> Optional { - if (auto digits = lexer.peek_string(2); digits.has_value()) { - auto number = digits->to_number(TrimWhitespace::No); - - if (number.has_value() && *number <= max_value) { - lexer.ignore(2); - return *number; - } - } - - return {}; - }; - - // https://tc39.es/ecma262/#prod-Hour - auto parse_hour = [&]() { - // Hour ::: - // 0 DecimalDigit - // 1 DecimalDigit - // 20 - // 21 - // 22 - // 23 - parse_result.hour = parse_two_digits(23); - return parse_result.hour.has_value(); - }; - - // https://tc39.es/ecma262/#prod-TimeSeparator - auto parse_time_separator = [&](auto extended) { - // TimeSeparator[Extended] ::: - // [+Extended] : - // [~Extended] [empty] - if (extended) - return lexer.consume_specific(':'); - return true; - }; - - // https://tc39.es/ecma262/#prod-MinuteSecond - auto parse_minute_second = [&](auto& result) { - // MinuteSecond ::: - // 0 DecimalDigit - // 1 DecimalDigit - // 2 DecimalDigit - // 3 DecimalDigit - // 4 DecimalDigit - // 5 DecimalDigit - result = parse_two_digits(59); - return result.has_value(); - }; - - // https://tc39.es/ecma262/#prod-TemporalDecimalSeparator - auto parse_temporal_decimal_separator = [&]() { - // TemporalDecimalSeparator ::: one of - // . , - return lexer.consume_specific('.') || lexer.consume_specific(','); - }; - - // https://tc39.es/ecma262/#prod-TemporalDecimalFraction - auto parse_temporal_decimal_fraction = [&]() { - // TemporalDecimalFraction ::: - // TemporalDecimalSeparator DecimalDigit - // TemporalDecimalSeparator DecimalDigit DecimalDigit - // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit - // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit - // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit - // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit - // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit - // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit - // TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit - auto position = lexer.tell(); - - if (!parse_temporal_decimal_separator()) - return false; - - for (size_t i = 0; i < 9; ++i) { - if (!lexer.next_is(is_ascii_digit)) - break; - lexer.ignore(); - } - - if (auto fraction = lexer.input().substring_view(position, lexer.tell() - position); fraction.length() > 1) { - parse_result.fraction = fraction; - return true; - } - - return false; - }; - - // https://tc39.es/ecma262/#prod-HourSubcomponents - auto parse_hour_subcomponents = [&](auto extended) { - // HourSubcomponents[Extended] ::: - // TimeSeparator[?Extended] MinuteSecond - // TimeSeparator[?Extended] MinuteSecond TimeSeparator[?Extended] MinuteSecond TemporalDecimalFraction[opt] - ArmedScopeGuard guard { [&, position = lexer.tell()]() { lexer.retreat(lexer.tell() - position); } }; - - if (!parse_time_separator(extended)) - return false; - if (!parse_minute_second(parse_result.minute)) - return false; - - if (lexer.is_eof()) { - guard.disarm(); - return true; - } - - if (!parse_time_separator(extended)) - return false; - if (!parse_minute_second(parse_result.second)) - return false; - - if (lexer.is_eof()) { - guard.disarm(); - return true; - } - - if (!parse_temporal_decimal_fraction()) - return false; - - guard.disarm(); - return true; - }; - - // https://tc39.es/ecma262/#prod-UTCOffset - // UTCOffset ::: - // ASCIISign Hour - // ASCIISign Hour HourSubcomponents[+Extended] - // ASCIISign Hour HourSubcomponents[~Extended] - if (!parse_ascii_sign()) - return {}; - if (!parse_hour()) - return {}; - - if (lexer.is_eof()) - return parse_result; - - if (!parse_hour_subcomponents(true) && !parse_hour_subcomponents(false)) - return {}; - if (lexer.is_eof()) - return parse_result; - - return {}; -} - // 21.4.1.33.1 IsTimeZoneOffsetString ( offsetString ), https://tc39.es/ecma262/#sec-istimezoneoffsetstring -bool is_time_zone_offset_string(StringView offset_string) +// 14.5.10 IsOffsetTimeZoneIdentifier ( offsetString ), https://tc39.es/proposal-temporal/#sec-isoffsettimezoneidentifier +bool is_offset_time_zone_identifier(StringView offset_string) { - // 1. Let parseResult be ParseText(StringToCodePoints(offsetString), UTCOffset). - auto parse_result = parse_utc_offset(offset_string); + // 1. Let parseResult be ParseText(StringToCodePoints(offsetString), UTCOffset[~SubMinutePrecision]). + auto parse_result = Temporal::parse_utc_offset(offset_string, Temporal::SubMinutePrecision::No); // 2. If parseResult is a List of errors, return false. // 3. Return true. @@ -808,66 +657,83 @@ bool is_time_zone_offset_string(StringView offset_string) } // 21.4.1.33.2 ParseTimeZoneOffsetString ( offsetString ), https://tc39.es/ecma262/#sec-parsetimezoneoffsetstring -double parse_time_zone_offset_string(StringView offset_string) +// 14.5.11 ParseDateTimeUTCOffset ( offsetString ), https://tc39.es/proposal-temporal/#sec-parsedatetimeutcoffset +ThrowCompletionOr parse_date_time_utc_offset(VM& vm, StringView offset_string) { - // 1. Let parseResult be ParseText(offsetString, UTCOffset). - auto parse_result = parse_utc_offset(offset_string); + // 1. Let parseResult be ParseText(offsetString, UTCOffset[+SubMinutePrecision]). + auto parse_result = Temporal::parse_utc_offset(offset_string, Temporal::SubMinutePrecision::Yes); - // 2. Assert: parseResult is not a List of errors. + // 2. If parseResult is a List of errors, throw a RangeError exception. + if (!parse_result.has_value()) + return vm.throw_completion(ErrorType::TemporalInvalidTimeZoneString, offset_string); + + return parse_date_time_utc_offset(*parse_result); +} + +// 21.4.1.33.2 ParseTimeZoneOffsetString ( offsetString ), https://tc39.es/ecma262/#sec-parsetimezoneoffsetstring +// 14.5.11 ParseDateTimeUTCOffset ( offsetString ), https://tc39.es/proposal-temporal/#sec-parsedatetimeutcoffset +double parse_date_time_utc_offset(StringView offset_string) +{ + // OPTIMIZATION: Some callers can assume that parsing will succeed. + + // 1. Let parseResult be ParseText(offsetString, UTCOffset[+SubMinutePrecision]). + auto parse_result = Temporal::parse_utc_offset(offset_string, Temporal::SubMinutePrecision::Yes); VERIFY(parse_result.has_value()); + return parse_date_time_utc_offset(*parse_result); +} + +// 21.4.1.33.2 ParseTimeZoneOffsetString ( offsetString ), https://tc39.es/ecma262/#sec-parsetimezoneoffsetstring +// 14.5.11 ParseDateTimeUTCOffset ( offsetString ), https://tc39.es/proposal-temporal/#sec-parsedatetimeutcoffset +double parse_date_time_utc_offset(Temporal::TimeZoneOffset const& parse_result) +{ + // OPTIMIZATION: Some callers will have already parsed and validated the time zone identifier. + // 3. Assert: parseResult contains a ASCIISign Parse Node. - VERIFY(parse_result->sign.has_value()); + VERIFY(parse_result.sign.has_value()); // 4. Let parsedSign be the source text matched by the ASCIISign Parse Node contained within parseResult. - auto parsed_sign = *parse_result->sign; - i8 sign { 0 }; - // 5. If parsedSign is the single code point U+002D (HYPHEN-MINUS), then - if (parsed_sign == '-') { - // a. Let sign be -1. - sign = -1; - } + // a. Let sign be -1. // 6. Else, - else { - // a. Let sign be 1. - sign = 1; - } + // a. Let sign be 1. + auto sign = parse_result.sign == '-' ? -1 : 1; - // 7. NOTE: Applications of StringToNumber below do not lose precision, since each of the parsed values is guaranteed to be a sufficiently short string of decimal digits. + // 7. NOTE: Applications of StringToNumber below do not lose precision, since each of the parsed values is guaranteed + // to be a sufficiently short string of decimal digits. // 8. Assert: parseResult contains an Hour Parse Node. - VERIFY(parse_result->hour.has_value()); + VERIFY(parse_result.hours.has_value()); // 9. Let parsedHours be the source text matched by the Hour Parse Node contained within parseResult. // 10. Let hours be ℝ(StringToNumber(CodePointsToString(parsedHours))). - auto hours = *parse_result->hour; + auto hours = parse_result.hours->to_number().value(); // 11. If parseResult does not contain a MinuteSecond Parse Node, then // a. Let minutes be 0. // 12. Else, // a. Let parsedMinutes be the source text matched by the first MinuteSecond Parse Node contained within parseResult. // b. Let minutes be ℝ(StringToNumber(CodePointsToString(parsedMinutes))). - double minutes = parse_result->minute.value_or(0); + double minutes = parse_result.minutes.has_value() ? parse_result.minutes->to_number().value() : 0; // 13. If parseResult does not contain two MinuteSecond Parse Nodes, then // a. Let seconds be 0. // 14. Else, // a. Let parsedSeconds be the source text matched by the second secondSecond Parse Node contained within parseResult. // b. Let seconds be ℝ(StringToNumber(CodePointsToString(parsedSeconds))). - double seconds = parse_result->second.value_or(0); + double seconds = parse_result.seconds.has_value() ? parse_result.seconds->to_number().value() : 0; double nanoseconds = 0; // 15. If parseResult does not contain a TemporalDecimalFraction Parse Node, then - if (!parse_result->fraction.has_value()) { + if (!parse_result.fraction.has_value()) { // a. Let nanoseconds be 0. nanoseconds = 0; } // 16. Else, else { // a. Let parsedFraction be the source text matched by the TemporalDecimalFraction Parse Node contained within parseResult. - auto parsed_fraction = *parse_result->fraction; + auto parsed_fraction = *parse_result.fraction; // b. Let fraction be the string-concatenation of CodePointsToString(parsedFraction) and "000000000". auto fraction = ByteString::formatted("{}000000000", parsed_fraction); diff --git a/Libraries/LibJS/Runtime/Date.h b/Libraries/LibJS/Runtime/Date.h index 91036948e83..dd522773e3c 100644 --- a/Libraries/LibJS/Runtime/Date.h +++ b/Libraries/LibJS/Runtime/Date.h @@ -1,6 +1,6 @@ /* * Copyright (c) 2020-2022, Linus Groh - * Copyright (c) 2022-2024, Tim Flynn + * Copyright (c) 2022-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -58,14 +58,6 @@ constexpr inline double ms_per_day = 86'400'000; constexpr inline double ns_per_day = 86'400'000'000'000; extern Crypto::SignedBigInteger const ns_per_day_bigint; -struct UTCOffset { - Optional sign; - Optional hour; - Optional minute; - Optional second; - Optional fraction; -}; - double day(double); double time_within_day(double); u16 days_in_year(i32); @@ -93,8 +85,9 @@ double make_time(double hour, double min, double sec, double ms); double make_day(double year, double month, double date); double make_date(double day, double time); double time_clip(double time); -Optional parse_utc_offset(StringView); -bool is_time_zone_offset_string(StringView offset_string); -double parse_time_zone_offset_string(StringView offset_string); +bool is_offset_time_zone_identifier(StringView offset_string); +ThrowCompletionOr parse_date_time_utc_offset(VM&, StringView offset_string); +double parse_date_time_utc_offset(StringView offset_string); +double parse_date_time_utc_offset(Temporal::TimeZoneOffset const&); } diff --git a/Libraries/LibJS/Runtime/DatePrototype.cpp b/Libraries/LibJS/Runtime/DatePrototype.cpp index 9f12a403535..14740241eb8 100644 --- a/Libraries/LibJS/Runtime/DatePrototype.cpp +++ b/Libraries/LibJS/Runtime/DatePrototype.cpp @@ -2,7 +2,7 @@ * Copyright (c) 2020-2023, Linus Groh * Copyright (c) 2021, Petróczi Zoltán * Copyright (c) 2021, Idan Horowitz - * Copyright (c) 2022, Tim Flynn + * Copyright (c) 2022-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -21,6 +21,8 @@ #include #include #include +#include +#include #include #include #include @@ -1066,19 +1068,14 @@ JS_DEFINE_NATIVE_FUNCTION(DatePrototype::to_string) } // 21.4.4.41.1 TimeString ( tv ), https://tc39.es/ecma262/#sec-timestring +// 14.5.8 TimeString ( tv ), https://tc39.es/proposal-temporal/#sec-timestring ByteString time_string(double time) { - // 1. Let hour be ToZeroPaddedDecimalString(ℝ(HourFromTime(tv)), 2). - auto hour = hour_from_time(time); + // 1. Let timeString be FormatTimeString(ℝ(HourFromTime(tv)), ℝ(MinFromTime(tv)), ℝ(SecFromTime(tv)), 0, 0). + auto time_string = Temporal::format_time_string(hour_from_time(time), min_from_time(time), sec_from_time(time), 0, 0); - // 2. Let minute be ToZeroPaddedDecimalString(ℝ(MinFromTime(tv)), 2). - auto minute = min_from_time(time); - - // 3. Let second be ToZeroPaddedDecimalString(ℝ(SecFromTime(tv)), 2). - auto second = sec_from_time(time); - - // 4. Return the string-concatenation of hour, ":", minute, ":", second, the code unit 0x0020 (SPACE), and "GMT". - return ByteString::formatted("{:02}:{:02}:{:02} GMT", hour, minute, second); + // 4. Return the string-concatenation of timeString, the code unit 0x0020 (SPACE), and "GMT". + return ByteString::formatted("{} GMT", time_string); } // 21.4.4.41.2 DateString ( tv ), https://tc39.es/ecma262/#sec-datestring @@ -1105,62 +1102,40 @@ ByteString date_string(double time) } // 21.4.4.41.3 TimeZoneString ( tv ), https://tc39.es/ecma262/#sec-timezoneestring +// 14.5.9 TimeZoneString ( tv ), https://tc39.es/proposal-temporal/#sec-timezoneestring ByteString time_zone_string(double time) { // 1. Let systemTimeZoneIdentifier be SystemTimeZoneIdentifier(). auto system_time_zone_identifier = JS::system_time_zone_identifier(); - double offset_nanoseconds { 0 }; + // 2. Let offsetMinutes be ! ParseTimeZoneIdentifier(systemTimeZoneIdentifier).[[OffsetMinutes]]. + auto offset_minutes = Temporal::parse_time_zone_identifier(system_time_zone_identifier).offset_minutes; auto in_dst = Unicode::TimeZoneOffset::InDST::No; - // 2. If IsTimeZoneOffsetString(systemTimeZoneIdentifier) is true, then - if (is_time_zone_offset_string(system_time_zone_identifier)) { - // a. Let offsetNs be ParseTimeZoneOffsetString(systemTimeZoneIdentifier). - offset_nanoseconds = parse_time_zone_offset_string(system_time_zone_identifier); - } - // 3. Else, - else { + // 2. If offsetMinutes is EMPTY, then + if (!offset_minutes.has_value()) { // a. Let offsetNs be GetNamedTimeZoneOffsetNanoseconds(systemTimeZoneIdentifier, ℤ(ℝ(tv) × 10^6)). auto offset = get_named_time_zone_offset_milliseconds(system_time_zone_identifier, time); - - offset_nanoseconds = static_cast(offset.offset.to_nanoseconds()); in_dst = offset.in_dst; + + // b. Set offsetMinutes to truncate(offsetNs / (60 × 10**9)). + offset_minutes = trunc(static_cast(offset.offset.to_nanoseconds()) / 60'000'000'000.0); } - // 4. Let offset be 𝔽(truncate(offsetNs / 106)). - auto offset = trunc(offset_nanoseconds / 1e6); + // 3. Let offsetString be FormatOffsetTimeZoneIdentifier(offsetMinutes, UNSEPARATED). + auto offset_string = Temporal::format_offset_time_zone_identifier(*offset_minutes, Temporal::TimeStyle::Unseparated); - StringView offset_sign; - - // 5. If offset is +0𝔽 or offset > +0𝔽, then - if (offset >= 0) { - // a. Let offsetSign be "+". - offset_sign = "+"sv; - // b. Let absOffset be offset. - } - // 6. Else, - else { - // a. Let offsetSign be "-". - offset_sign = "-"sv; - // b. Let absOffset be -offset. - offset *= -1; - } - - // 7. Let offsetMin be ToZeroPaddedDecimalString(ℝ(MinFromTime(absOffset)), 2). - auto offset_min = min_from_time(offset); - - // 8. Let offsetHour be ToZeroPaddedDecimalString(ℝ(HourFromTime(absOffset)), 2). - auto offset_hour = hour_from_time(offset); - - // 9. Let tzName be an implementation-defined string that is either the empty String or the string-concatenation of the code unit 0x0020 (SPACE), the code unit 0x0028 (LEFT PARENTHESIS), an implementation-defined timezone name, and the code unit 0x0029 (RIGHT PARENTHESIS). + // 5. Let tzName be an implementation-defined string that is either the empty String or the string-concatenation of + // the code unit 0x0020 (SPACE), the code unit 0x0028 (LEFT PARENTHESIS), an implementation-defined timezone name, + // and the code unit 0x0029 (RIGHT PARENTHESIS). auto tz_name = Unicode::current_time_zone(); // Most implementations seem to prefer the long-form display name of the time zone. Not super important, but we may as well match that behavior. if (auto name = Unicode::time_zone_display_name(Unicode::default_locale(), tz_name, in_dst, time); name.has_value()) tz_name = name.release_value(); - // 10. Return the string-concatenation of offsetSign, offsetHour, offsetMin, and tzName. - return ByteString::formatted("{}{:02}{:02} ({})", offset_sign, offset_hour, offset_min, tz_name); + // 10. Return the string-concatenation of offsetString and tzName. + return ByteString::formatted("{} ({})", offset_string, tz_name); } // 21.4.4.41.4 ToDateString ( tv ), https://tc39.es/ecma262/#sec-todatestring diff --git a/Libraries/LibJS/Runtime/Intl/DateTimeFormatConstructor.cpp b/Libraries/LibJS/Runtime/Intl/DateTimeFormatConstructor.cpp index 64a9187ae6f..a6e946d394f 100644 --- a/Libraries/LibJS/Runtime/Intl/DateTimeFormatConstructor.cpp +++ b/Libraries/LibJS/Runtime/Intl/DateTimeFormatConstructor.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2024, Tim Flynn + * Copyright (c) 2021-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -81,6 +82,8 @@ JS_DEFINE_NATIVE_FUNCTION(DateTimeFormatConstructor::supported_locales_of) } // 11.1.2 CreateDateTimeFormat ( newTarget, locales, options, required, defaults ), https://tc39.es/ecma402/#sec-createdatetimeformat +// 15.7.1 CreateDateTimeFormat ( newTarget, locales, options, required, defaults [ , toLocaleStringTimeZone ] ), https://tc39.es/proposal-temporal/#sec-createdatetimeformat +// FIXME: Update the rest of this AO for Temporal once we have the required Temporal objects. ThrowCompletionOr> create_date_time_format(VM& vm, FunctionObject& new_target, Value locales_value, Value options_value, OptionRequired required, OptionDefaults defaults) { // 1. Let dateTimeFormat be ? OrdinaryCreateFromConstructor(newTarget, "%Intl.DateTimeFormat.prototype%", « [[InitializedDateTimeFormat]], [[Locale]], [[Calendar]], [[NumberingSystem]], [[TimeZone]], [[HourCycle]], [[DateStyle]], [[TimeStyle]], [[DateTimeFormat]], [[BoundFormat]] »). @@ -200,29 +203,25 @@ ThrowCompletionOr> create_date_time_format(VM& vm, Funct } // 29. If IsTimeZoneOffsetString(timeZone) is true, then - bool is_time_zone_offset_string = JS::is_time_zone_offset_string(time_zone); + bool is_time_zone_offset_string = JS::is_offset_time_zone_identifier(time_zone); if (is_time_zone_offset_string) { - // a. Let parseResult be ParseText(StringToCodePoints(timeZone), UTCOffset). - auto parse_result = parse_utc_offset(time_zone); + // a. Let parseResult be ParseText(StringToCodePoints(timeZone), UTCOffset[~SubMinutePrecision]). + auto parse_result = Temporal::parse_utc_offset(time_zone, Temporal::SubMinutePrecision::No); // b. Assert: parseResult is a Parse Node. VERIFY(parse_result.has_value()); - // c. If parseResult contains more than one MinuteSecond Parse Node, throw a RangeError exception. - if (parse_result->second.has_value()) - return vm.throw_completion(ErrorType::OptionIsNotValidValue, time_zone, vm.names.timeZone); + // c. Let offsetNanoseconds be ? ParseDateTimeUTCOffset(timeZone). + auto offset_nanoseconds = TRY(parse_date_time_utc_offset(vm, time_zone)); - // d. Let offsetNanoseconds be ParseTimeZoneOffsetString(timeZone). - auto offset_nanoseconds = parse_time_zone_offset_string(time_zone); - - // e. Let offsetMinutes be offsetNanoseconds / (6 × 10**10). + // d. Let offsetMinutes be offsetNanoseconds / (6 × 10**10). auto offset_minutes = offset_nanoseconds / 60'000'000'000; - // f. Assert: offsetMinutes is an integer. + // e. Assert: offsetMinutes is an integer. VERIFY(trunc(offset_minutes) == offset_minutes); - // g. Set timeZone to FormatOffsetTimeZoneIdentifier(offsetMinutes). + // f. Set timeZone to FormatOffsetTimeZoneIdentifier(offsetMinutes). time_zone = format_offset_time_zone_identifier(offset_minutes); } // 30. Else, diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp index e838f97d469..f1ac2c6e7a6 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp @@ -1366,10 +1366,7 @@ ThrowCompletionOr to_offset_string(VM& vm, Value argument) return vm.throw_completion(ErrorType::TemporalInvalidTimeZoneString, offset); // 3. Perform ? ParseDateTimeUTCOffset(offset). - // FIXME: ParseTimeZoneOffsetString should be renamed to ParseDateTimeUTCOffset and updated for Temporal. For now, we - // can just check with the ISO8601 parser directly. - if (!parse_utc_offset(argument.as_string().utf8_string_view(), SubMinutePrecision::Yes).has_value()) - return vm.throw_completion(ErrorType::TemporalInvalidTimeZoneString, offset); + TRY(parse_date_time_utc_offset(vm, offset.as_string().utf8_string_view())); // 4. Return offset. return offset.as_string().utf8_string(); diff --git a/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp b/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp index 33cc50020ac..31bc53655af 100644 --- a/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp +++ b/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp @@ -115,8 +115,7 @@ TimeZone parse_time_zone_identifier(ParseResult const& parse_result) // b. Let offsetString be the source text matched by the UTCOffset[~SubMinutePrecision] Parse Node contained within parseResult. // c. Let offsetNanoseconds be ! ParseDateTimeUTCOffset(offsetString). - // FIXME: ParseTimeZoneOffsetString should be renamed to ParseDateTimeUTCOffset and updated for Temporal. - auto offset_nanoseconds = parse_time_zone_offset_string(parse_result.time_zone_offset->source_text); + auto offset_nanoseconds = parse_date_time_utc_offset(parse_result.time_zone_offset->source_text); // d. Let offsetMinutes be offsetNanoseconds / (60 × 10**9). auto offset_minutes = offset_nanoseconds / 60'000'000'000;