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
This commit is contained in:
Timothy Flynn 2024-06-25 15:27:20 -04:00 committed by Andreas Kling
parent 1b2d47e6bb
commit 672a555f98
15 changed files with 155 additions and 51 deletions

View File

@ -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 <regex.h> and contains many Serenity extensions
# It is therefore not reasonable to run it on Lagom, and we only run the Regex test

View File

@ -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)

View File

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

View File

@ -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)

View File

@ -11,7 +11,6 @@
#include <AK/StringBuilder.h>
#include <AK/Time.h>
#include <LibCore/DateTime.h>
#include <LibTimeZone/TimeZone.h>
#include <LibUnicode/TimeZone.h>
#include <errno.h>
#include <time.h>
@ -44,8 +43,8 @@ static Optional<StringView> 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()

View File

@ -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)

View File

@ -13,8 +13,6 @@
#include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/Intl/AbstractOperations.h>
#include <LibJS/Runtime/Temporal/ISO8601.h>
#include <LibTimeZone/TimeZone.h>
#include <LibUnicode/TimeZone.h>
#include <time.h>
namespace JS {
@ -390,31 +388,27 @@ Vector<Crypto::SignedBigInteger> 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<double>(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<double>(offset.offset.to_nanoseconds());
}
// 4. Let offsetMs be truncate(offsetNs / 10^6).

View File

@ -9,6 +9,7 @@
#include <LibCrypto/BigInt/SignedBigInteger.h>
#include <LibJS/Runtime/Object.h>
#include <LibUnicode/TimeZone.h>
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<Crypto::SignedBigInteger> 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);

View File

@ -24,7 +24,6 @@
#include <LibJS/Runtime/Temporal/Instant.h>
#include <LibJS/Runtime/Value.h>
#include <LibJS/Runtime/ValueInlines.h>
#include <LibTimeZone/TimeZone.h>
#include <LibUnicode/DisplayNames.h>
#include <LibUnicode/Locale.h>
#include <LibUnicode/TimeZone.h>
@ -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<double>(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);

View File

@ -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<double>(offset.offset.to_nanoseconds()));
}
// 11.4.5 Temporal.TimeZone.prototype.getOffsetStringFor ( instant ), https://tc39.es/proposal-temporal/#sec-temporal.timezone.prototype.getoffsetstringfor

View File

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

View File

@ -172,14 +172,14 @@ Optional<String> date_time_field_display_name(StringView locale, StringView fiel
return icu_string_to_string(result);
}
Optional<String> time_zone_display_name(StringView locale, StringView time_zone_identifier, TimeZone::InDST in_dst, double time)
Optional<String> 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<bool>(time_zone_name.isBogus()))

View File

@ -9,8 +9,8 @@
#include <AK/Optional.h>
#include <AK/String.h>
#include <AK/StringView.h>
#include <LibTimeZone/TimeZone.h>
#include <LibUnicode/Locale.h>
#include <LibUnicode/TimeZone.h>
namespace Unicode {
@ -27,7 +27,7 @@ Optional<String> region_display_name(StringView locale, StringView region);
Optional<String> script_display_name(StringView locale, StringView script);
Optional<String> calendar_display_name(StringView locale, StringView calendar);
Optional<String> date_time_field_display_name(StringView locale, StringView field, Style);
Optional<String> time_zone_display_name(StringView locale, StringView time_zone_identifier, TimeZone::InDST, double time);
Optional<String> time_zone_display_name(StringView locale, StringView time_zone_identifier, TimeZoneOffset::InDST, double time);
Optional<String> currency_display_name(StringView locale, StringView currency, Style);
Optional<String> currency_numeric_display_name(StringView locale, StringView currency);

View File

@ -118,4 +118,25 @@ Optional<String> resolve_primary_time_zone(StringView time_zone)
return icu_string_to_string(iana_id);
}
Optional<TimeZoneOffset> 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<UDate>(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,
};
}
}

View File

@ -6,15 +6,27 @@
#include <AK/Optional.h>
#include <AK/String.h>
#include <AK/Time.h>
#include <AK/Vector.h>
#pragma once
namespace Unicode {
struct TimeZoneOffset {
enum class InDST {
No,
Yes,
};
Duration offset;
InDST in_dst { InDST::No };
};
String current_time_zone();
Vector<String> const& available_time_zones();
Vector<String> available_time_zones_in_region(StringView region);
Optional<String> resolve_primary_time_zone(StringView time_zone);
Optional<TimeZoneOffset> time_zone_offset(StringView time_zone, UnixDateTime time);
}