From d527eb62da311d5c73c6821975c1d7fff36df340 Mon Sep 17 00:00:00 2001 From: Linus Groh Date: Tue, 11 Jan 2022 20:20:16 +0100 Subject: [PATCH] LibJS: Support non-UTC time zones in Temporal :^) We can now recognize & normalize all time zones from the IANA time zone database and not just 'UTC', which makes the LibJS Temporal implementation a lot more useful! Thanks to the newly added LibTimeZone, this was incredibly easy to implement :^) This already includes these recent editorial changes in the Temporal spec: https://github.com/tc39/proposal-temporal/commit/27bffe1 --- Userland/Libraries/LibJS/CMakeLists.txt | 2 +- .../LibJS/Runtime/Temporal/TimeZone.cpp | 40 +++++++++---------- .../Temporal/TimeZone/TimeZone.from.js | 10 ++++- .../builtins/Temporal/TimeZone/TimeZone.js | 19 +++++++-- .../TimeZone/TimeZone.prototype.toString.js | 5 +++ 5 files changed, 49 insertions(+), 27 deletions(-) diff --git a/Userland/Libraries/LibJS/CMakeLists.txt b/Userland/Libraries/LibJS/CMakeLists.txt index e59cc737fa1..4c104dbda5e 100644 --- a/Userland/Libraries/LibJS/CMakeLists.txt +++ b/Userland/Libraries/LibJS/CMakeLists.txt @@ -219,4 +219,4 @@ set(SOURCES ) serenity_lib(LibJS js) -target_link_libraries(LibJS LibM LibCore LibCrypto LibRegex LibSyntax LibUnicode) +target_link_libraries(LibJS LibM LibCore LibCrypto LibRegex LibSyntax LibTimeZone LibUnicode) diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp index d6586b1dc8f..3c2312cded6 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp @@ -16,6 +16,7 @@ #include #include #include +#include namespace JS::Temporal { @@ -27,35 +28,30 @@ TimeZone::TimeZone(String identifier, Object& prototype) } // 11.1.1 IsValidTimeZoneName ( timeZone ), https://tc39.es/proposal-temporal/#sec-isvalidtimezonename -// NOTE: This is the minimum implementation of IsValidTimeZoneName, supporting only the "UTC" time zone. +// 15.1.1 IsValidTimeZoneName ( timeZone ), https://tc39.es/proposal-temporal/#sup-isvalidtimezonename bool is_valid_time_zone_name(String const& time_zone) { - // 1. Assert: Type(timeZone) is String. - - // 2. Let tzText be ! StringToCodePoints(timeZone). - // 3. Let tzUpperText be the result of toUppercase(tzText), according to the Unicode Default Case Conversion algorithm. - // 4. Let tzUpper be ! CodePointsToString(tzUpperText). - auto tz_upper = time_zone.to_uppercase(); - - // 5. If tzUpper and "UTC" are the same sequence of code points, return true. - if (tz_upper == "UTC") - return true; - - // 6. Return false. - return false; + // 1. If one of the Zone or Link names of the IANA Time Zone Database is an ASCII-case-insensitive match of timeZone as described in 6.1, return true. + // 2. If timeZone is an ASCII-case-insensitive match of "UTC", 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(); } // 11.1.2 CanonicalizeTimeZoneName ( timeZone ), https://tc39.es/proposal-temporal/#sec-canonicalizetimezonename -// NOTE: This is the minimum implementation of CanonicalizeTimeZoneName, supporting only the "UTC" time zone. +// 15.1.2 CanonicalizeTimeZoneName ( timeZone ), https://tc39.es/proposal-temporal/#sup-canonicalizetimezonename String canonicalize_time_zone_name(String const& time_zone) { - // 1. Assert: Type(timeZone) is String. + // 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); - // 2. Assert: ! IsValidTimeZoneName(timeZone) is true. - VERIFY(is_valid_time_zone_name(time_zone)); + // 3. If ianaTimeZone is "Etc/UTC" or "Etc/GMT", return "UTC". + // NOTE: This is already done in canonicalize_time_zone(). - // 3. Return "UTC". - return "UTC"; + // 4. Return ianaTimeZone. + return *iana_time_zone; } // 11.1.3 DefaultTimeZone ( ), https://tc39.es/proposal-temporal/#sec-defaulttimezone @@ -160,12 +156,12 @@ ISODateTime get_iso_parts_from_epoch(BigInt const& epoch_nanoseconds) } // 11.6.4 GetIANATimeZoneEpochValue ( timeZoneIdentifier, year, month, day, hour, minute, second, millisecond, microsecond, nanosecond ), https://tc39.es/proposal-temporal/#sec-temporal-getianatimezoneepochvalue -MarkedValueList get_iana_time_zone_epoch_value(GlobalObject& global_object, StringView time_zone_identifier, i32 year, u8 month, u8 day, u8 hour, u8 minute, u8 second, u16 millisecond, u16 microsecond, u16 nanosecond) +MarkedValueList get_iana_time_zone_epoch_value(GlobalObject& global_object, [[maybe_unused]] StringView time_zone_identifier, i32 year, u8 month, u8 day, u8 hour, u8 minute, u8 second, u16 millisecond, u16 microsecond, u16 nanosecond) { // The abstract operation GetIANATimeZoneEpochValue is an implementation-defined algorithm that returns a List of integers. Each integer in the List represents a number of nanoseconds since the Unix epoch in UTC that may correspond to the given calendar date and wall-clock time in the IANA time zone identified by timeZoneIdentifier. // When the input represents a local time repeating multiple times at a negative time zone transition (e.g. when the daylight saving time ends or the time zone offset is decreased due to a time zone rule change), the returned List will have more than one element. When the input represents a skipped local time at a positive time zone transition (e.g. when the daylight saving time starts or the time zone offset is increased due to a time zone rule change), the returned List will be empty. Otherwise, the returned List will have one element. - VERIFY(time_zone_identifier == "UTC"sv); + // FIXME: Implement this properly for non-UTC timezones. // FIXME: MarkedValueList for T != Value would still be nice. auto& vm = global_object.vm(); auto list = MarkedValueList { vm.heap() }; 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 f16e3f6d888..9fcf5cab9fb 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Temporal/TimeZone/TimeZone.from.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Temporal/TimeZone/TimeZone.from.js @@ -10,6 +10,14 @@ describe("normal behavior", () => { expect(Temporal.TimeZone.from(timeZone)).toBe(timeZone); expect(Temporal.TimeZone.from(timeZoneLike)).toBe(timeZoneLike); expect(Temporal.TimeZone.from(zonedDateTimeLike)).toBe(zonedDateTimeLike.timeZone); - // TODO: test from("string") once ParseTemporalTimeZoneString is working + expect(Temporal.TimeZone.from("UTC").id).toBe("UTC"); + expect(Temporal.TimeZone.from("GMT").id).toBe("UTC"); + expect(Temporal.TimeZone.from("Etc/UTC").id).toBe("UTC"); + expect(Temporal.TimeZone.from("Etc/GMT").id).toBe("UTC"); + // FIXME: https://github.com/tc39/proposal-temporal/issues/1993 + // expect(Temporal.TimeZone.from("Etc/GMT+12").id).toBe("Etc/GMT+12"); + // expect(Temporal.TimeZone.from("Etc/GMT-12").id).toBe("Etc/GMT-12"); + expect(Temporal.TimeZone.from("Europe/London").id).toBe("Europe/London"); + expect(Temporal.TimeZone.from("Europe/Isle_of_Man").id).toBe("Europe/London"); }); }); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Temporal/TimeZone/TimeZone.js b/Userland/Libraries/LibJS/Tests/builtins/Temporal/TimeZone/TimeZone.js index 6d3e53babbb..66910b50fcd 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Temporal/TimeZone/TimeZone.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Temporal/TimeZone/TimeZone.js @@ -36,9 +36,22 @@ describe("normal behavior", () => { }); test("canonicalizes time zone name", () => { - expect(new Temporal.TimeZone("Utc").id).toBe("UTC"); - expect(new Temporal.TimeZone("utc").id).toBe("UTC"); - expect(new Temporal.TimeZone("uTC").id).toBe("UTC"); + const values = [ + ["UTC", "UTC"], + ["Utc", "UTC"], + ["utc", "UTC"], + ["uTc", "UTC"], + ["GMT", "UTC"], + ["Etc/UTC", "UTC"], + ["Etc/GMT", "UTC"], + ["Etc/GMT+12", "Etc/GMT+12"], + ["Etc/GMT-12", "Etc/GMT-12"], + ["Europe/London", "Europe/London"], + ["Europe/Isle_of_Man", "Europe/London"], + ]; + for (const [arg, expected] of values) { + expect(new Temporal.TimeZone(arg).id).toBe(expected); + } }); test("numeric UTC offset", () => { 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 2814b13c7eb..feefdc7463c 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 @@ -8,6 +8,11 @@ describe("correct behavior", () => { ["utc", "UTC"], ["Utc", "UTC"], ["UTC", "UTC"], + ["GMT", "UTC"], + ["Etc/UTC", "UTC"], + ["Etc/GMT", "UTC"], + ["Europe/London", "Europe/London"], + ["Europe/Isle_of_Man", "Europe/London"], ["+00:00", "+00:00"], ["+00:00:00", "+00:00"], ["+00:00:00.000", "+00:00"],