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