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.
This commit is contained in:
Timothy Flynn 2024-11-20 20:06:42 -05:00
parent d1a988930f
commit 91e25ca588
6 changed files with 113 additions and 284 deletions

View file

@ -1,20 +1,19 @@
/*
* Copyright (c) 2020-2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2022-2024, Tim Flynn <trflynn89@serenityos.org>
* Copyright (c) 2022-2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/CharacterTypes.h>
#include <AK/GenericLexer.h>
#include <AK/NumericLimits.h>
#include <AK/ScopeGuard.h>
#include <AK/StringBuilder.h>
#include <AK/Time.h>
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/Date.h>
#include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/Intl/AbstractOperations.h>
#include <LibJS/Runtime/Temporal/ISO8601.h>
#include <LibJS/Runtime/Temporal/TimeZone.h>
#include <time.h>
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<double>(*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<double>(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<double>(*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<double>(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<UTCOffset> 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<u8> {
if (auto digits = lexer.peek_string(2); digits.has_value()) {
auto number = digits->to_number<u8>(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<double> 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<RangeError>(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<u8>().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<u8>().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<u8>().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);

View file

@ -1,6 +1,6 @@
/*
* Copyright (c) 2020-2022, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2022-2024, Tim Flynn <trflynn89@serenityos.org>
* Copyright (c) 2022-2024, Tim Flynn <trflynn89@ladybird.org>
*
* 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<char> sign;
Optional<u8> hour;
Optional<u8> minute;
Optional<u8> second;
Optional<StringView> 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<UTCOffset> 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<double> 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&);
}

View file

@ -2,7 +2,7 @@
* Copyright (c) 2020-2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2021, Petróczi Zoltán <petroczizoltan@tutanota.com>
* Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org>
* Copyright (c) 2022, Tim Flynn <trflynn89@serenityos.org>
* Copyright (c) 2022-2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -21,6 +21,8 @@
#include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/Intl/DateTimeFormat.h>
#include <LibJS/Runtime/Intl/DateTimeFormatConstructor.h>
#include <LibJS/Runtime/Temporal/AbstractOperations.h>
#include <LibJS/Runtime/Temporal/TimeZone.h>
#include <LibJS/Runtime/Value.h>
#include <LibJS/Runtime/ValueInlines.h>
#include <LibUnicode/DisplayNames.h>
@ -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<double>(offset.offset.to_nanoseconds());
in_dst = offset.in_dst;
// b. Set offsetMinutes to truncate(offsetNs / (60 × 10**9)).
offset_minutes = trunc(static_cast<double>(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

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021-2024, Tim Flynn <trflynn89@serenityos.org>
* Copyright (c) 2021-2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -11,6 +11,7 @@
#include <LibJS/Runtime/Intl/AbstractOperations.h>
#include <LibJS/Runtime/Intl/DateTimeFormat.h>
#include <LibJS/Runtime/Intl/DateTimeFormatConstructor.h>
#include <LibJS/Runtime/Temporal/ISO8601.h>
#include <LibUnicode/DateTimeFormat.h>
#include <LibUnicode/Locale.h>
@ -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<GC::Ref<DateTimeFormat>> 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<GC::Ref<DateTimeFormat>> 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<RangeError>(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,

View file

@ -1366,10 +1366,7 @@ ThrowCompletionOr<String> to_offset_string(VM& vm, Value argument)
return vm.throw_completion<TypeError>(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<RangeError>(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();

View file

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