Produce JS-visible string results as UTF-16 at their source, including numeric formatting, BigInt and BigFraction formatting, URI encoding, console formatting, parser errors, regular expression errors, Intl and Temporal records, LibUnicode locale boundaries, and LibWeb bindings. Handle fractional radix formatting through the UTF-16 builder view.
307 lines
9.4 KiB
C++
307 lines
9.4 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<Utf16String>> cached_system_time_zone;
|
|
return *cached_system_time_zone;
|
|
}
|
|
|
|
static Utf16String current_time_zone_impl(OwnPtr<icu::TimeZone> time_zone)
|
|
{
|
|
UErrorCode status = U_ZERO_ERROR;
|
|
|
|
if (!time_zone || *time_zone == icu::TimeZone::getUnknown())
|
|
return "UTC"_utf16;
|
|
|
|
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"_utf16;
|
|
|
|
return icu_string_to_utf16_string(time_zone_name);
|
|
}
|
|
|
|
static Utf16String current_host_time_zone()
|
|
{
|
|
return current_time_zone_impl(adopt_own_if_nonnull(icu::TimeZone::detectHostTimeZone()));
|
|
}
|
|
|
|
static Utf16String current_default_time_zone()
|
|
{
|
|
return current_time_zone_impl(adopt_own_if_nonnull(icu::TimeZone::createDefault()));
|
|
}
|
|
|
|
Utf16String 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(Utf16View 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<Utf16String> icu_available_time_zones(Optional<StringView> region)
|
|
{
|
|
UErrorCode status = U_ZERO_ERROR;
|
|
|
|
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());
|
|
}
|
|
|
|
auto time_zone_enumerator = adopt_own_if_nonnull(icu::TimeZone::createTimeZoneIDEnumeration(UCAL_ZONE_TYPE_ANY, icu_region, nullptr, status));
|
|
if (icu_failure(status))
|
|
return { Utf16String::from_ascii_without_validation("UTC"sv.bytes()) };
|
|
|
|
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()));
|
|
}
|
|
|
|
quick_sort(time_zones);
|
|
return time_zones;
|
|
}
|
|
|
|
Vector<Utf16String> const& available_time_zones()
|
|
{
|
|
static NeverDestroyed<Vector<Utf16String>> time_zones { icu_available_time_zones({}) };
|
|
return *time_zones;
|
|
}
|
|
|
|
Vector<Utf16String> available_time_zones_in_region(Utf16View region)
|
|
{
|
|
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() });
|
|
}
|
|
|
|
Optional<Utf16String> 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_utf16_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 {};
|
|
}
|
|
|
|
}
|