| 
									
										
										
										
											2024-06-24 10:17:32 -04:00
										 |  |  | /*
 | 
					
						
							| 
									
										
										
										
											2025-06-01 08:22:19 -04:00
										 |  |  |  * Copyright (c) 2024-2025, Tim Flynn <trflynn89@ladybird.org> | 
					
						
							| 
									
										
										
										
											2024-06-24 10:17:32 -04:00
										 |  |  |  * | 
					
						
							|  |  |  |  * SPDX-License-Identifier: BSD-2-Clause | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-25 11:06:08 -04:00
										 |  |  | #include <AK/Array.h>
 | 
					
						
							| 
									
										
										
										
											2024-06-24 10:17:32 -04:00
										 |  |  | #include <AK/NonnullOwnPtr.h>
 | 
					
						
							| 
									
										
										
										
											2024-06-25 11:06:08 -04:00
										 |  |  | #include <AK/QuickSort.h>
 | 
					
						
							| 
									
										
										
										
											2024-06-24 10:17:32 -04:00
										 |  |  | #include <LibUnicode/ICU.h>
 | 
					
						
							|  |  |  | #include <LibUnicode/TimeZone.h>
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-02 11:04:29 -04:00
										 |  |  | #include <unicode/basictz.h>
 | 
					
						
							| 
									
										
										
										
											2024-06-24 10:17:32 -04:00
										 |  |  | #include <unicode/timezone.h>
 | 
					
						
							| 
									
										
										
										
											2024-06-25 11:06:08 -04:00
										 |  |  | #include <unicode/ucal.h>
 | 
					
						
							| 
									
										
										
										
											2024-06-24 10:17:32 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | namespace Unicode { | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-24 12:07:24 -04:00
										 |  |  | static Optional<String> cached_system_time_zone; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-16 19:26:59 -07:00
										 |  |  | static String current_time_zone_impl(OwnPtr<icu::TimeZone> time_zone) | 
					
						
							| 
									
										
										
										
											2024-06-24 10:17:32 -04:00
										 |  |  | { | 
					
						
							|  |  |  |     UErrorCode status = U_ZERO_ERROR; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-01 08:22:19 -04:00
										 |  |  |     if (!time_zone || *time_zone == icu::TimeZone::getUnknown()) | 
					
						
							| 
									
										
										
										
											2024-06-24 10:17:32 -04:00
										 |  |  |         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; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-16 19:26:59 -07:00
										 |  |  |     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(); }); | 
					
						
							| 
									
										
										
										
											2024-08-24 12:07:24 -04:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | void clear_system_time_zone_cache() | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     cached_system_time_zone.clear(); | 
					
						
							| 
									
										
										
										
											2024-06-24 10:17:32 -04:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-01 08:22:19 -04:00
										 |  |  | 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()); | 
					
						
							| 
									
										
										
										
											2025-09-16 19:26:59 -07:00
										 |  |  |     cached_system_time_zone = current_default_time_zone(); | 
					
						
							| 
									
										
										
										
											2025-06-01 08:22:19 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |     return {}; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-25 11:06:08 -04: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); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-25 11:33:26 -04:00
										 |  |  | static Vector<String> icu_available_time_zones(Optional<ByteString> const& region) | 
					
						
							| 
									
										
										
										
											2024-06-25 11:06:08 -04:00
										 |  |  | { | 
					
						
							| 
									
										
										
										
											2024-06-25 11:33:26 -04:00
										 |  |  |     UErrorCode status = U_ZERO_ERROR; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     char const* icu_region = region.has_value() ? region->characters() : nullptr; | 
					
						
							| 
									
										
										
										
											2024-06-25 11:06:08 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-25 11:33:26 -04:00
										 |  |  |     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 }; | 
					
						
							| 
									
										
										
										
											2024-06-25 11:06:08 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-02 15:44:33 -04:00
										 |  |  |     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 }); | 
					
						
							| 
									
										
										
										
											2024-06-25 11:33:26 -04:00
										 |  |  |     }); | 
					
						
							| 
									
										
										
										
											2024-06-25 11:06:08 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-25 11:33:26 -04:00
										 |  |  |     quick_sort(time_zones); | 
					
						
							|  |  |  |     return time_zones; | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2024-06-25 11:06:08 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-25 11:33:26 -04:00
										 |  |  | Vector<String> const& available_time_zones() | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     static auto time_zones = icu_available_time_zones({}); | 
					
						
							| 
									
										
										
										
											2024-06-25 11:06:08 -04:00
										 |  |  |     return time_zones; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-25 11:33:26 -04:00
										 |  |  | Vector<String> available_time_zones_in_region(StringView region) | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     return icu_available_time_zones(region); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-25 11:06:08 -04:00
										 |  |  | 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); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-02 11:04:29 -04: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); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-25 15:27:20 -04:00
										 |  |  | Optional<TimeZoneOffset> time_zone_offset(StringView time_zone, UnixDateTime time) | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     UErrorCode status = U_ZERO_ERROR; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-03 11:20:20 -04:00
										 |  |  |     auto time_zone_data = TimeZoneData::for_time_zone(time_zone); | 
					
						
							|  |  |  |     if (!time_zone_data.has_value()) | 
					
						
							| 
									
										
										
										
											2024-06-25 15:27:20 -04:00
										 |  |  |         return {}; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     i32 raw_offset = 0; | 
					
						
							|  |  |  |     i32 dst_offset = 0; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-02 11:04:29 -04:00
										 |  |  |     auto icu_time = to_icu_time(time); | 
					
						
							| 
									
										
										
										
											2025-01-18 13:29:28 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     time_zone_data->time_zone().getOffset(icu_time, 0, raw_offset, dst_offset, status); | 
					
						
							| 
									
										
										
										
											2024-06-25 15:27:20 -04:00
										 |  |  |     if (icu_failure(status)) | 
					
						
							|  |  |  |         return {}; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return TimeZoneOffset { | 
					
						
							| 
									
										
										
										
											2024-07-16 23:45:18 -06:00
										 |  |  |         .offset = AK::Duration::from_milliseconds(raw_offset + dst_offset), | 
					
						
							| 
									
										
										
										
											2024-06-25 15:27:20 -04:00
										 |  |  |         .in_dst = dst_offset == 0 ? TimeZoneOffset::InDST::No : TimeZoneOffset::InDST::Yes, | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-02 11:04:29 -04:00
										 |  |  | 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()) | 
					
						
							|  |  |  |         offsets.append(*former); | 
					
						
							|  |  |  |     if (latter.has_value() && latter->offset != former->offset) | 
					
						
							|  |  |  |         offsets.append(*latter); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return offsets; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-24 10:17:32 -04:00
										 |  |  | } |