From 672a555f981a81944bba8433d08fb627631139db Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Tue, 25 Jun 2024 15:27:20 -0400 Subject: [PATCH] LibCore+LibJS+LibUnicode: Port retrieving time zone offsets to ICU The changes to tests are due to LibTimeZone incorrectly interpreting time stamps in the TZDB. The TZDB will list zone transitions in either UTC or the zone's local time (which is then subject to DST offsets). LibTimeZone did not handle the latter at all. For example: The following rule is in effect until November 18, 6PM UTC. America/Chicago -5:50:36 - LMT 1883 Nov 18 18:00u The following rule is in effect until March 1, 2AM in Chicago time. But at that time, a DST transition occurs, so the local time is actually 3AM. America/Chicago -6:00 Chicago C%sT 1936 Mar 1 2:00 --- Meta/Lagom/CMakeLists.txt | 2 +- Tests/LibCore/CMakeLists.txt | 2 +- Tests/LibUnicode/TestTimeZone.cpp | 75 +++++++++++++++++++ Userland/Libraries/LibCore/CMakeLists.txt | 2 +- Userland/Libraries/LibCore/DateTime.cpp | 5 +- Userland/Libraries/LibJS/CMakeLists.txt | 2 +- Userland/Libraries/LibJS/Runtime/Date.cpp | 28 +++---- Userland/Libraries/LibJS/Runtime/Date.h | 3 +- .../Libraries/LibJS/Runtime/DatePrototype.cpp | 17 ++--- .../Runtime/Temporal/TimeZonePrototype.cpp | 3 +- ...eZone.prototype.getOffsetNanosecondsFor.js | 26 +++---- .../Libraries/LibUnicode/DisplayNames.cpp | 4 +- Userland/Libraries/LibUnicode/DisplayNames.h | 4 +- Userland/Libraries/LibUnicode/TimeZone.cpp | 21 ++++++ Userland/Libraries/LibUnicode/TimeZone.h | 12 +++ 15 files changed, 155 insertions(+), 51 deletions(-) diff --git a/Meta/Lagom/CMakeLists.txt b/Meta/Lagom/CMakeLists.txt index 6622a90d4e9..afd0f4c535b 100644 --- a/Meta/Lagom/CMakeLists.txt +++ b/Meta/Lagom/CMakeLists.txt @@ -544,7 +544,7 @@ if (BUILD_TESTING) lagom_test(../../Tests/LibCore/TestLibCorePromise.cpp LIBS LibThreading) endif() - lagom_test(../../Tests/LibCore/TestLibCoreDateTime.cpp LIBS LibTimeZone) + lagom_test(../../Tests/LibCore/TestLibCoreDateTime.cpp LIBS LibUnicode) # RegexLibC test POSIX and contains many Serenity extensions # It is therefore not reasonable to run it on Lagom, and we only run the Regex test diff --git a/Tests/LibCore/CMakeLists.txt b/Tests/LibCore/CMakeLists.txt index a159bf2c217..1a1f82f1de4 100644 --- a/Tests/LibCore/CMakeLists.txt +++ b/Tests/LibCore/CMakeLists.txt @@ -14,7 +14,7 @@ foreach(source IN LISTS TEST_SOURCES) serenity_test("${source}" LibCore) endforeach() -target_link_libraries(TestLibCoreDateTime PRIVATE LibTimeZone) +target_link_libraries(TestLibCoreDateTime PRIVATE LibUnicode) target_link_libraries(TestLibCorePromise PRIVATE LibThreading) # NOTE: Required because of the LocalServer tests target_link_libraries(TestLibCoreStream PRIVATE LibThreading) diff --git a/Tests/LibUnicode/TestTimeZone.cpp b/Tests/LibUnicode/TestTimeZone.cpp index 15f34901324..8124a2aa431 100644 --- a/Tests/LibUnicode/TestTimeZone.cpp +++ b/Tests/LibUnicode/TestTimeZone.cpp @@ -67,3 +67,78 @@ TEST_CASE(resolve_primary_time_zone) 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); } + +using enum Unicode::TimeZoneOffset::InDST; + +static void test_offset(StringView time_zone, i64 time, Duration expected_offset, Unicode::TimeZoneOffset::InDST expected_in_dst) +{ + auto actual_offset = Unicode::time_zone_offset(time_zone, AK::UnixDateTime::from_seconds_since_epoch(time)); + VERIFY(actual_offset.has_value()); + + EXPECT_EQ(actual_offset->offset, expected_offset); + EXPECT_EQ(actual_offset->in_dst, expected_in_dst); +} + +static constexpr Duration offset(i64 sign, i64 hours, i64 minutes, i64 seconds) +{ + return Duration::from_seconds(sign * ((hours * 3600) + (minutes * 60) + seconds)); +} + +// Useful website to convert times in the TZDB (which sometimes are and aren't UTC) to UTC and the desired local time: +// https://www.epochconverter.com/#tools +// +// In the tests below, if only UTC time is shown as a comment, then the corresponding Rule change in the TZDB was specified +// as UTC. Otherwise, the TZDB time was local, and was converted to a UTC timestamp for that test. +TEST_CASE(time_zone_offset) +{ + EXPECT(!Unicode::time_zone_offset("I don't exist"sv, {}).has_value()); + + test_offset("America/Chicago"sv, -2717647201, offset(-1, 5, 50, 36), No); // November 18, 1883 5:59:59 PM UTC + test_offset("America/Chicago"sv, -2717647200, offset(-1, 6, 0, 0), No); // November 18, 1883 6:00:00 PM UTC + test_offset("America/Chicago"sv, -1067788860, offset(-1, 6, 0, 0), No); // March 1, 1936 1:59:00 AM Chicago (March 1, 1936 7:59:00 AM UTC) + test_offset("America/Chicago"sv, -1067788800, offset(-1, 5, 0, 0), No); // March 1, 1936 3:00:00 AM Chicago (March 1, 1936 8:00:00 AM UTC) + test_offset("America/Chicago"sv, -1045414860, offset(-1, 5, 0, 0), No); // November 15, 1936 1:59:00 AM Chicago (November 15, 1936 6:59:00 AM UTC) + test_offset("America/Chicago"sv, -1045411200, offset(-1, 6, 0, 0), No); // November 15, 1936 2:00:00 AM Chicago (November 15, 1936 8:00:00 AM UTC) + + test_offset("Europe/London"sv, -3852662326, offset(-1, 0, 1, 15), No); // November 30, 1847 11:59:59 PM London (December 1, 1847 12:01:14 AM UTC) + test_offset("Europe/London"sv, -3852662325, offset(+1, 0, 0, 0), No); // December 1, 1847 12:01:15 AM London (December 1, 1847 12:01:15 AM UTC) + test_offset("Europe/London"sv, -59004001, offset(+1, 0, 0, 0), No); // February 18, 1968 1:59:59 AM London (February 18, 1968 1:59:59 AM UTC) + test_offset("Europe/London"sv, -59004000, offset(+1, 1, 0, 0), Yes); // February 18, 1968 3:00:00 AM London (February 18, 1968 2:00:00 AM UTC) + test_offset("Europe/London"sv, 57722399, offset(+1, 1, 0, 0), No); // October 31, 1971 1:59:59 AM UTC + test_offset("Europe/London"sv, 57722400, offset(+1, 0, 0, 0), No); // October 31, 1971 2:00:00 AM UTC + + test_offset("UTC"sv, -1641846268, offset(+1, 0, 00, 00), No); + test_offset("UTC"sv, 0, offset(+1, 0, 00, 00), No); + test_offset("UTC"sv, 1641846268, offset(+1, 0, 00, 00), No); + + test_offset("Etc/GMT+4"sv, -1641846268, offset(-1, 4, 00, 00), No); + test_offset("Etc/GMT+5"sv, 0, offset(-1, 5, 00, 00), No); + test_offset("Etc/GMT+6"sv, 1641846268, offset(-1, 6, 00, 00), No); + + test_offset("Etc/GMT-12"sv, -1641846268, offset(+1, 12, 00, 00), No); + test_offset("Etc/GMT-13"sv, 0, offset(+1, 13, 00, 00), No); + test_offset("Etc/GMT-14"sv, 1641846268, offset(+1, 14, 00, 00), No); +} + +TEST_CASE(time_zone_offset_with_dst) +{ + test_offset("America/New_York"sv, 1642576528, offset(-1, 5, 00, 00), No); // January 19, 2022 2:15:28 AM New York (January 19, 2022 7:15:28 AM UTC) + test_offset("America/New_York"sv, 1663568128, offset(-1, 4, 00, 00), Yes); // September 19, 2022 2:15:28 AM New York (September 19, 2022 6:15:28 AM UTC) + test_offset("America/New_York"sv, 1671471238, offset(-1, 5, 00, 00), No); // December 19, 2022 12:33:58 PM New York (December 19, 2022 5:33:58 PM UTC) + + // Phoenix does not observe DST. + test_offset("America/Phoenix"sv, 1642583728, offset(-1, 7, 00, 00), No); // January 19, 2022 2:15:28 AM Phoenix (January 19, 2022 9:15:28 AM UTC) + test_offset("America/Phoenix"sv, 1663578928, offset(-1, 7, 00, 00), No); // September 19, 2022 2:15:28 AM Phoenix (September 19, 2022 9:15:28 AM UTC) + test_offset("America/Phoenix"sv, 1671478438, offset(-1, 7, 00, 00), No); // December 19, 2022 12:33:58 PM Phoenix (December 19, 2022 7:33:58 PM UTC) + + // Moscow's observed DST changed several times in 1919. + test_offset("Europe/Moscow"sv, -1609459200, offset(+1, 3, 31, 19), Yes); // January 1, 1919 12:00:00 AM UTC + test_offset("Europe/Moscow"sv, -1596429079, offset(+1, 4, 31, 19), Yes); // June 1, 1919 12:00:00 AM Moscow (May 31, 1919 7:28:41 PM UTC) + test_offset("Europe/Moscow"sv, -1592625600, offset(+1, 4, 00, 00), Yes); // July 15, 1919 12:00:00 AM Moscow (July 14, 1919 8:00:00 PM UTC) + test_offset("Europe/Moscow"sv, -1589079600, offset(+1, 3, 00, 00), No); // August 25, 1919 12:00:00 AM Moscow (August 24, 1919 9:00:00 PM UTC) + + // Paraguay begins the year in DST. + test_offset("America/Asuncion"sv, 1642569328, offset(-1, 3, 00, 00), Yes); // January 19, 2022 2:15:28 AM Asuncion (January 19, 2022 5:15:28 AM UTC) + test_offset("America/Asuncion"sv, 1663568128, offset(-1, 4, 00, 00), No); // September 19, 2022 2:15:28 AM Asuncion (September 19, 2022 6:15:28 AM UTC) + test_offset("America/Asuncion"sv, 1671464038, offset(-1, 3, 00, 00), Yes); // December 19, 2022 12:33:58 PM Asuncion (December 19, 2022 3:33:58 PM UTC) +} diff --git a/Userland/Libraries/LibCore/CMakeLists.txt b/Userland/Libraries/LibCore/CMakeLists.txt index 6d7711ca773..a8cf281bb1b 100644 --- a/Userland/Libraries/LibCore/CMakeLists.txt +++ b/Userland/Libraries/LibCore/CMakeLists.txt @@ -88,7 +88,7 @@ if (APPLE) endif() serenity_lib(LibCore core) -target_link_libraries(LibCore PRIVATE LibCrypt LibTimeZone LibURL) +target_link_libraries(LibCore PRIVATE LibCrypt LibUnicode LibURL) target_link_libraries(LibCore PUBLIC LibCoreMinimal) if (APPLE) diff --git a/Userland/Libraries/LibCore/DateTime.cpp b/Userland/Libraries/LibCore/DateTime.cpp index 2a4b2f80eee..4155ed2be17 100644 --- a/Userland/Libraries/LibCore/DateTime.cpp +++ b/Userland/Libraries/LibCore/DateTime.cpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include #include @@ -44,8 +43,8 @@ static Optional parse_time_zone_name(GenericLexer& lexer) static void apply_time_zone_offset(StringView time_zone, UnixDateTime& time) { - if (auto offset = TimeZone::get_time_zone_offset(time_zone, time); offset.has_value()) - time -= Duration::from_seconds(offset->seconds); + if (auto offset = Unicode::time_zone_offset(time_zone, time); offset.has_value()) + time -= offset->offset; } DateTime DateTime::now() diff --git a/Userland/Libraries/LibJS/CMakeLists.txt b/Userland/Libraries/LibJS/CMakeLists.txt index 6685ad4b807..87c1d13182e 100644 --- a/Userland/Libraries/LibJS/CMakeLists.txt +++ b/Userland/Libraries/LibJS/CMakeLists.txt @@ -271,7 +271,7 @@ set(SOURCES ) serenity_lib(LibJS js) -target_link_libraries(LibJS PRIVATE LibCore LibCrypto LibFileSystem LibRegex LibSyntax LibTimeZone) +target_link_libraries(LibJS PRIVATE LibCore LibCrypto LibFileSystem LibRegex LibSyntax) # Link LibUnicode publicly to ensure ICU data (which is in libicudata.a) is available in any process using LibJS. target_link_libraries(LibJS PUBLIC LibUnicode) diff --git a/Userland/Libraries/LibJS/Runtime/Date.cpp b/Userland/Libraries/LibJS/Runtime/Date.cpp index 831f505275d..b4c80a94241 100644 --- a/Userland/Libraries/LibJS/Runtime/Date.cpp +++ b/Userland/Libraries/LibJS/Runtime/Date.cpp @@ -13,8 +13,6 @@ #include #include #include -#include -#include #include namespace JS { @@ -390,31 +388,27 @@ Vector get_named_time_zone_epoch_nanoseconds(StringVie auto local_nanoseconds = get_utc_epoch_nanoseconds(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond); auto local_time = UnixDateTime::from_nanoseconds_since_epoch(clip_bigint_to_sane_time(local_nanoseconds)); - // FIXME: LibTimeZone does not behave exactly as the spec expects. It does not consider repeated or skipped time points. - auto offset = TimeZone::get_time_zone_offset(time_zone_identifier, local_time); + // FIXME: LibUnicode does not behave exactly as the spec expects. It does not consider repeated or skipped time points. + auto offset = Unicode::time_zone_offset(time_zone_identifier, local_time); // Can only fail if the time zone identifier is invalid, which cannot be the case here. VERIFY(offset.has_value()); - return { local_nanoseconds.minus(Crypto::SignedBigInteger { offset->seconds }.multiplied_by(s_one_billion_bigint)) }; + return { local_nanoseconds.minus(Crypto::SignedBigInteger { offset->offset.to_nanoseconds() }) }; } // 21.4.1.21 GetNamedTimeZoneOffsetNanoseconds ( timeZoneIdentifier, epochNanoseconds ), https://tc39.es/ecma262/#sec-getnamedtimezoneoffsetnanoseconds -i64 get_named_time_zone_offset_nanoseconds(StringView time_zone_identifier, Crypto::SignedBigInteger const& epoch_nanoseconds) +Unicode::TimeZoneOffset get_named_time_zone_offset_nanoseconds(StringView time_zone_identifier, Crypto::SignedBigInteger const& epoch_nanoseconds) { - // Only called with validated time zone identifier as argument. - auto time_zone = TimeZone::time_zone_from_string(time_zone_identifier); - VERIFY(time_zone.has_value()); - // Since UnixDateTime::from_seconds_since_epoch() and UnixDateTime::from_nanoseconds_since_epoch() both take an i64, converting to // seconds first gives us a greater range. The TZDB doesn't have sub-second offsets. auto seconds = epoch_nanoseconds.divided_by(s_one_billion_bigint).quotient; auto time = UnixDateTime::from_seconds_since_epoch(clip_bigint_to_sane_time(seconds)); - auto offset = TimeZone::get_time_zone_offset(*time_zone, time); + auto offset = Unicode::time_zone_offset(time_zone_identifier, time); VERIFY(offset.has_value()); - return offset->seconds * 1'000'000'000; + return offset.release_value(); } // 21.4.1.24 SystemTimeZoneIdentifier ( ), https://tc39.es/ecma262/#sec-systemtimezoneidentifier @@ -455,7 +449,8 @@ double local_time(double time) else { // a. Let offsetNs be GetNamedTimeZoneOffsetNanoseconds(systemTimeZoneIdentifier, ℤ(ℝ(t) × 10^6)). auto time_bigint = Crypto::SignedBigInteger { time }.multiplied_by(s_one_million_bigint); - offset_nanoseconds = get_named_time_zone_offset_nanoseconds(system_time_zone_identifier, time_bigint); + auto offset = get_named_time_zone_offset_nanoseconds(system_time_zone_identifier, time_bigint); + offset_nanoseconds = static_cast(offset.offset.to_nanoseconds()); } // 4. Let offsetMs be truncate(offsetNs / 10^6). @@ -497,13 +492,14 @@ double utc_time(double time) // ii. Let possibleInstantsBefore be GetNamedTimeZoneEpochNanoseconds(systemTimeZoneIdentifier, ℝ(YearFromTime(tBefore)), ℝ(MonthFromTime(tBefore)) + 1, ℝ(DateFromTime(tBefore)), ℝ(HourFromTime(tBefore)), ℝ(MinFromTime(tBefore)), ℝ(SecFromTime(tBefore)), ℝ(msFromTime(tBefore)), 0, 0), where tBefore is the largest integral Number < t for which possibleInstantsBefore is not empty (i.e., tBefore represents the last local time before the transition). // iii. Let disambiguatedInstant be the last element of possibleInstantsBefore. - // FIXME: This branch currently cannot be reached with our implementation, because LibTimeZone does not handle skipped time points. - // When GetNamedTimeZoneEpochNanoseconds is updated to use a LibTimeZone API which does handle them, implement these steps. + // FIXME: This branch currently cannot be reached with our implementation, because LibUnicode does not handle skipped time points. + // When GetNamedTimeZoneEpochNanoseconds is updated to use a LibUnicode API which does handle them, implement these steps. VERIFY_NOT_REACHED(); } // e. Let offsetNs be GetNamedTimeZoneOffsetNanoseconds(systemTimeZoneIdentifier, disambiguatedInstant). - offset_nanoseconds = get_named_time_zone_offset_nanoseconds(system_time_zone_identifier, disambiguated_instant); + auto offset = get_named_time_zone_offset_nanoseconds(system_time_zone_identifier, disambiguated_instant); + offset_nanoseconds = static_cast(offset.offset.to_nanoseconds()); } // 4. Let offsetMs be truncate(offsetNs / 10^6). diff --git a/Userland/Libraries/LibJS/Runtime/Date.h b/Userland/Libraries/LibJS/Runtime/Date.h index 7c7a44dbb2f..3a2e10e1c6c 100644 --- a/Userland/Libraries/LibJS/Runtime/Date.h +++ b/Userland/Libraries/LibJS/Runtime/Date.h @@ -9,6 +9,7 @@ #include #include +#include namespace JS { @@ -74,7 +75,7 @@ u8 sec_from_time(double); 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); +Unicode::TimeZoneOffset get_named_time_zone_offset_nanoseconds(StringView time_zone_identifier, Crypto::SignedBigInteger const& epoch_nanoseconds); String system_time_zone_identifier(); double local_time(double time); double utc_time(double time); diff --git a/Userland/Libraries/LibJS/Runtime/DatePrototype.cpp b/Userland/Libraries/LibJS/Runtime/DatePrototype.cpp index 9e3100071c0..3bb54f17fd6 100644 --- a/Userland/Libraries/LibJS/Runtime/DatePrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/DatePrototype.cpp @@ -24,7 +24,6 @@ #include #include #include -#include #include #include #include @@ -1114,6 +1113,7 @@ ByteString time_zone_string(double time) auto system_time_zone_identifier = JS::system_time_zone_identifier(); double offset_nanoseconds { 0 }; + auto in_dst = Unicode::TimeZoneOffset::InDST::No; // 2. If IsTimeZoneOffsetString(systemTimeZoneIdentifier) is true, then if (is_time_zone_offset_string(system_time_zone_identifier)) { @@ -1124,7 +1124,10 @@ ByteString time_zone_string(double time) else { // a. Let offsetNs be GetNamedTimeZoneOffsetNanoseconds(systemTimeZoneIdentifier, ℤ(ℝ(tv) × 10^6)). auto time_bigint = Crypto::SignedBigInteger { time }.multiplied_by(Crypto::UnsignedBigInteger { 1'000'000 }); - offset_nanoseconds = get_named_time_zone_offset_nanoseconds(system_time_zone_identifier, time_bigint); + auto offset = get_named_time_zone_offset_nanoseconds(system_time_zone_identifier, time_bigint); + + offset_nanoseconds = static_cast(offset.offset.to_nanoseconds()); + in_dst = offset.in_dst; } // 4. Let offset be 𝔽(truncate(offsetNs / 106)). @@ -1153,15 +1156,11 @@ ByteString time_zone_string(double time) 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). - String tz_name; + 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 maybe_offset = TimeZone::get_time_zone_offset(tz_name, AK::UnixDateTime::from_milliseconds_since_epoch(time)); maybe_offset.has_value()) { - if (auto name = Unicode::time_zone_display_name(Unicode::default_locale(), tz_name, maybe_offset->in_dst, time); name.has_value()) - tz_name = name.release_value(); - } else { - tz_name = Unicode::current_time_zone(); - } + 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); diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/TimeZonePrototype.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/TimeZonePrototype.cpp index ae4ba2592bb..a0705d0c532 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/TimeZonePrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/TimeZonePrototype.cpp @@ -73,7 +73,8 @@ JS_DEFINE_NATIVE_FUNCTION(TimeZonePrototype::get_offset_nanoseconds_for) return Value(*time_zone->offset_nanoseconds()); // 5. Return 𝔽(GetNamedTimeZoneOffsetNanoseconds(timeZone.[[Identifier]], instant.[[Nanoseconds]])). - return Value((double)get_named_time_zone_offset_nanoseconds(time_zone->identifier(), instant->nanoseconds().big_integer())); + auto offset = get_named_time_zone_offset_nanoseconds(time_zone->identifier(), instant->nanoseconds().big_integer()); + return Value(static_cast(offset.offset.to_nanoseconds())); } // 11.4.5 Temporal.TimeZone.prototype.getOffsetStringFor ( instant ), https://tc39.es/proposal-temporal/#sec-temporal.timezone.prototype.getoffsetstringfor diff --git a/Userland/Libraries/LibJS/Tests/builtins/Temporal/TimeZone/TimeZone.prototype.getOffsetNanosecondsFor.js b/Userland/Libraries/LibJS/Tests/builtins/Temporal/TimeZone/TimeZone.prototype.getOffsetNanosecondsFor.js index 6b93c128ff7..6c76ea952db 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Temporal/TimeZone/TimeZone.prototype.getOffsetNanosecondsFor.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Temporal/TimeZone/TimeZone.prototype.getOffsetNanosecondsFor.js @@ -4,7 +4,7 @@ describe("correct behavior", () => { }); test("basic functionality", () => { - // Adapted from TestTimeZone.cpp's TEST_CASE(get_time_zone_offset). + // Adapted from TestTimeZone.cpp's TEST_CASE(time_zone_offset). function offset(sign, hours, minutes, seconds) { return sign * (hours * 3600 + minutes * 60 + seconds) * 1_000_000_000; @@ -16,19 +16,19 @@ describe("correct behavior", () => { expect(actualOffset).toBe(expectedOffset); } - testOffset("America/Chicago", -2717647201, offset(-1, 5, 50, 36)); // Sunday, November 18, 1883 5:59:59 PM - testOffset("America/Chicago", -2717647200, offset(-1, 6, 0, 0)); // Sunday, November 18, 1883 6:00:00 PM - testOffset("America/Chicago", -1067810460, offset(-1, 6, 0, 0)); // Sunday, March 1, 1936 1:59:00 AM - testOffset("America/Chicago", -1067810400, offset(-1, 5, 0, 0)); // Sunday, March 1, 1936 2:00:00 AM - testOffset("America/Chicago", -1045432860, offset(-1, 5, 0, 0)); // Sunday, November 15, 1936 1:59:00 AM - testOffset("America/Chicago", -1045432800, offset(-1, 6, 0, 0)); // Sunday, November 15, 1936 2:00:00 AM + testOffset("America/Chicago", -2717647201, offset(-1, 5, 50, 36)); // November 18, 1883 5:59:59 PM UTC + testOffset("America/Chicago", -2717647200, offset(-1, 6, 0, 0)); // November 18, 1883 6:00:00 PM UTC + testOffset("America/Chicago", -1067788860, offset(-1, 6, 0, 0)); // March 1, 1936 1:59:00 AM Chicago (March 1, 1936 7:59:00 AM UTC) + testOffset("America/Chicago", -1067788800, offset(-1, 5, 0, 0)); // March 1, 1936 3:00:00 AM Chicago (March 1, 1936 8:00:00 AM UTC) + testOffset("America/Chicago", -1045414860, offset(-1, 5, 0, 0)); // November 15, 1936 1:59:00 AM Chicago (November 15, 1936 6:59:00 AM UTC) + testOffset("America/Chicago", -1045411200, offset(-1, 6, 0, 0)); // November 15, 1936 2:00:00 AM Chicago (November 15, 1936 8:00:00 AM UTC) - testOffset("Europe/London", -3852662401, offset(-1, 0, 1, 15)); // Tuesday, November 30, 1847 11:59:59 PM - testOffset("Europe/London", -3852662400, offset(+1, 0, 0, 0)); // Wednesday, December 1, 1847 12:00:00 AM - testOffset("Europe/London", -37238401, offset(+1, 0, 0, 0)); // Saturday, October 26, 1968 11:59:59 PM - testOffset("Europe/London", -37238400, offset(+1, 1, 0, 0)); // Sunday, October 27, 1968 12:00:00 AM - testOffset("Europe/London", 57722399, offset(+1, 1, 0, 0)); // Sunday, October 31, 1971 1:59:59 AM - testOffset("Europe/London", 57722400, offset(+1, 0, 0, 0)); // Sunday, October 31, 1971 2:00:00 AM + testOffset("Europe/London", -3852662326, offset(-1, 0, 1, 15)); // November 30, 1847 11:59:59 PM London (December 1, 1847 12:01:14 AM UTC) + testOffset("Europe/London", -3852662325, offset(+1, 0, 0, 0)); // December 1, 1847 12:01:15 AM London (December 1, 1847 12:01:15 AM UTC) + testOffset("Europe/London", -59004001, offset(+1, 0, 0, 0)); // February 18, 1968 1:59:59 AM London (February 18, 1968 1:59:59 AM UTC) + testOffset("Europe/London", -59004000, offset(+1, 1, 0, 0)); // February 18, 1968 3:00:00 AM London (February 18, 1968 2:00:00 AM UTC) + testOffset("Europe/London", 57722399, offset(+1, 1, 0, 0)); // October 31, 1971 1:59:59 AM UTC + testOffset("Europe/London", 57722400, offset(+1, 0, 0, 0)); // October 31, 1971 2:00:00 AM UTC testOffset("UTC", -1641846268, offset(+1, 0, 0, 0)); testOffset("UTC", 0, offset(+1, 0, 0, 0)); diff --git a/Userland/Libraries/LibUnicode/DisplayNames.cpp b/Userland/Libraries/LibUnicode/DisplayNames.cpp index f13dd10d4c9..e8111f246c5 100644 --- a/Userland/Libraries/LibUnicode/DisplayNames.cpp +++ b/Userland/Libraries/LibUnicode/DisplayNames.cpp @@ -172,14 +172,14 @@ Optional date_time_field_display_name(StringView locale, StringView fiel return icu_string_to_string(result); } -Optional time_zone_display_name(StringView locale, StringView time_zone_identifier, TimeZone::InDST in_dst, double time) +Optional time_zone_display_name(StringView locale, StringView time_zone_identifier, TimeZoneOffset::InDST in_dst, double time) { auto locale_data = LocaleData::for_locale(locale); if (!locale_data.has_value()) return {}; icu::UnicodeString time_zone_name; - auto type = in_dst == TimeZone::InDST::Yes ? UTZNM_LONG_DAYLIGHT : UTZNM_LONG_STANDARD; + auto type = in_dst == TimeZoneOffset::InDST::Yes ? UTZNM_LONG_DAYLIGHT : UTZNM_LONG_STANDARD; locale_data->time_zone_names().getDisplayName(icu_string(time_zone_identifier), type, time, time_zone_name); if (static_cast(time_zone_name.isBogus())) diff --git a/Userland/Libraries/LibUnicode/DisplayNames.h b/Userland/Libraries/LibUnicode/DisplayNames.h index e699cabf831..615cd260137 100644 --- a/Userland/Libraries/LibUnicode/DisplayNames.h +++ b/Userland/Libraries/LibUnicode/DisplayNames.h @@ -9,8 +9,8 @@ #include #include #include -#include #include +#include namespace Unicode { @@ -27,7 +27,7 @@ Optional region_display_name(StringView locale, StringView region); Optional script_display_name(StringView locale, StringView script); Optional calendar_display_name(StringView locale, StringView calendar); Optional date_time_field_display_name(StringView locale, StringView field, Style); -Optional time_zone_display_name(StringView locale, StringView time_zone_identifier, TimeZone::InDST, double time); +Optional time_zone_display_name(StringView locale, StringView time_zone_identifier, TimeZoneOffset::InDST, double time); Optional currency_display_name(StringView locale, StringView currency, Style); Optional currency_numeric_display_name(StringView locale, StringView currency); diff --git a/Userland/Libraries/LibUnicode/TimeZone.cpp b/Userland/Libraries/LibUnicode/TimeZone.cpp index 662583811f3..4941c73d1e7 100644 --- a/Userland/Libraries/LibUnicode/TimeZone.cpp +++ b/Userland/Libraries/LibUnicode/TimeZone.cpp @@ -118,4 +118,25 @@ Optional resolve_primary_time_zone(StringView time_zone) return icu_string_to_string(iana_id); } +Optional time_zone_offset(StringView time_zone, UnixDateTime time) +{ + UErrorCode status = U_ZERO_ERROR; + + auto icu_time_zone = adopt_own_if_nonnull(icu::TimeZone::createTimeZone(icu_string(time_zone))); + if (!icu_time_zone || *icu_time_zone == icu::TimeZone::getUnknown()) + return {}; + + i32 raw_offset = 0; + i32 dst_offset = 0; + + icu_time_zone->getOffset(static_cast(time.milliseconds_since_epoch()), 0, raw_offset, dst_offset, status); + if (icu_failure(status)) + return {}; + + return TimeZoneOffset { + .offset = Duration::from_milliseconds(raw_offset + dst_offset), + .in_dst = dst_offset == 0 ? TimeZoneOffset::InDST::No : TimeZoneOffset::InDST::Yes, + }; +} + } diff --git a/Userland/Libraries/LibUnicode/TimeZone.h b/Userland/Libraries/LibUnicode/TimeZone.h index 6e5b7ca4fa6..6a6b6469a39 100644 --- a/Userland/Libraries/LibUnicode/TimeZone.h +++ b/Userland/Libraries/LibUnicode/TimeZone.h @@ -6,15 +6,27 @@ #include #include +#include #include #pragma once namespace Unicode { +struct TimeZoneOffset { + enum class InDST { + No, + Yes, + }; + + Duration offset; + InDST in_dst { InDST::No }; +}; + String current_time_zone(); Vector const& available_time_zones(); Vector available_time_zones_in_region(StringView region); Optional resolve_primary_time_zone(StringView time_zone); +Optional time_zone_offset(StringView time_zone, UnixDateTime time); }