ladybird/Libraries/LibUnicode/TimeZone.cpp
Andreas Kling 164ed80244 Meta: Enable exit-time destructor warnings for libraries
Enable -Wexit-time-destructors for all in-tree library targets and
update process-lifetime library statics so they no longer register
exit-time destructors. Long-lived caches, lookup tables, singleton
registries, and generated constants now use NeverDestroyed or leaked
references where the data is intended to live until process exit.

Update LibWeb, LibLine, and the binding generators so regenerated
sources follow the same rule instead of reintroducing destructed
statics.
2026-06-04 19:20:49 +02:00

285 lines
8.5 KiB
C++

/*
* Copyright (c) 2024-2026, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Array.h>
#include <AK/NeverDestroyed.h>
#include <AK/NonnullOwnPtr.h>
#include <AK/QuickSort.h>
#include <LibUnicode/ICU.h>
#include <LibUnicode/TimeZone.h>
#include <unicode/basictz.h>
#include <unicode/timezone.h>
#include <unicode/ucal.h>
namespace Unicode {
static auto& cached_system_time_zone()
{
static NeverDestroyed<Optional<String>> cached_system_time_zone;
return *cached_system_time_zone;
}
static String current_time_zone_impl(OwnPtr<icu::TimeZone> time_zone)
{
UErrorCode status = U_ZERO_ERROR;
if (!time_zone || *time_zone == icu::TimeZone::getUnknown())
return "UTC"_string;
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))
return "UTC"_string;
return icu_string_to_string(time_zone_name);
}
static String current_host_time_zone()
{
return current_time_zone_impl(adopt_own_if_nonnull(icu::TimeZone::detectHostTimeZone()));
}
static String current_default_time_zone()
{
return current_time_zone_impl(adopt_own_if_nonnull(icu::TimeZone::createDefault()));
}
String current_time_zone()
{
return cached_system_time_zone().ensure([] { return current_host_time_zone(); });
}
void clear_system_time_zone_cache()
{
cached_system_time_zone().clear();
}
ErrorOr<void> set_current_time_zone(StringView time_zone)
{
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());
cached_system_time_zone() = current_default_time_zone();
return {};
}
// 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);
}
static Vector<String> icu_available_time_zones(Optional<ByteString> const& region)
{
UErrorCode status = U_ZERO_ERROR;
char const* icu_region = region.has_value() ? region->characters() : nullptr;
auto time_zone_enumerator = adopt_own_if_nonnull(icu::TimeZone::createTimeZoneIDEnumeration(UCAL_ZONE_TYPE_ANY, icu_region, nullptr, status));
if (icu_failure(status))
return { "UTC"_string };
auto time_zones = icu_string_enumeration_to_list(move(time_zone_enumerator), nullptr, [](char const* zone, size_t zone_length) {
return !is_legacy_non_iana_time_zone({ zone, zone_length });
});
quick_sort(time_zones);
return time_zones;
}
Vector<String> const& available_time_zones()
{
static NeverDestroyed<Vector<String>> time_zones { icu_available_time_zones({}) };
return *time_zones;
}
Vector<String> available_time_zones_in_region(StringView region)
{
return icu_available_time_zones(region);
}
Optional<String> resolve_primary_time_zone(StringView time_zone)
{
UErrorCode status = U_ZERO_ERROR;
icu::UnicodeString iana_id;
icu::TimeZone::getIanaID(icu_string(time_zone), iana_id, status);
if (icu_failure(status))
return {};
return icu_string_to_string(iana_id);
}
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);
}
Optional<TimeZoneOffset> time_zone_offset(StringView time_zone, UnixDateTime time)
{
UErrorCode status = U_ZERO_ERROR;
auto time_zone_data = TimeZoneData::for_time_zone(time_zone);
if (!time_zone_data.has_value())
return {};
i32 raw_offset = 0;
i32 dst_offset = 0;
auto icu_time = to_icu_time(time);
time_zone_data->time_zone().getOffset(icu_time, 0, 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,
};
}
Vector<TimeZoneOffset> disambiguated_time_zone_offsets(StringView time_zone, UnixDateTime time)
{
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;
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()) {
offsets.append(*former);
}
return offsets;
}
Optional<TimeZoneTransition> get_time_zone_transition(StringView time_zone, UnixDateTime time, TimeZoneTransition::Options options)
{
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 {};
}
}