2024-06-24 11:17:32 -03:00
|
|
|
/*
|
2026-03-08 16:56:24 -03:00
|
|
|
* Copyright (c) 2024-2026, Tim Flynn <trflynn89@ladybird.org>
|
2024-06-24 11:17:32 -03:00
|
|
|
*
|
|
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
|
*/
|
|
|
|
|
|
2024-06-25 12:06:08 -03:00
|
|
|
#include <AK/Array.h>
|
2026-06-04 05:40:32 -03:00
|
|
|
#include <AK/NeverDestroyed.h>
|
2024-06-24 11:17:32 -03:00
|
|
|
#include <AK/NonnullOwnPtr.h>
|
2024-06-25 12:06:08 -03:00
|
|
|
#include <AK/QuickSort.h>
|
2024-06-24 11:17:32 -03:00
|
|
|
#include <LibUnicode/ICU.h>
|
|
|
|
|
#include <LibUnicode/TimeZone.h>
|
|
|
|
|
|
2025-06-02 12:04:29 -03:00
|
|
|
#include <unicode/basictz.h>
|
2024-06-24 11:17:32 -03:00
|
|
|
#include <unicode/timezone.h>
|
2024-06-25 12:06:08 -03:00
|
|
|
#include <unicode/ucal.h>
|
2024-06-24 11:17:32 -03:00
|
|
|
|
|
|
|
|
namespace Unicode {
|
|
|
|
|
|
2026-06-04 05:40:32 -03:00
|
|
|
static auto& cached_system_time_zone()
|
|
|
|
|
{
|
2026-06-21 14:03:10 -03:00
|
|
|
static NeverDestroyed<Optional<Utf16String>> cached_system_time_zone;
|
2026-06-04 05:40:32 -03:00
|
|
|
return *cached_system_time_zone;
|
|
|
|
|
}
|
2024-08-24 13:07:24 -03:00
|
|
|
|
2026-06-21 14:03:10 -03:00
|
|
|
static Utf16String current_time_zone_impl(OwnPtr<icu::TimeZone> time_zone)
|
2024-06-24 11:17:32 -03:00
|
|
|
{
|
|
|
|
|
UErrorCode status = U_ZERO_ERROR;
|
|
|
|
|
|
2025-06-01 09:22:19 -03:00
|
|
|
if (!time_zone || *time_zone == icu::TimeZone::getUnknown())
|
2026-06-21 14:03:10 -03:00
|
|
|
return "UTC"_utf16;
|
2024-06-24 11:17:32 -03:00
|
|
|
|
|
|
|
|
icu::UnicodeString time_zone_id;
|
|
|
|
|
time_zone->getID(time_zone_id);
|
|
|
|
|
|
|
|
|
|
icu::UnicodeString time_zone_name;
|
|
|
|
|
time_zone->getCanonicalID(time_zone_id, time_zone_name, status);
|
|
|
|
|
|
|
|
|
|
if (icu_failure(status))
|
2026-06-21 14:03:10 -03:00
|
|
|
return "UTC"_utf16;
|
2024-06-24 11:17:32 -03:00
|
|
|
|
2026-06-21 14:03:10 -03:00
|
|
|
return icu_string_to_utf16_string(time_zone_name);
|
2025-09-16 23:26:59 -03:00
|
|
|
}
|
|
|
|
|
|
2026-06-21 14:03:10 -03:00
|
|
|
static Utf16String current_host_time_zone()
|
2025-09-16 23:26:59 -03:00
|
|
|
{
|
|
|
|
|
return current_time_zone_impl(adopt_own_if_nonnull(icu::TimeZone::detectHostTimeZone()));
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 14:03:10 -03:00
|
|
|
static Utf16String current_default_time_zone()
|
2025-09-16 23:26:59 -03:00
|
|
|
{
|
|
|
|
|
return current_time_zone_impl(adopt_own_if_nonnull(icu::TimeZone::createDefault()));
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 14:03:10 -03:00
|
|
|
Utf16String current_time_zone()
|
2025-09-16 23:26:59 -03:00
|
|
|
{
|
2026-06-04 05:40:32 -03:00
|
|
|
return cached_system_time_zone().ensure([] { return current_host_time_zone(); });
|
2024-08-24 13:07:24 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void clear_system_time_zone_cache()
|
|
|
|
|
{
|
2026-06-04 05:40:32 -03:00
|
|
|
cached_system_time_zone().clear();
|
2024-06-24 11:17:32 -03:00
|
|
|
}
|
|
|
|
|
|
2026-06-21 14:03:10 -03:00
|
|
|
ErrorOr<void> set_current_time_zone(Utf16View time_zone)
|
2025-06-01 09:22:19 -03:00
|
|
|
{
|
|
|
|
|
auto time_zone_data = TimeZoneData::for_time_zone(time_zone);
|
|
|
|
|
if (!time_zone_data.has_value())
|
|
|
|
|
return Error::from_string_literal("Unable to find the provided time zone");
|
|
|
|
|
|
|
|
|
|
icu::TimeZone::setDefault(time_zone_data->time_zone());
|
2026-06-04 05:40:32 -03:00
|
|
|
cached_system_time_zone() = current_default_time_zone();
|
2025-06-01 09:22:19 -03:00
|
|
|
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-25 12:06:08 -03:00
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-21 14:03:10 -03:00
|
|
|
static Vector<Utf16String> icu_available_time_zones(Optional<StringView> region)
|
2024-06-25 12:06:08 -03:00
|
|
|
{
|
2024-06-25 12:33:26 -03:00
|
|
|
UErrorCode status = U_ZERO_ERROR;
|
|
|
|
|
|
2026-06-21 14:03:10 -03:00
|
|
|
Array<u8, 4> region_buffer {};
|
|
|
|
|
char const* icu_region = nullptr;
|
|
|
|
|
if (region.has_value()) {
|
|
|
|
|
VERIFY(region->length() < region_buffer.size());
|
|
|
|
|
region->bytes().copy_to(region_buffer.span());
|
|
|
|
|
icu_region = reinterpret_cast<char const*>(region_buffer.data());
|
|
|
|
|
}
|
2024-06-25 12:06:08 -03:00
|
|
|
|
2024-06-25 12:33:26 -03:00
|
|
|
auto time_zone_enumerator = adopt_own_if_nonnull(icu::TimeZone::createTimeZoneIDEnumeration(UCAL_ZONE_TYPE_ANY, icu_region, nullptr, status));
|
|
|
|
|
if (icu_failure(status))
|
2026-06-21 14:03:10 -03:00
|
|
|
return { Utf16String::from_ascii_without_validation("UTC"sv.bytes()) };
|
2024-06-25 12:06:08 -03:00
|
|
|
|
2026-06-21 14:03:10 -03:00
|
|
|
Vector<Utf16String> time_zones;
|
|
|
|
|
while (true) {
|
|
|
|
|
i32 length = 0;
|
|
|
|
|
auto const* time_zone = time_zone_enumerator->next(&length, status);
|
|
|
|
|
|
|
|
|
|
if (icu_failure(status) || time_zone == nullptr)
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
StringView time_zone_view { time_zone, static_cast<size_t>(length) };
|
|
|
|
|
if (!is_legacy_non_iana_time_zone(time_zone_view))
|
|
|
|
|
time_zones.append(Utf16String::from_ascii_without_validation(time_zone_view.bytes()));
|
|
|
|
|
}
|
2024-06-25 12:06:08 -03:00
|
|
|
|
2024-06-25 12:33:26 -03:00
|
|
|
quick_sort(time_zones);
|
|
|
|
|
return time_zones;
|
|
|
|
|
}
|
2024-06-25 12:06:08 -03:00
|
|
|
|
2026-06-21 14:03:10 -03:00
|
|
|
Vector<Utf16String> const& available_time_zones()
|
2024-06-25 12:33:26 -03:00
|
|
|
{
|
2026-06-21 14:03:10 -03:00
|
|
|
static NeverDestroyed<Vector<Utf16String>> time_zones { icu_available_time_zones({}) };
|
2026-06-04 05:40:32 -03:00
|
|
|
return *time_zones;
|
2024-06-25 12:06:08 -03:00
|
|
|
}
|
|
|
|
|
|
2026-06-21 14:03:10 -03:00
|
|
|
Vector<Utf16String> available_time_zones_in_region(Utf16View region)
|
2024-06-25 12:33:26 -03:00
|
|
|
{
|
2026-06-21 14:03:10 -03:00
|
|
|
VERIFY(region.length_in_code_units() < 4);
|
|
|
|
|
Array<char, 4> region_buffer {};
|
|
|
|
|
for (auto i = 0uz; i < region.length_in_code_units(); ++i) {
|
|
|
|
|
auto code_unit = region.code_unit_at(i);
|
|
|
|
|
VERIFY(code_unit <= 0x7f);
|
|
|
|
|
region_buffer[i] = static_cast<char>(code_unit);
|
|
|
|
|
}
|
|
|
|
|
return icu_available_time_zones(StringView { region_buffer.data(), region.length_in_code_units() });
|
2024-06-25 12:33:26 -03:00
|
|
|
}
|
|
|
|
|
|
Libraries: Clean up UTF-16 source text paths
Store parser errors, source range filenames, source code filenames,
module source, and Rust parser errors as UTF-16 where they flow back
into JavaScript-visible strings. Keep byte-oriented source buffers
byte-backed.
Remove temporary PrimitiveString, ByteString, and UTF-8 detours from
JSON, RegExp, module debug logging, print formatting, and tests.
2026-06-21 14:03:19 -03:00
|
|
|
Optional<Utf16String> resolve_primary_time_zone(Utf16View time_zone)
|
2024-06-25 12:06:08 -03:00
|
|
|
{
|
|
|
|
|
UErrorCode status = U_ZERO_ERROR;
|
|
|
|
|
|
|
|
|
|
icu::UnicodeString iana_id;
|
|
|
|
|
icu::TimeZone::getIanaID(icu_string(time_zone), iana_id, status);
|
|
|
|
|
|
|
|
|
|
if (icu_failure(status))
|
|
|
|
|
return {};
|
|
|
|
|
|
2026-06-21 14:03:10 -03:00
|
|
|
return icu_string_to_utf16_string(iana_id);
|
2024-06-25 12:06:08 -03:00
|
|
|
}
|
|
|
|
|
|
2025-06-02 12:04:29 -03:00
|
|
|
static UDate to_icu_time(UnixDateTime time)
|
|
|
|
|
{
|
|
|
|
|
// We must clamp the time we provide to ICU such that the result of converting milliseconds to days fits in an i32.
|
|
|
|
|
// Further, that conversion must still be valid after applying DST offsets to the time we provide.
|
|
|
|
|
static constexpr auto min_time = (static_cast<UDate>(AK::NumericLimits<i32>::min()) + U_MILLIS_PER_DAY) * U_MILLIS_PER_DAY;
|
|
|
|
|
static constexpr auto max_time = (static_cast<UDate>(AK::NumericLimits<i32>::max()) - U_MILLIS_PER_DAY) * U_MILLIS_PER_DAY;
|
|
|
|
|
return clamp(static_cast<UDate>(time.milliseconds_since_epoch()), min_time, max_time);
|
|
|
|
|
}
|
|
|
|
|
|
Libraries: Clean up UTF-16 source text paths
Store parser errors, source range filenames, source code filenames,
module source, and Rust parser errors as UTF-16 where they flow back
into JavaScript-visible strings. Keep byte-oriented source buffers
byte-backed.
Remove temporary PrimitiveString, ByteString, and UTF-8 detours from
JSON, RegExp, module debug logging, print formatting, and tests.
2026-06-21 14:03:19 -03:00
|
|
|
Optional<TimeZoneOffset> time_zone_offset(Utf16View time_zone, UnixDateTime time)
|
2024-06-25 16:27:20 -03:00
|
|
|
{
|
|
|
|
|
UErrorCode status = U_ZERO_ERROR;
|
|
|
|
|
|
2024-09-03 12:20:20 -03:00
|
|
|
auto time_zone_data = TimeZoneData::for_time_zone(time_zone);
|
|
|
|
|
if (!time_zone_data.has_value())
|
2024-06-25 16:27:20 -03:00
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
i32 raw_offset = 0;
|
|
|
|
|
i32 dst_offset = 0;
|
|
|
|
|
|
2025-06-02 12:04:29 -03:00
|
|
|
auto icu_time = to_icu_time(time);
|
2025-01-18 15:29:28 -03:00
|
|
|
|
|
|
|
|
time_zone_data->time_zone().getOffset(icu_time, 0, raw_offset, dst_offset, status);
|
2024-06-25 16:27:20 -03:00
|
|
|
if (icu_failure(status))
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
return TimeZoneOffset {
|
2024-07-17 02:45:18 -03:00
|
|
|
.offset = AK::Duration::from_milliseconds(raw_offset + dst_offset),
|
2024-06-25 16:27:20 -03:00
|
|
|
.in_dst = dst_offset == 0 ? TimeZoneOffset::InDST::No : TimeZoneOffset::InDST::Yes,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
Libraries: Clean up UTF-16 source text paths
Store parser errors, source range filenames, source code filenames,
module source, and Rust parser errors as UTF-16 where they flow back
into JavaScript-visible strings. Keep byte-oriented source buffers
byte-backed.
Remove temporary PrimitiveString, ByteString, and UTF-8 detours from
JSON, RegExp, module debug logging, print formatting, and tests.
2026-06-21 14:03:19 -03:00
|
|
|
Vector<TimeZoneOffset> disambiguated_time_zone_offsets(Utf16View time_zone, UnixDateTime time)
|
2025-06-02 12:04:29 -03:00
|
|
|
{
|
|
|
|
|
UErrorCode status = U_ZERO_ERROR;
|
|
|
|
|
|
|
|
|
|
auto time_zone_data = TimeZoneData::for_time_zone(time_zone);
|
|
|
|
|
if (!time_zone_data.has_value())
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
auto& basic_time_zone = as<icu::BasicTimeZone>(time_zone_data->time_zone());
|
|
|
|
|
auto icu_time = to_icu_time(time);
|
|
|
|
|
|
|
|
|
|
auto get_offset = [&](auto disambiguation_option) -> Optional<TimeZoneOffset> {
|
|
|
|
|
i32 raw_offset = 0;
|
|
|
|
|
i32 dst_offset = 0;
|
|
|
|
|
|
|
|
|
|
basic_time_zone.getOffsetFromLocal(icu_time, disambiguation_option, disambiguation_option, raw_offset, dst_offset, status);
|
|
|
|
|
if (icu_failure(status))
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
return TimeZoneOffset {
|
|
|
|
|
.offset = AK::Duration::from_milliseconds(raw_offset + dst_offset),
|
|
|
|
|
.in_dst = dst_offset == 0 ? TimeZoneOffset::InDST::No : TimeZoneOffset::InDST::Yes,
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto former = get_offset(UCAL_TZ_LOCAL_FORMER);
|
|
|
|
|
auto latter = get_offset(UCAL_TZ_LOCAL_LATTER);
|
|
|
|
|
|
|
|
|
|
Vector<TimeZoneOffset> offsets;
|
2026-03-08 16:56:24 -03:00
|
|
|
|
|
|
|
|
if (former.has_value() && latter.has_value()) {
|
|
|
|
|
if (former->offset == latter->offset) {
|
|
|
|
|
offsets.append(*former);
|
|
|
|
|
} else if (former->offset > latter->offset) {
|
|
|
|
|
offsets.append(*former);
|
|
|
|
|
offsets.append(*latter);
|
|
|
|
|
}
|
|
|
|
|
} else if (former.has_value()) {
|
2025-06-02 12:04:29 -03:00
|
|
|
offsets.append(*former);
|
2026-03-08 16:56:24 -03:00
|
|
|
}
|
2025-06-02 12:04:29 -03:00
|
|
|
|
|
|
|
|
return offsets;
|
|
|
|
|
}
|
|
|
|
|
|
Libraries: Clean up UTF-16 source text paths
Store parser errors, source range filenames, source code filenames,
module source, and Rust parser errors as UTF-16 where they flow back
into JavaScript-visible strings. Keep byte-oriented source buffers
byte-backed.
Remove temporary PrimitiveString, ByteString, and UTF-8 detours from
JSON, RegExp, module debug logging, print formatting, and tests.
2026-06-21 14:03:19 -03:00
|
|
|
Optional<TimeZoneTransition> get_time_zone_transition(Utf16View time_zone, UnixDateTime time, TimeZoneTransition::Options options)
|
2026-01-14 10:56:42 -03:00
|
|
|
{
|
|
|
|
|
auto time_zone_data = TimeZoneData::for_time_zone(time_zone);
|
|
|
|
|
if (!time_zone_data.has_value())
|
|
|
|
|
return OptionalNone {};
|
|
|
|
|
|
|
|
|
|
auto& basic_time_zone = as<icu::BasicTimeZone>(time_zone_data->time_zone());
|
|
|
|
|
|
|
|
|
|
auto current_icu_time = to_icu_time(time);
|
|
|
|
|
bool include_current_time = options.include_given_time == TimeZoneTransition::Options::IncludeGivenTime::Yes;
|
|
|
|
|
|
|
|
|
|
icu::TimeZoneTransition result;
|
|
|
|
|
auto found_transition = [&] {
|
|
|
|
|
if (options.direction == TimeZoneTransition::Options::Direction::Previous)
|
|
|
|
|
return basic_time_zone.getPreviousTransition(current_icu_time, include_current_time, result);
|
|
|
|
|
|
|
|
|
|
return basic_time_zone.getNextTransition(current_icu_time, include_current_time, result);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
while (found_transition()) {
|
|
|
|
|
auto time_result = result.getTime();
|
|
|
|
|
|
|
|
|
|
switch (options.transition_rule) {
|
|
|
|
|
case TimeZoneTransition::Options::TransitionRule::AnyTransition: {
|
|
|
|
|
return TimeZoneTransition {
|
|
|
|
|
.transition = AK::Duration::from_milliseconds(time_result),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
case TimeZoneTransition::Options::TransitionRule::TransitionWhereUTCOffsetChanges: {
|
|
|
|
|
auto const* from_rule = result.getFrom();
|
|
|
|
|
auto const* to_rule = result.getTo();
|
|
|
|
|
|
|
|
|
|
i32 from_utc_offset = from_rule->getRawOffset() + from_rule->getDSTSavings();
|
|
|
|
|
i32 to_utc_offset = to_rule->getRawOffset() + to_rule->getDSTSavings();
|
|
|
|
|
|
|
|
|
|
if (from_utc_offset != to_utc_offset) {
|
|
|
|
|
return TimeZoneTransition {
|
|
|
|
|
.transition = AK::Duration::from_milliseconds(time_result),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
current_icu_time = time_result;
|
|
|
|
|
include_current_time = false;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return OptionalNone {};
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-24 11:17:32 -03:00
|
|
|
}
|