From 4fc0fba646caae0bffca37a171cff2f74477d176 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Tue, 25 Jun 2024 11:06:08 -0400 Subject: [PATCH] LibCore+LibJS+LibUnicode: Port retrieving available time zones to ICU This required updating some LibJS spec steps to their latest versions, as the data expected by the old steps does not quite match the APIs that are available with the ICU. The new spec steps are much more aligned. --- Tests/LibUnicode/TestTimeZone.cpp | 14 ++++ Userland/Libraries/LibCore/DateTime.cpp | 10 ++- Userland/Libraries/LibJS/Runtime/Date.cpp | 68 +++++------------ Userland/Libraries/LibJS/Runtime/Date.h | 7 +- .../LibJS/Runtime/Intl/AbstractOperations.cpp | 74 ++++++++++++++++++ .../LibJS/Runtime/Intl/AbstractOperations.h | 3 + .../Intl/DateTimeFormatConstructor.cpp | 18 +++-- .../Libraries/LibJS/Runtime/Intl/Intl.cpp | 38 ++++------ .../LibJS/Runtime/Temporal/TimeZone.cpp | 39 +++++++--- ...ateTimeFormat.prototype.resolvedOptions.js | 6 +- .../Temporal/TimeZone/TimeZone.from.js | 2 +- .../builtins/Temporal/TimeZone/TimeZone.js | 2 +- .../TimeZone/TimeZone.prototype.toString.js | 2 +- Userland/Libraries/LibUnicode/TimeZone.cpp | 75 +++++++++++++++++++ Userland/Libraries/LibUnicode/TimeZone.h | 4 + 15 files changed, 259 insertions(+), 103 deletions(-) diff --git a/Tests/LibUnicode/TestTimeZone.cpp b/Tests/LibUnicode/TestTimeZone.cpp index 017ab32edcf..2992e3ea510 100644 --- a/Tests/LibUnicode/TestTimeZone.cpp +++ b/Tests/LibUnicode/TestTimeZone.cpp @@ -41,3 +41,17 @@ TEST_CASE(current_time_zone) EXPECT_EQ(Unicode::current_time_zone(), "UTC"sv); } } + +TEST_CASE(available_time_zones) +{ + auto const& time_zones = Unicode::available_time_zones(); + EXPECT(time_zones.contains_slow("UTC"sv)); + EXPECT(!time_zones.contains_slow("EAT"sv)); +} + +TEST_CASE(resolve_primary_time_zone) +{ + EXPECT_EQ(Unicode::resolve_primary_time_zone("UTC"sv), "Etc/UTC"sv); + EXPECT_EQ(Unicode::resolve_primary_time_zone("Asia/Katmandu"sv), "Asia/Kathmandu"sv); + EXPECT_EQ(Unicode::resolve_primary_time_zone("Australia/Canberra"sv), "Australia/Sydney"sv); +} diff --git a/Userland/Libraries/LibCore/DateTime.cpp b/Userland/Libraries/LibCore/DateTime.cpp index 7972779bbc9..2a4b2f80eee 100644 --- a/Userland/Libraries/LibCore/DateTime.cpp +++ b/Userland/Libraries/LibCore/DateTime.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -19,6 +20,7 @@ namespace Core { static Optional parse_time_zone_name(GenericLexer& lexer) { + auto const& time_zones = Unicode::available_time_zones(); auto start_position = lexer.tell(); Optional canonicalized_time_zone; @@ -26,8 +28,12 @@ static Optional parse_time_zone_name(GenericLexer& lexer) lexer.ignore_until([&](auto) { auto time_zone = lexer.input().substring_view(start_position, lexer.tell() - start_position + 1); - canonicalized_time_zone = TimeZone::canonicalize_time_zone(time_zone); - return canonicalized_time_zone.has_value(); + auto it = time_zones.find_if([&](auto const& candidate) { return time_zone.equals_ignoring_ascii_case(candidate); }); + if (it == time_zones.end()) + return false; + + canonicalized_time_zone = *it; + return true; }); if (canonicalized_time_zone.has_value()) diff --git a/Userland/Libraries/LibJS/Runtime/Date.cpp b/Userland/Libraries/LibJS/Runtime/Date.cpp index 955f6a3c1b8..831f505275d 100644 --- a/Userland/Libraries/LibJS/Runtime/Date.cpp +++ b/Userland/Libraries/LibJS/Runtime/Date.cpp @@ -1,6 +1,6 @@ /* * Copyright (c) 2020-2023, Linus Groh - * Copyright (c) 2022-2023, Tim Flynn + * Copyright (c) 2022-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -416,58 +417,25 @@ i64 get_named_time_zone_offset_nanoseconds(StringView time_zone_identifier, Cryp return offset->seconds * 1'000'000'000; } -// 21.4.1.23 AvailableNamedTimeZoneIdentifiers ( ), https://tc39.es/ecma262/#sec-time-zone-identifier-record -Vector available_named_time_zone_identifiers() -{ - // 1. If the implementation does not include local political rules for any time zones, then - // a. Return « the Time Zone Identifier Record { [[Identifier]]: "UTC", [[PrimaryIdentifier]]: "UTC" } ». - // NOTE: This step is not applicable as LibTimeZone will always return at least UTC, even if the TZDB is disabled. - - // 2. Let identifiers be the List of unique available named time zone identifiers. - auto identifiers = TimeZone::all_time_zones(); - - // 3. Sort identifiers into the same order as if an Array of the same values had been sorted using %Array.prototype.sort% with undefined as comparefn. - // NOTE: LibTimeZone provides the identifiers already sorted. - - // 4. Let result be a new empty List. - Vector result; - result.ensure_capacity(identifiers.size()); - - bool found_utc = false; - - // 5. For each element identifier of identifiers, do - for (auto identifier : identifiers) { - // a. Let primary be identifier. - auto primary = identifier.name; - - // b. If identifier is a non-primary time zone identifier in this implementation and identifier is not "UTC", then - if (identifier.is_link == TimeZone::IsLink::Yes && identifier.name != "UTC"sv) { - // i. Set primary to the primary time zone identifier associated with identifier. - // ii. NOTE: An implementation may need to resolve identifier iteratively to obtain the primary time zone identifier. - primary = TimeZone::canonicalize_time_zone(identifier.name).value(); - } - - // c. Let record be the Time Zone Identifier Record { [[Identifier]]: identifier, [[PrimaryIdentifier]]: primary }. - TimeZoneIdentifier record { .identifier = identifier.name, .primary_identifier = primary }; - - // d. Append record to result. - result.unchecked_append(record); - - if (!found_utc && identifier.name == "UTC"sv && primary == "UTC"sv) - found_utc = true; - } - - // 6. Assert: result contains a Time Zone Identifier Record r such that r.[[Identifier]] is "UTC" and r.[[PrimaryIdentifier]] is "UTC". - VERIFY(found_utc); - - // 7. Return result. - return result; -} - // 21.4.1.24 SystemTimeZoneIdentifier ( ), https://tc39.es/ecma262/#sec-systemtimezoneidentifier String system_time_zone_identifier() { - return Unicode::current_time_zone(); + // 1. If the implementation only supports the UTC time zone, return "UTC". + + // 2. Let systemTimeZoneString be the String representing the host environment's current time zone, either a primary + // 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)) { + auto time_zone_identifier = Intl::get_available_named_time_zone_identifier(system_time_zone_string); + if (!time_zone_identifier.has_value()) + return "UTC"_string; + + system_time_zone_string = time_zone_identifier->primary_identifier; + } + + // 3. Return systemTimeZoneString. + return system_time_zone_string; } // 21.4.1.25 LocalTime ( t ), https://tc39.es/ecma262/#sec-localtime diff --git a/Userland/Libraries/LibJS/Runtime/Date.h b/Userland/Libraries/LibJS/Runtime/Date.h index 5eb2b569326..7c7a44dbb2f 100644 --- a/Userland/Libraries/LibJS/Runtime/Date.h +++ b/Userland/Libraries/LibJS/Runtime/Date.h @@ -1,6 +1,6 @@ /* * Copyright (c) 2020-2022, Linus Groh - * Copyright (c) 2022-2023, Tim Flynn + * Copyright (c) 2022-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -35,8 +35,8 @@ private: // 21.4.1.22 Time Zone Identifier Record, https://tc39.es/ecma262/#sec-time-zone-identifier-record struct TimeZoneIdentifier { - StringView identifier; // [[Identifier]] - StringView primary_identifier; // [[PrimaryIdentifier]] + String identifier; // [[Identifier]] + String primary_identifier; // [[PrimaryIdentifier]] }; // https://tc39.es/ecma262/#eqn-HoursPerDay @@ -75,7 +75,6 @@ u16 ms_from_time(double); Crypto::SignedBigInteger get_utc_epoch_nanoseconds(i32 year, u8 month, u8 day, u8 hour, u8 minute, u8 second, u16 millisecond, u16 microsecond, u16 nanosecond); Vector get_named_time_zone_epoch_nanoseconds(StringView time_zone_identifier, i32 year, u8 month, u8 day, u8 hour, u8 minute, u8 second, u16 millisecond, u16 microsecond, u16 nanosecond); i64 get_named_time_zone_offset_nanoseconds(StringView time_zone_identifier, Crypto::SignedBigInteger const& epoch_nanoseconds); -Vector available_named_time_zone_identifiers(); String system_time_zone_identifier(); double local_time(double time); double utc_time(double time); diff --git a/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp index b7fe2ebb1f3..86c3e045366 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include namespace JS::Intl { @@ -138,6 +139,79 @@ bool is_well_formed_currency_code(StringView currency) return true; } +// 6.5.1 AvailableNamedTimeZoneIdentifiers ( ), https://tc39.es/ecma402/#sup-availablenamedtimezoneidentifiers +Vector const& available_named_time_zone_identifiers() +{ + // It is recommended that the result of AvailableNamedTimeZoneIdentifiers remains the same for the lifetime of the surrounding agent. + static auto named_time_zone_identifiers = []() { + // 1. Let identifiers be a List containing the String value of each Zone or Link name in the IANA Time Zone Database. + auto const& identifiers = Unicode::available_time_zones(); + + // 2. Assert: No element of identifiers is an ASCII-case-insensitive match for any other element. + // 3. Assert: Every element of identifiers identifies a Zone or Link name in the IANA Time Zone Database. + // 4. Sort identifiers according to lexicographic code unit order. + // NOTE: All of the above is handled by LibUnicode. + + // 5. Let result be a new empty List. + Vector result; + result.ensure_capacity(identifiers.size()); + + bool found_utc = false; + + // 6. For each element identifier of identifiers, do + for (auto const& identifier : identifiers) { + // a. Let primary be identifier. + auto primary = identifier; + + // b. If identifier is a Link name and identifier is not "UTC", then + if (identifier != "UTC"sv) { + if (auto resolved = Unicode::resolve_primary_time_zone(identifier); resolved.has_value() && identifier != resolved) { + // i. Set primary to the Zone name that identifier resolves to, according to the rules for resolving Link + // names in the IANA Time Zone Database. + primary = resolved.release_value(); + + // ii. NOTE: An implementation may need to resolve identifier iteratively. + } + } + + // c. If primary is one of "Etc/UTC", "Etc/GMT", or "GMT", set primary to "UTC". + if (primary.is_one_of("Etc/UTC"sv, "Etc/GMT"sv, "GMT"sv)) + primary = "UTC"_string; + + // d. Let record be the Time Zone Identifier Record { [[Identifier]]: identifier, [[PrimaryIdentifier]]: primary }. + TimeZoneIdentifier record { .identifier = identifier, .primary_identifier = primary }; + + // e. Append record to result. + result.unchecked_append(move(record)); + + if (!found_utc && identifier == "UTC"sv && primary == "UTC"sv) + found_utc = true; + } + + // 7. Assert: result contains a Time Zone Identifier Record r such that r.[[Identifier]] is "UTC" and r.[[PrimaryIdentifier]] is "UTC". + VERIFY(found_utc); + + // 8. Return result. + return result; + }(); + + return named_time_zone_identifiers; +} + +// 6.5.2 GetAvailableNamedTimeZoneIdentifier ( timeZoneIdentifier ), https://tc39.es/ecma402/#sec-getavailablenamedtimezoneidentifier +Optional get_available_named_time_zone_identifier(StringView time_zone_identifier) +{ + // 1. For each element record of AvailableNamedTimeZoneIdentifiers(), do + for (auto const& record : available_named_time_zone_identifiers()) { + // a. If record.[[Identifier]] is an ASCII-case-insensitive match for timeZoneIdentifier, return record. + if (record.identifier.equals_ignoring_ascii_case(time_zone_identifier)) + return record; + } + + // 2. Return EMPTY. + return {}; +} + // 6.6.1 IsWellFormedUnitIdentifier ( unitIdentifier ), https://tc39.es/ecma402/#sec-iswellformedunitidentifier bool is_well_formed_unit_identifier(StringView unit_identifier) { diff --git a/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h index 98b793f6e87..2f595c252f3 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -51,6 +52,8 @@ using StringOrBoolean = Variant; bool is_structurally_valid_language_tag(StringView locale); String canonicalize_unicode_locale_id(StringView locale); bool is_well_formed_currency_code(StringView currency); +Vector const& available_named_time_zone_identifiers(); +Optional get_available_named_time_zone_identifier(StringView time_zone_identifier); bool is_well_formed_unit_identifier(StringView unit_identifier); ThrowCompletionOr> canonicalize_locale_list(VM&, Value locales); Optional lookup_matching_locale_by_prefix(ReadonlySpan requested_locales); diff --git a/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormatConstructor.cpp b/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormatConstructor.cpp index c6f24351f82..703a58eeca6 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormatConstructor.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormatConstructor.cpp @@ -231,15 +231,17 @@ ThrowCompletionOr> create_date_time_format(VM& vm, // g. Set timeZone to FormatOffsetTimeZoneIdentifier(offsetMinutes). time_zone = format_offset_time_zone_identifier(offset_minutes); } - // 33. Else if IsValidTimeZoneName(timeZone) is true, then - else if (Temporal::is_available_time_zone_name(time_zone)) { - // a. Set timeZone to CanonicalizeTimeZoneName(timeZone). - time_zone = MUST(Temporal::canonicalize_time_zone_name(vm, time_zone)); - } - // 34. Else, + // 33. Else else { - // a. Throw a RangeError exception. - return vm.throw_completion(ErrorType::OptionIsNotValidValue, time_zone, vm.names.timeZone); + // a. Let timeZoneIdentifierRecord be GetAvailableNamedTimeZoneIdentifier(timeZone). + auto time_zone_identifier_record = get_available_named_time_zone_identifier(time_zone); + + // b. If timeZoneIdentifierRecord is EMPTY, throw a RangeError exception. + if (!time_zone_identifier_record.has_value()) + return vm.throw_completion(ErrorType::OptionIsNotValidValue, time_zone, vm.names.timeZone); + + // c. Set timeZone to timeZoneIdentifierRecord.[[PrimaryIdentifier]]. + time_zone = time_zone_identifier_record->primary_identifier; } // 35. Set dateTimeFormat.[[TimeZone]] to timeZone. diff --git a/Userland/Libraries/LibJS/Runtime/Intl/Intl.cpp b/Userland/Libraries/LibJS/Runtime/Intl/Intl.cpp index 52e5ec8d237..19ca735541b 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/Intl.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/Intl.cpp @@ -1,11 +1,13 @@ /* * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ #include #include +#include #include #include #include @@ -21,7 +23,6 @@ #include #include #include -#include #include #include #include @@ -82,32 +83,25 @@ JS_DEFINE_NATIVE_FUNCTION(Intl::get_canonical_locales) return Array::create_from(realm, marked_locale_list); } -// 6.5.4 AvailableCanonicalTimeZones ( ), https://tc39.es/ecma402/#sec-availablecanonicaltimezones -static Vector available_canonical_time_zones() +// 6.5.4 AvailablePrimaryTimeZoneIdentifiers ( ), https://tc39.es/ecma402/#sec-availableprimarytimezoneidentifiers +static Vector available_primary_time_zone_identifiers() { - // 1. Let names be a List of all supported Zone and Link names in the IANA Time Zone Database. - auto names = TimeZone::all_time_zones(); + // 1. Let records be AvailableNamedTimeZoneIdentifiers(). + auto const& records = available_named_time_zone_identifiers(); // 2. Let result be a new empty List. - Vector result; + Vector result; - // 3. For each element name of names, do - for (auto const& name : names) { - // a. Assert: IsValidTimeZoneName( name ) is true. - // b. Let canonical be ! CanonicalizeTimeZoneName( name ). - auto canonical = TimeZone::canonicalize_time_zone(name.name).value(); - - // c. If result does not contain an element equal to canonical, then - if (!result.contains_slow(canonical)) { - // i. Append canonical to the end of result. - result.append(canonical); + // 3. For each element timeZoneIdentifierRecord of records, do + for (auto const& time_zone_identifier_record : records) { + // a. If timeZoneIdentifierRecord.[[Identifier]] is timeZoneIdentifierRecord.[[PrimaryIdentifier]], then + if (time_zone_identifier_record.identifier == time_zone_identifier_record.primary_identifier) { + // i. Append timeZoneIdentifierRecord.[[Identifier]] to result. + result.append(time_zone_identifier_record.identifier); } } - // 4. Sort result in order as if an Array of the same values had been sorted using %Array.prototype.sort% using undefined as comparefn. - quick_sort(result); - - // 5. Return result. + // 4. Return result. return result; } @@ -143,8 +137,8 @@ JS_DEFINE_NATIVE_FUNCTION(Intl::supported_values_of) } // 6. Else if key is "timeZone", then else if (key == "timeZone"sv) { - // a. Let list be ! AvailableCanonicalTimeZones( ). - static auto const time_zones = available_canonical_time_zones(); + // a. Let list be ! AvailablePrimaryTimeZoneIdentifiers( ). + static auto const time_zones = available_primary_time_zone_identifiers(); list = time_zones.span(); } // 7. Else if key is "unit", then diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp index d408e1c0dcd..0f722a58ac2 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp @@ -20,7 +20,7 @@ #include #include #include -#include +#include namespace JS::Temporal { @@ -36,28 +36,45 @@ TimeZone::TimeZone(Object& prototype) bool is_available_time_zone_name(StringView time_zone) { // 1. Let timeZones be AvailableTimeZones(). + auto const& time_zones = Unicode::available_time_zones(); + // 2. For each String candidate in timeZones, do - // a. If timeZone is an ASCII-case-insensitive match for candidate, return true. + for (auto const& candidate : time_zones) { + // a. If timeZone is an ASCII-case-insensitive match for candidate, return true. + if (time_zone.equals_ignoring_ascii_case(candidate)) + return true; + } + // 3. Return false. - // NOTE: When LibTimeZone is built without ENABLE_TIME_ZONE_DATA, this only recognizes 'UTC', - // which matches the minimum requirements of the Temporal spec. - return ::TimeZone::time_zone_from_string(time_zone).has_value(); + return false; } // 6.4.2 CanonicalizeTimeZoneName ( timeZone ), https://tc39.es/ecma402/#sec-canonicalizetimezonename // 11.1.2 CanonicalizeTimeZoneName ( timeZone ), https://tc39.es/proposal-temporal/#sec-canonicalizetimezonename // 15.1.2 CanonicalizeTimeZoneName ( timeZone ), https://tc39.es/proposal-temporal/#sup-canonicalizetimezonename -ThrowCompletionOr canonicalize_time_zone_name(VM& vm, StringView time_zone) +ThrowCompletionOr canonicalize_time_zone_name(VM&, StringView time_zone) { - // 1. Let ianaTimeZone be the String value of the Zone or Link name of the IANA Time Zone Database that is an ASCII-case-insensitive match of timeZone as described in 6.1. - // 2. If ianaTimeZone is a Link name, let ianaTimeZone be the String value of the corresponding Zone name as specified in the file backward of the IANA Time Zone Database. - auto iana_time_zone = ::TimeZone::canonicalize_time_zone(time_zone); + auto const& time_zones = Unicode::available_time_zones(); + + // 1. Let ianaTimeZone be the String value of the Zone or Link name of the IANA Time Zone Database that is an + // ASCII-case-insensitive match of timeZone as described in 6.1. + auto it = time_zones.find_if([&](auto const& candidate) { + return time_zone.equals_ignoring_ascii_case(candidate); + }); + VERIFY(it != time_zones.end()); + + // 2. If ianaTimeZone is a Link name, let ianaTimeZone be the String value of the corresponding Zone name as specified + // in the file backward of the IANA Time Zone Database. + auto iana_time_zone = Unicode::resolve_primary_time_zone(*it).value_or_lazy_evaluated([&]() { + return MUST(String::from_utf8(time_zone)); + }); // 3. If ianaTimeZone is one of "Etc/UTC", "Etc/GMT", or "GMT", return "UTC". - // NOTE: This is already done in canonicalize_time_zone(). + if (iana_time_zone.is_one_of("Etc/UTC"sv, "Etc/GMT"sv, "GMT"sv)) + return "UTC"_string; // 4. Return ianaTimeZone. - return TRY_OR_THROW_OOM(vm, String::from_utf8(*iana_time_zone)); + return iana_time_zone; } // 11.6.1 CreateTemporalTimeZone ( identifier [ , newTarget ] ), https://tc39.es/proposal-temporal/#sec-temporal-createtemporaltimezone diff --git a/Userland/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.resolvedOptions.js b/Userland/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.resolvedOptions.js index bcfcbaf6ec7..50e9f1056f7 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.resolvedOptions.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.resolvedOptions.js @@ -124,13 +124,13 @@ describe("correct behavior", () => { }); test("timeZone", () => { - const en = new Intl.DateTimeFormat("en", { timeZone: "EST" }); - expect(en.resolvedOptions().timeZone).toBe("EST"); + const en = new Intl.DateTimeFormat("en", { timeZone: "EST5EDT" }); + expect(en.resolvedOptions().timeZone).toBe("EST5EDT"); const el = new Intl.DateTimeFormat("el", { timeZone: "UTC" }); expect(el.resolvedOptions().timeZone).toBe("UTC"); - ["UTC", "EST", "+01:02", "-20:30", "+00:00"].forEach(timeZone => { + ["UTC", "EST5EDT", "+01:02", "-20:30", "+00:00"].forEach(timeZone => { const en = new Intl.DateTimeFormat("en", { timeZone: timeZone }); expect(en.resolvedOptions().timeZone).toBe(timeZone); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Temporal/TimeZone/TimeZone.from.js b/Userland/Libraries/LibJS/Tests/builtins/Temporal/TimeZone/TimeZone.from.js index ef3d01d35a0..1b96d2fcad6 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Temporal/TimeZone/TimeZone.from.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Temporal/TimeZone/TimeZone.from.js @@ -26,7 +26,7 @@ describe("normal behavior", () => { ["Etc/GMT+12", "Etc/GMT+12"], ["Etc/GMT-12", "Etc/GMT-12"], ["Europe/London", "Europe/London"], - ["Europe/Isle_of_Man", "Europe/London"], + ["Australia/Canberra", "Australia/Sydney"], ["1970-01-01T00:00:00+01", "+01:00"], ["1970-01-01T00:00:00.000000000+01", "+01:00"], ["1970-01-01T00:00:00.000000000+01:00:00", "+01:00"], diff --git a/Userland/Libraries/LibJS/Tests/builtins/Temporal/TimeZone/TimeZone.js b/Userland/Libraries/LibJS/Tests/builtins/Temporal/TimeZone/TimeZone.js index 66910b50fcd..89979acb3d3 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Temporal/TimeZone/TimeZone.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Temporal/TimeZone/TimeZone.js @@ -47,7 +47,7 @@ describe("normal behavior", () => { ["Etc/GMT+12", "Etc/GMT+12"], ["Etc/GMT-12", "Etc/GMT-12"], ["Europe/London", "Europe/London"], - ["Europe/Isle_of_Man", "Europe/London"], + ["Australia/Canberra", "Australia/Sydney"], ]; for (const [arg, expected] of values) { expect(new Temporal.TimeZone(arg).id).toBe(expected); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Temporal/TimeZone/TimeZone.prototype.toString.js b/Userland/Libraries/LibJS/Tests/builtins/Temporal/TimeZone/TimeZone.prototype.toString.js index feefdc7463c..6e452075ff8 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Temporal/TimeZone/TimeZone.prototype.toString.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Temporal/TimeZone/TimeZone.prototype.toString.js @@ -12,7 +12,7 @@ describe("correct behavior", () => { ["Etc/UTC", "UTC"], ["Etc/GMT", "UTC"], ["Europe/London", "Europe/London"], - ["Europe/Isle_of_Man", "Europe/London"], + ["Australia/Canberra", "Australia/Sydney"], ["+00:00", "+00:00"], ["+00:00:00", "+00:00"], ["+00:00:00.000", "+00:00"], diff --git a/Userland/Libraries/LibUnicode/TimeZone.cpp b/Userland/Libraries/LibUnicode/TimeZone.cpp index c2435560802..07b9a9ddd85 100644 --- a/Userland/Libraries/LibUnicode/TimeZone.cpp +++ b/Userland/Libraries/LibUnicode/TimeZone.cpp @@ -6,11 +6,14 @@ #define AK_DONT_REPLACE_STD +#include #include +#include #include #include #include +#include namespace Unicode { @@ -34,4 +37,76 @@ String current_time_zone() return icu_string_to_string(time_zone_name); } +// https://github.com/unicode-org/icu/blob/main/icu4c/source/tools/tzcode/icuzones +static constexpr bool is_legacy_non_iana_time_zone(StringView time_zone) +{ + constexpr auto legacy_zones = to_array({ + "ACT"sv, + "AET"sv, + "AGT"sv, + "ART"sv, + "AST"sv, + "BET"sv, + "BST"sv, + "Canada/East-Saskatchewan"sv, + "CAT"sv, + "CNT"sv, + "CST"sv, + "CTT"sv, + "EAT"sv, + "ECT"sv, + "IET"sv, + "IST"sv, + "JST"sv, + "MIT"sv, + "NET"sv, + "NST"sv, + "PLT"sv, + "PNT"sv, + "PRT"sv, + "PST"sv, + "SST"sv, + "US/Pacific-New"sv, + "VST"sv, + }); + + if (time_zone.starts_with("SystemV/"sv)) + return true; + + return legacy_zones.contains_slow(time_zone); +} + +Vector const& available_time_zones() +{ + static auto time_zones = []() -> Vector { + UErrorCode status = U_ZERO_ERROR; + + auto time_zone_enumerator = adopt_own_if_nonnull(icu::TimeZone::createEnumeration(status)); + if (icu_failure(status)) + return { "UTC"_string }; + + auto time_zones = icu_string_enumeration_to_list(move(time_zone_enumerator), [](char const* zone) { + return !is_legacy_non_iana_time_zone({ zone, strlen(zone) }); + }); + + quick_sort(time_zones); + return time_zones; + }(); + + return time_zones; +} + +Optional resolve_primary_time_zone(StringView time_zone) +{ + UErrorCode status = U_ZERO_ERROR; + + icu::UnicodeString iana_id; + icu::TimeZone::getIanaID(icu_string(time_zone), iana_id, status); + + if (icu_failure(status)) + return {}; + + return icu_string_to_string(iana_id); +} + } diff --git a/Userland/Libraries/LibUnicode/TimeZone.h b/Userland/Libraries/LibUnicode/TimeZone.h index daf6130484c..a3a59063c56 100644 --- a/Userland/Libraries/LibUnicode/TimeZone.h +++ b/Userland/Libraries/LibUnicode/TimeZone.h @@ -4,12 +4,16 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include +#include #pragma once namespace Unicode { String current_time_zone(); +Vector const& available_time_zones(); +Optional resolve_primary_time_zone(StringView time_zone); }