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