diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 71008d0e5..a06bfcdef 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -237,6 +237,13 @@ task("downloadWindowsZonesMapping") { out.println("\t{ \"$windowsName\", \"$usualName\" },") } out.println("};") + out.println("""static const std::unordered_map zone_ids = {""") + var i = 0 + for ((usualName, windowsName) in mapping) { + out.println("\t{ \"$usualName\", $i },") + ++i + } + out.println("};") } } } diff --git a/core/commonMain/src/DayOfWeek.kt b/core/commonMain/src/DayOfWeek.kt index 73b28036d..c14233dde 100644 --- a/core/commonMain/src/DayOfWeek.kt +++ b/core/commonMain/src/DayOfWeek.kt @@ -5,6 +5,8 @@ package kotlinx.datetime +import kotlin.native.concurrent.* + public expect enum class DayOfWeek { MONDAY, TUESDAY, @@ -17,6 +19,7 @@ public expect enum class DayOfWeek { public val DayOfWeek.number: Int get() = ordinal + 1 +@SharedImmutable private val allDaysOfWeek = DayOfWeek.values().asList() public fun DayOfWeek(number: Int): DayOfWeek { require(number in 1..7) diff --git a/core/commonMain/src/Month.kt b/core/commonMain/src/Month.kt index c4a64d50f..8ee98566c 100644 --- a/core/commonMain/src/Month.kt +++ b/core/commonMain/src/Month.kt @@ -5,6 +5,8 @@ package kotlinx.datetime +import kotlin.native.concurrent.* + public expect enum class Month { JANUARY, FEBRUARY, @@ -24,7 +26,9 @@ public expect enum class Month { public val Month.number: Int get() = ordinal + 1 +@SharedImmutable private val allMonths = Month.values().asList() + public fun Month(number: Int): Month { require(number in 1..12) return allMonths[number - 1] diff --git a/core/nativeMain/cinterop/cpp/apple.mm b/core/nativeMain/cinterop/cpp/apple.mm index cefa29de0..02858a2fe 100644 --- a/core/nativeMain/cinterop/cpp/apple.mm +++ b/core/nativeMain/cinterop/cpp/apple.mm @@ -12,6 +12,7 @@ #import #import #import +#import #import #import #include "helper_macros.hpp" @@ -29,11 +30,52 @@ extern "C" { #include "cdate.h" +} -char * get_system_timezone() +static std::vector populate() +{ + std::vector v; + auto names = NSTimeZone.knownTimeZoneNames; + for (size_t i = 0; i < names.count; ++i) { + v.push_back([NSTimeZone timeZoneWithName: names[i]]); + } + return v; +} + +static std::vector zones_cache = populate(); + +static TZID id_by_name(NSString *zone_name) +{ + auto abbreviations = NSTimeZone.abbreviationDictionary; + auto true_name = [abbreviations valueForKey: zone_name]; + const NSString *name = zone_name; + if (true_name != nil) { + name = true_name; + } + for (size_t i = 0; i < zones_cache.size(); ++i) { + if ([name isEqualToString:zones_cache[i].name]) { + return i; + } + } + return TZID_INVALID; +} + +static NSTimeZone *timezone_by_id(TZID id) +{ + try { + return zones_cache.at(id); + } catch (std::out_of_range e) { + return nullptr; + } +} + +extern "C" { + +char * get_system_timezone(TZID *tzid) { CFTimeZoneRef zone = CFTimeZoneCopySystem(); // always succeeds auto name = CFTimeZoneGetName(zone); + *tzid = id_by_name((__bridge NSString *)name); CFIndex bufferSize = CFStringGetLength(name) + 1; char * buffer = (char *)malloc(sizeof(char) * bufferSize); if (buffer == nullptr) { @@ -41,14 +83,10 @@ return nullptr; } // only fails if the name is not UTF8-encoded, which is an anomaly. - if (CFStringGetCString(name, buffer, bufferSize, kCFStringEncodingUTF8)) - { - CFRelease(zone); - return buffer; - } + auto result = CFStringGetCString(name, buffer, bufferSize, kCFStringEncodingUTF8); + assert(result); CFRelease(zone); - free(buffer); - return nullptr; + return buffer; } char ** available_zone_ids() @@ -77,25 +115,22 @@ return zones_copy; } -int offset_at_instant(const char *zone_name, int64_t epoch_sec) +int offset_at_instant(TZID zone_id, int64_t epoch_sec) { - auto zone_name_nsstring = [NSString stringWithUTF8String: zone_name]; - auto zone = zone_by_name(zone_name_nsstring); + auto zone = timezone_by_id(zone_id); + if (zone == nil) { return INT_MAX; } auto date = [NSDate dateWithTimeIntervalSince1970: epoch_sec]; return (int32_t)[zone secondsFromGMTForDate: date]; } -bool is_known_timezone(const char *zone_name) { - auto zone_name_nsstring = [NSString stringWithUTF8String: zone_name]; - return (zone_by_name(zone_name_nsstring) != nil); +TZID timezone_by_name(const char *zone_name) { + return id_by_name([NSString stringWithUTF8String: zone_name]); } -int offset_at_datetime(const char *zone_name, int64_t epoch_sec, int *offset) { +int offset_at_datetime(TZID zone_id, int64_t epoch_sec, int *offset) { *offset = INT_MAX; - // timezone name - auto zone_name_nsstring = [NSString stringWithUTF8String: zone_name]; // timezone - auto zone = zone_by_name(zone_name_nsstring); + auto zone = timezone_by_id(zone_id); if (zone == nil) { return 0; } /* a date in an unspecified timezone, defined by the number of seconds since the start of the epoch in *that* unspecified timezone */ diff --git a/core/nativeMain/cinterop/cpp/cdate.cpp b/core/nativeMain/cinterop/cpp/cdate.cpp index 1cfdd7598..25b4bad12 100644 --- a/core/nativeMain/cinterop/cpp/cdate.cpp +++ b/core/nativeMain/cinterop/cpp/cdate.cpp @@ -15,6 +15,10 @@ using namespace date; using namespace std::chrono; +extern "C" { +#include "cdate.h" +} + template static char * timezone_name(const T& zone) { @@ -26,17 +30,44 @@ static char * timezone_name(const T& zone) return name_copy; } -extern "C" { +static const time_zone *zone_by_id(TZID id) +{ + /* The `date` library provides a linked list of `tzdb` objects. `get_tzdb()` + always returns the head of that list. For now, the list never changes: + a call to `reload_tzdb()` would be required to load the updated version + of the timezone database. We never do this because for now (with use of + `date`) this operation is not even present for the configuration that + uses the system timezone database. If we move to C++20 support for this, + it may be feasible to call `reload_tzdb()` and construct a more elaborate + ID scheme. */ + auto& tzdb = get_tzdb(); + try { + return &tzdb.zones.at(id); + } catch (std::out_of_range e) { + throw std::runtime_error("Invalid timezone id"); + } +} -#include "cdate.h" +static TZID id_by_zone(const tzdb& db, const time_zone* tz) +{ + size_t id = tz - &db.zones[0]; + if (id >= db.zones.size()) { + throw std::runtime_error("The time zone is not part of the tzdb"); + } + return id; +} -char * get_system_timezone() +extern "C" { + +char * get_system_timezone(TZID * id) { try { auto& tzdb = get_tzdb(); auto zone = tzdb.current_zone(); + *id = id_by_zone(tzdb, zone); return timezone_name(*zone); } catch (std::runtime_error e) { + *id = TZID_INVALID; return nullptr; } } @@ -59,15 +90,14 @@ char ** available_zone_ids() } } -int offset_at_instant(const char *zone_name, int64_t epoch_sec) +int offset_at_instant(TZID zone_id, int64_t epoch_sec) { try { - auto& tzdb = get_tzdb(); /* `sys_time` is usually Unix time (UTC, not counting leap seconds). Starting from C++20, it is specified in the standard. */ auto stime = sys_time( std::chrono::seconds(epoch_sec)); - auto zone = tzdb.locate_zone(zone_name); + auto zone = zone_by_id(zone_id); auto info = zone->get_info(stime); return info.offset.count(); } catch (std::runtime_error e) { @@ -75,22 +105,20 @@ int offset_at_instant(const char *zone_name, int64_t epoch_sec) } } -bool is_known_timezone(const char *zone_name) +TZID timezone_by_name(const char *zone_name) { try { auto& tzdb = get_tzdb(); - tzdb.locate_zone(zone_name); - return true; + return id_by_zone(tzdb, tzdb.locate_zone(zone_name)); } catch (std::runtime_error e) { - return false; + return TZID_INVALID; } } -int offset_at_datetime(const char *zone_name, int64_t epoch_sec, int *offset) +int offset_at_datetime(TZID zone_id, int64_t epoch_sec, int *offset) { try { - auto& tzdb = get_tzdb(); - auto zone = tzdb.locate_zone(zone_name); + auto zone = zone_by_id(zone_id); local_seconds seconds((std::chrono::seconds(epoch_sec))); auto info = zone->get_info(seconds); switch (info.result) { @@ -107,6 +135,10 @@ int offset_at_datetime(const char *zone_name, int64_t epoch_sec, int *offset) if (info.second.offset.count() != *offset) *offset = info.first.offset.count(); return 0; + default: + // the pattern matching above is supposedly exhaustive + *offset = INT_MAX; + return 0; } } catch (std::runtime_error e) { *offset = INT_MAX; diff --git a/core/nativeMain/cinterop/cpp/windows.cpp b/core/nativeMain/cinterop/cpp/windows.cpp index 0bec1413b..d3b06342d 100644 --- a/core/nativeMain/cinterop/cpp/windows.cpp +++ b/core/nativeMain/cinterop/cpp/windows.cpp @@ -27,6 +27,9 @@ #include "date/date.h" #include "helper_macros.hpp" #include "windows_zones.hpp" +extern "C" { +#include "cdate.h" +} /* The maximum length of the registry key name for timezones. Taken from https://docs.microsoft.com/en-us/windows/win32/api/timezoneapi/ns-timezoneapi-dynamic_time_zone_information @@ -54,6 +57,16 @@ static std::string key_to_string(const DYNAMIC_TIME_ZONE_INFORMATION& dtzi) { return buf; } +/* Finds the unique number assigned to each standard name. */ +static TZID id_by_name(const std::string& name) +{ + try { + return zone_ids.at(name); + } catch (std::out_of_range e) { + return TZID_INVALID; + } +} + /* Returns a standard timezone name given a Windows registry key name. The returned C string is guaranteed to have static lifetime. */ static const char *native_name_to_standard_name(const std::string& native) { @@ -72,11 +85,10 @@ static const char *native_name_to_standard_name(const std::string& native) { } // The next time the timezone cache should be flushed. -static std::chrono::time_point - next_flush = std::chrono::steady_clock::now(); +static auto next_flush = std::chrono::time_point::min(); // The timezone cache. Access to it should be guarded with `cache_rwlock`. static std::unordered_map< - std::string, DYNAMIC_TIME_ZONE_INFORMATION> cache; + TZID, DYNAMIC_TIME_ZONE_INFORMATION> cache; // The read-write lock guarding access to the cache. static std::shared_mutex cache_rwlock; @@ -89,37 +101,40 @@ static void repopulate_timezone_cache( return; } cache.clear(); + std::unordered_map + native_to_zones; DYNAMIC_TIME_ZONE_INFORMATION dtzi{}; next_flush = current_time + CACHE_INVALIDATION_TIMEOUT; for (DWORD dwResult = 0, i = 0; dwResult != ERROR_NO_MORE_ITEMS; ++i) { dwResult = EnumDynamicTimeZoneInformation(i, &dtzi); if (dwResult == ERROR_SUCCESS) { - cache[key_to_string(dtzi)] = dtzi; + native_to_zones[key_to_string(dtzi)] = dtzi; } } -} - -/* Populates `dtzi` with the time zone information for Windows registry key - `native_name`. Throws `std::out_of_range` if the name is invalid. */ -static void time_zone_by_native_name( - const std::string& native_name, DYNAMIC_TIME_ZONE_INFORMATION& dtzi) -{ - const auto current_time = std::chrono::steady_clock::now(); - if (current_time > next_flush) { - repopulate_timezone_cache(current_time); + for (auto it = standard_to_windows.begin(); + it != standard_to_windows.end(); ++it) + { + try { + auto& dtzi = native_to_zones.at(it->second); + auto id = id_by_name(it->first); + cache[id] = dtzi; + } catch (std::out_of_range e) { + } } - const std::shared_lock lock(cache_rwlock); - dtzi = cache.at(native_name); } /* Populates `dtzi` with the time zone information for standard timezone name `name`. Returns `false` if the name is invalid. */ -static bool time_zone_by_name( - const char *name, DYNAMIC_TIME_ZONE_INFORMATION& dtzi) +static bool time_zone_by_id( + TZID id, DYNAMIC_TIME_ZONE_INFORMATION& dtzi) { try { - auto& native_name = standard_to_windows.at(name); - time_zone_by_native_name(native_name, dtzi); + const auto current_time = std::chrono::steady_clock::now(); + if (current_time > next_flush) { + repopulate_timezone_cache(current_time); + } + const std::shared_lock lock(cache_rwlock); + dtzi = cache.at(id); return true; } catch (std::out_of_range e) { return false; @@ -261,9 +276,7 @@ static int offset_at_systime(DYNAMIC_TIME_ZONE_INFORMATION& dtzi, extern "C" { -#include "cdate.h" - -char * get_system_timezone() +char * get_system_timezone(TZID* id) { DYNAMIC_TIME_ZONE_INFORMATION dtzi{}; auto result = GetDynamicTimeZoneInformation(&dtzi); @@ -272,8 +285,10 @@ char * get_system_timezone() auto key = key_to_string(dtzi); auto name = native_name_to_standard_name(key); if (name == nullptr) { + *id = TZID_INVALID; return nullptr; } else { + *id = id_by_name(name); return strdup(name); } } @@ -305,10 +320,10 @@ char ** available_zone_ids() return zones; } -int offset_at_instant(const char *zone_name, int64_t epoch_sec) +int offset_at_instant(TZID zone_id, int64_t epoch_sec) { DYNAMIC_TIME_ZONE_INFORMATION dtzi{}; - bool result = time_zone_by_name(zone_name, dtzi); + bool result = time_zone_by_id(zone_id, dtzi); if (!result) { return INT_MAX; } @@ -317,16 +332,21 @@ int offset_at_instant(const char *zone_name, int64_t epoch_sec) return offset_at_systime(dtzi, systime); } -bool is_known_timezone(const char *zone_name) +TZID timezone_by_name(const char *zone_name) { DYNAMIC_TIME_ZONE_INFORMATION dtzi{}; - return time_zone_by_name(zone_name, dtzi); + TZID id = id_by_name(zone_name); + if (time_zone_by_id(id, dtzi)) { + return id; + } else { + return TZID_INVALID; + } } -int offset_at_datetime(const char *zone_name, int64_t epoch_sec, int *offset) +int offset_at_datetime(TZID zone_id, int64_t epoch_sec, int *offset) { DYNAMIC_TIME_ZONE_INFORMATION dtzi{}; - bool result = time_zone_by_name(zone_name, dtzi); + bool result = time_zone_by_id(zone_id, dtzi); if (!result) { return INT_MAX; } diff --git a/core/nativeMain/cinterop/public/cdate.h b/core/nativeMain/cinterop/public/cdate.h index 5c16e1e6d..2b2137d8b 100644 --- a/core/nativeMain/cinterop/public/cdate.h +++ b/core/nativeMain/cinterop/public/cdate.h @@ -3,12 +3,17 @@ * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. */ // This file specifies the native interface for datetime information queries. +#pragma once #include #include #include -/* Returns a string that must be freed by the caller, or null. */ -char * get_system_timezone(); +typedef size_t TZID; +const size_t TZID_INVALID = SIZE_MAX; + +/* Returns a string that must be freed by the caller, or null. + If something is returned, `id` has the id of the timezone. */ +char * get_system_timezone(TZID* id); /* Returns an array of strings. The end of the array is marked with a NULL. The array and its contents must be freed by the caller. @@ -16,13 +21,14 @@ char * get_system_timezone(); char ** available_zone_ids(); // returns the offset, or INT_MAX if there's a problem with the time zone. -int offset_at_instant(const char *zone_name, int64_t epoch_sec); +int offset_at_instant(TZID zone, int64_t epoch_sec); -bool is_known_timezone(const char *zone_name); +// returns the id of the timezone or TZID_INVALID in case of an error. +TZID timezone_by_name(const char *zone_name); /* Sets the result in "offset"; in case an existing value in "offset" is an acceptable one, leaves it untouched. Returns the number of seconds that the caller needs to add to their existing estimation of date, which is needed in case the time does not exist, having fallen in the gap. In case of an error, "offset" is set to INT_MAX. */ -int offset_at_datetime(const char *zone_name, int64_t epoch_sec, int *offset); +int offset_at_datetime(TZID zone, int64_t epoch_sec, int *offset); diff --git a/core/nativeMain/cinterop/public/helper_macros.hpp b/core/nativeMain/cinterop/public/helper_macros.hpp index 8d492c723..c55a87ab6 100644 --- a/core/nativeMain/cinterop/public/helper_macros.hpp +++ b/core/nativeMain/cinterop/public/helper_macros.hpp @@ -2,6 +2,7 @@ * Copyright 2016-2020 JetBrains s.r.o. * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. */ +#pragma once /* Frees an array of allocated pointers. `array` is the array to free, and `length_var` is a variable that holds the current amount of diff --git a/core/nativeMain/cinterop/public/windows_zones.hpp b/core/nativeMain/cinterop/public/windows_zones.hpp index 74b988a85..71cb8a724 100644 --- a/core/nativeMain/cinterop/public/windows_zones.hpp +++ b/core/nativeMain/cinterop/public/windows_zones.hpp @@ -601,3 +601,464 @@ static const std::unordered_map windows_to_standard = { "Samoa Standard Time", "Pacific/Apia" }, { "Line Islands Standard Time", "Pacific/Kiritimati" }, }; +static const std::unordered_map zone_ids = { + { "Etc/GMT+12", 0 }, + { "Etc/GMT+11", 1 }, + { "Pacific/Pago_Pago", 2 }, + { "Pacific/Niue", 3 }, + { "Pacific/Midway", 4 }, + { "America/Adak", 5 }, + { "Pacific/Honolulu", 6 }, + { "Pacific/Rarotonga", 7 }, + { "Pacific/Tahiti", 8 }, + { "Pacific/Johnston", 9 }, + { "Etc/GMT+10", 10 }, + { "Pacific/Marquesas", 11 }, + { "America/Anchorage", 12 }, + { "America/Juneau", 13 }, + { "America/Metlakatla", 14 }, + { "America/Nome", 15 }, + { "America/Sitka", 16 }, + { "America/Yakutat", 17 }, + { "Etc/GMT+9", 18 }, + { "Pacific/Gambier", 19 }, + { "America/Tijuana", 20 }, + { "America/Santa_Isabel", 21 }, + { "Etc/GMT+8", 22 }, + { "Pacific/Pitcairn", 23 }, + { "America/Los_Angeles", 24 }, + { "America/Vancouver", 25 }, + { "America/Dawson", 26 }, + { "America/Whitehorse", 27 }, + { "PST8PDT", 28 }, + { "America/Phoenix", 29 }, + { "America/Dawson_Creek", 30 }, + { "America/Creston", 31 }, + { "America/Fort_Nelson", 32 }, + { "America/Hermosillo", 33 }, + { "Etc/GMT+7", 34 }, + { "America/Chihuahua", 35 }, + { "America/Mazatlan", 36 }, + { "America/Denver", 37 }, + { "America/Edmonton", 38 }, + { "America/Cambridge_Bay", 39 }, + { "America/Inuvik", 40 }, + { "America/Yellowknife", 41 }, + { "America/Ojinaga", 42 }, + { "America/Boise", 43 }, + { "MST7MDT", 44 }, + { "America/Guatemala", 45 }, + { "America/Belize", 46 }, + { "America/Costa_Rica", 47 }, + { "Pacific/Galapagos", 48 }, + { "America/Tegucigalpa", 49 }, + { "America/Managua", 50 }, + { "America/El_Salvador", 51 }, + { "Etc/GMT+6", 52 }, + { "America/Chicago", 53 }, + { "America/Winnipeg", 54 }, + { "America/Rainy_River", 55 }, + { "America/Rankin_Inlet", 56 }, + { "America/Resolute", 57 }, + { "America/Matamoros", 58 }, + { "America/Indiana/Knox", 59 }, + { "America/Indiana/Tell_City", 60 }, + { "America/Menominee", 61 }, + { "America/North_Dakota/Beulah", 62 }, + { "America/North_Dakota/Center", 63 }, + { "America/North_Dakota/New_Salem", 64 }, + { "CST6CDT", 65 }, + { "Pacific/Easter", 66 }, + { "America/Mexico_City", 67 }, + { "America/Bahia_Banderas", 68 }, + { "America/Merida", 69 }, + { "America/Monterrey", 70 }, + { "America/Regina", 71 }, + { "America/Swift_Current", 72 }, + { "America/Bogota", 73 }, + { "America/Rio_Branco", 74 }, + { "America/Eirunepe", 75 }, + { "America/Coral_Harbour", 76 }, + { "America/Guayaquil", 77 }, + { "America/Jamaica", 78 }, + { "America/Cayman", 79 }, + { "America/Panama", 80 }, + { "America/Lima", 81 }, + { "Etc/GMT+5", 82 }, + { "America/Cancun", 83 }, + { "America/New_York", 84 }, + { "America/Nassau", 85 }, + { "America/Toronto", 86 }, + { "America/Iqaluit", 87 }, + { "America/Montreal", 88 }, + { "America/Nipigon", 89 }, + { "America/Pangnirtung", 90 }, + { "America/Thunder_Bay", 91 }, + { "America/Detroit", 92 }, + { "America/Indiana/Petersburg", 93 }, + { "America/Indiana/Vincennes", 94 }, + { "America/Indiana/Winamac", 95 }, + { "America/Kentucky/Monticello", 96 }, + { "America/Louisville", 97 }, + { "EST5EDT", 98 }, + { "America/Port-au-Prince", 99 }, + { "America/Havana", 100 }, + { "America/Indianapolis", 101 }, + { "America/Indiana/Marengo", 102 }, + { "America/Indiana/Vevay", 103 }, + { "America/Grand_Turk", 104 }, + { "America/Asuncion", 105 }, + { "America/Halifax", 106 }, + { "Atlantic/Bermuda", 107 }, + { "America/Glace_Bay", 108 }, + { "America/Goose_Bay", 109 }, + { "America/Moncton", 110 }, + { "America/Thule", 111 }, + { "America/Caracas", 112 }, + { "America/Cuiaba", 113 }, + { "America/Campo_Grande", 114 }, + { "America/La_Paz", 115 }, + { "America/Antigua", 116 }, + { "America/Anguilla", 117 }, + { "America/Aruba", 118 }, + { "America/Barbados", 119 }, + { "America/St_Barthelemy", 120 }, + { "America/Kralendijk", 121 }, + { "America/Manaus", 122 }, + { "America/Boa_Vista", 123 }, + { "America/Porto_Velho", 124 }, + { "America/Blanc-Sablon", 125 }, + { "America/Curacao", 126 }, + { "America/Dominica", 127 }, + { "America/Santo_Domingo", 128 }, + { "America/Grenada", 129 }, + { "America/Guadeloupe", 130 }, + { "America/Guyana", 131 }, + { "America/St_Kitts", 132 }, + { "America/St_Lucia", 133 }, + { "America/Marigot", 134 }, + { "America/Martinique", 135 }, + { "America/Montserrat", 136 }, + { "America/Puerto_Rico", 137 }, + { "America/Lower_Princes", 138 }, + { "America/Port_of_Spain", 139 }, + { "America/St_Vincent", 140 }, + { "America/Tortola", 141 }, + { "America/St_Thomas", 142 }, + { "Etc/GMT+4", 143 }, + { "America/Santiago", 144 }, + { "America/St_Johns", 145 }, + { "America/Araguaina", 146 }, + { "America/Sao_Paulo", 147 }, + { "America/Cayenne", 148 }, + { "Antarctica/Rothera", 149 }, + { "Antarctica/Palmer", 150 }, + { "America/Fortaleza", 151 }, + { "America/Belem", 152 }, + { "America/Maceio", 153 }, + { "America/Recife", 154 }, + { "America/Santarem", 155 }, + { "Atlantic/Stanley", 156 }, + { "America/Paramaribo", 157 }, + { "Etc/GMT+3", 158 }, + { "America/Buenos_Aires", 159 }, + { "America/Argentina/La_Rioja", 160 }, + { "America/Argentina/Rio_Gallegos", 161 }, + { "America/Argentina/Salta", 162 }, + { "America/Argentina/San_Juan", 163 }, + { "America/Argentina/San_Luis", 164 }, + { "America/Argentina/Tucuman", 165 }, + { "America/Argentina/Ushuaia", 166 }, + { "America/Catamarca", 167 }, + { "America/Cordoba", 168 }, + { "America/Jujuy", 169 }, + { "America/Mendoza", 170 }, + { "America/Godthab", 171 }, + { "America/Montevideo", 172 }, + { "America/Punta_Arenas", 173 }, + { "America/Miquelon", 174 }, + { "America/Bahia", 175 }, + { "Etc/GMT+2", 176 }, + { "America/Noronha", 177 }, + { "Atlantic/South_Georgia", 178 }, + { "Atlantic/Azores", 179 }, + { "America/Scoresbysund", 180 }, + { "Atlantic/Cape_Verde", 181 }, + { "Etc/GMT+1", 182 }, + { "Etc/GMT", 183 }, + { "America/Danmarkshavn", 184 }, + { "Etc/UTC", 185 }, + { "Europe/London", 186 }, + { "Atlantic/Canary", 187 }, + { "Atlantic/Faeroe", 188 }, + { "Europe/Guernsey", 189 }, + { "Europe/Dublin", 190 }, + { "Europe/Isle_of_Man", 191 }, + { "Europe/Jersey", 192 }, + { "Europe/Lisbon", 193 }, + { "Atlantic/Madeira", 194 }, + { "Atlantic/Reykjavik", 195 }, + { "Africa/Ouagadougou", 196 }, + { "Africa/Abidjan", 197 }, + { "Africa/Accra", 198 }, + { "Africa/Banjul", 199 }, + { "Africa/Conakry", 200 }, + { "Africa/Bissau", 201 }, + { "Africa/Monrovia", 202 }, + { "Africa/Bamako", 203 }, + { "Africa/Nouakchott", 204 }, + { "Atlantic/St_Helena", 205 }, + { "Africa/Freetown", 206 }, + { "Africa/Dakar", 207 }, + { "Africa/Lome", 208 }, + { "Africa/Sao_Tome", 209 }, + { "Africa/Casablanca", 210 }, + { "Africa/El_Aaiun", 211 }, + { "Europe/Berlin", 212 }, + { "Europe/Andorra", 213 }, + { "Europe/Vienna", 214 }, + { "Europe/Zurich", 215 }, + { "Europe/Busingen", 216 }, + { "Europe/Gibraltar", 217 }, + { "Europe/Rome", 218 }, + { "Europe/Vaduz", 219 }, + { "Europe/Luxembourg", 220 }, + { "Europe/Monaco", 221 }, + { "Europe/Malta", 222 }, + { "Europe/Amsterdam", 223 }, + { "Europe/Oslo", 224 }, + { "Europe/Stockholm", 225 }, + { "Arctic/Longyearbyen", 226 }, + { "Europe/San_Marino", 227 }, + { "Europe/Vatican", 228 }, + { "Europe/Budapest", 229 }, + { "Europe/Tirane", 230 }, + { "Europe/Prague", 231 }, + { "Europe/Podgorica", 232 }, + { "Europe/Belgrade", 233 }, + { "Europe/Ljubljana", 234 }, + { "Europe/Bratislava", 235 }, + { "Europe/Paris", 236 }, + { "Europe/Brussels", 237 }, + { "Europe/Copenhagen", 238 }, + { "Europe/Madrid", 239 }, + { "Africa/Ceuta", 240 }, + { "Europe/Warsaw", 241 }, + { "Europe/Sarajevo", 242 }, + { "Europe/Zagreb", 243 }, + { "Europe/Skopje", 244 }, + { "Africa/Lagos", 245 }, + { "Africa/Luanda", 246 }, + { "Africa/Porto-Novo", 247 }, + { "Africa/Kinshasa", 248 }, + { "Africa/Bangui", 249 }, + { "Africa/Brazzaville", 250 }, + { "Africa/Douala", 251 }, + { "Africa/Algiers", 252 }, + { "Africa/Libreville", 253 }, + { "Africa/Malabo", 254 }, + { "Africa/Niamey", 255 }, + { "Africa/Ndjamena", 256 }, + { "Africa/Tunis", 257 }, + { "Etc/GMT-1", 258 }, + { "Asia/Amman", 259 }, + { "Europe/Bucharest", 260 }, + { "Asia/Nicosia", 261 }, + { "Asia/Famagusta", 262 }, + { "Europe/Athens", 263 }, + { "Asia/Beirut", 264 }, + { "Africa/Cairo", 265 }, + { "Europe/Chisinau", 266 }, + { "Asia/Damascus", 267 }, + { "Asia/Hebron", 268 }, + { "Asia/Gaza", 269 }, + { "Africa/Johannesburg", 270 }, + { "Africa/Bujumbura", 271 }, + { "Africa/Gaborone", 272 }, + { "Africa/Lubumbashi", 273 }, + { "Africa/Maseru", 274 }, + { "Africa/Blantyre", 275 }, + { "Africa/Maputo", 276 }, + { "Africa/Kigali", 277 }, + { "Africa/Mbabane", 278 }, + { "Africa/Lusaka", 279 }, + { "Africa/Harare", 280 }, + { "Etc/GMT-2", 281 }, + { "Europe/Kiev", 282 }, + { "Europe/Mariehamn", 283 }, + { "Europe/Sofia", 284 }, + { "Europe/Tallinn", 285 }, + { "Europe/Helsinki", 286 }, + { "Europe/Vilnius", 287 }, + { "Europe/Riga", 288 }, + { "Europe/Uzhgorod", 289 }, + { "Europe/Zaporozhye", 290 }, + { "Asia/Jerusalem", 291 }, + { "Europe/Kaliningrad", 292 }, + { "Africa/Khartoum", 293 }, + { "Africa/Tripoli", 294 }, + { "Africa/Windhoek", 295 }, + { "Asia/Baghdad", 296 }, + { "Europe/Istanbul", 297 }, + { "Asia/Riyadh", 298 }, + { "Asia/Bahrain", 299 }, + { "Asia/Kuwait", 300 }, + { "Asia/Qatar", 301 }, + { "Asia/Aden", 302 }, + { "Europe/Minsk", 303 }, + { "Europe/Moscow", 304 }, + { "Europe/Kirov", 305 }, + { "Europe/Simferopol", 306 }, + { "Africa/Nairobi", 307 }, + { "Antarctica/Syowa", 308 }, + { "Africa/Djibouti", 309 }, + { "Africa/Asmera", 310 }, + { "Africa/Addis_Ababa", 311 }, + { "Indian/Comoro", 312 }, + { "Indian/Antananarivo", 313 }, + { "Africa/Mogadishu", 314 }, + { "Africa/Juba", 315 }, + { "Africa/Dar_es_Salaam", 316 }, + { "Africa/Kampala", 317 }, + { "Indian/Mayotte", 318 }, + { "Etc/GMT-3", 319 }, + { "Asia/Tehran", 320 }, + { "Asia/Dubai", 321 }, + { "Asia/Muscat", 322 }, + { "Etc/GMT-4", 323 }, + { "Europe/Astrakhan", 324 }, + { "Europe/Ulyanovsk", 325 }, + { "Asia/Baku", 326 }, + { "Europe/Samara", 327 }, + { "Indian/Mauritius", 328 }, + { "Indian/Reunion", 329 }, + { "Indian/Mahe", 330 }, + { "Europe/Saratov", 331 }, + { "Asia/Tbilisi", 332 }, + { "Europe/Volgograd", 333 }, + { "Asia/Yerevan", 334 }, + { "Asia/Kabul", 335 }, + { "Asia/Tashkent", 336 }, + { "Antarctica/Mawson", 337 }, + { "Asia/Oral", 338 }, + { "Asia/Aqtau", 339 }, + { "Asia/Aqtobe", 340 }, + { "Asia/Atyrau", 341 }, + { "Indian/Maldives", 342 }, + { "Indian/Kerguelen", 343 }, + { "Asia/Dushanbe", 344 }, + { "Asia/Ashgabat", 345 }, + { "Asia/Samarkand", 346 }, + { "Etc/GMT-5", 347 }, + { "Asia/Yekaterinburg", 348 }, + { "Asia/Karachi", 349 }, + { "Asia/Qyzylorda", 350 }, + { "Asia/Calcutta", 351 }, + { "Asia/Colombo", 352 }, + { "Asia/Katmandu", 353 }, + { "Asia/Almaty", 354 }, + { "Antarctica/Vostok", 355 }, + { "Asia/Urumqi", 356 }, + { "Indian/Chagos", 357 }, + { "Asia/Bishkek", 358 }, + { "Asia/Qostanay", 359 }, + { "Etc/GMT-6", 360 }, + { "Asia/Dhaka", 361 }, + { "Asia/Thimphu", 362 }, + { "Asia/Omsk", 363 }, + { "Asia/Rangoon", 364 }, + { "Indian/Cocos", 365 }, + { "Asia/Bangkok", 366 }, + { "Antarctica/Davis", 367 }, + { "Indian/Christmas", 368 }, + { "Asia/Jakarta", 369 }, + { "Asia/Pontianak", 370 }, + { "Asia/Phnom_Penh", 371 }, + { "Asia/Vientiane", 372 }, + { "Asia/Saigon", 373 }, + { "Etc/GMT-7", 374 }, + { "Asia/Barnaul", 375 }, + { "Asia/Hovd", 376 }, + { "Asia/Krasnoyarsk", 377 }, + { "Asia/Novokuznetsk", 378 }, + { "Asia/Novosibirsk", 379 }, + { "Asia/Tomsk", 380 }, + { "Asia/Shanghai", 381 }, + { "Asia/Hong_Kong", 382 }, + { "Asia/Macau", 383 }, + { "Asia/Irkutsk", 384 }, + { "Asia/Singapore", 385 }, + { "Antarctica/Casey", 386 }, + { "Asia/Brunei", 387 }, + { "Asia/Makassar", 388 }, + { "Asia/Kuala_Lumpur", 389 }, + { "Asia/Kuching", 390 }, + { "Asia/Manila", 391 }, + { "Etc/GMT-8", 392 }, + { "Australia/Perth", 393 }, + { "Asia/Taipei", 394 }, + { "Asia/Ulaanbaatar", 395 }, + { "Asia/Choibalsan", 396 }, + { "Australia/Eucla", 397 }, + { "Asia/Chita", 398 }, + { "Asia/Tokyo", 399 }, + { "Asia/Jayapura", 400 }, + { "Pacific/Palau", 401 }, + { "Asia/Dili", 402 }, + { "Etc/GMT-9", 403 }, + { "Asia/Pyongyang", 404 }, + { "Asia/Seoul", 405 }, + { "Asia/Yakutsk", 406 }, + { "Asia/Khandyga", 407 }, + { "Australia/Adelaide", 408 }, + { "Australia/Broken_Hill", 409 }, + { "Australia/Darwin", 410 }, + { "Australia/Brisbane", 411 }, + { "Australia/Lindeman", 412 }, + { "Australia/Sydney", 413 }, + { "Australia/Melbourne", 414 }, + { "Pacific/Port_Moresby", 415 }, + { "Antarctica/DumontDUrville", 416 }, + { "Pacific/Truk", 417 }, + { "Pacific/Guam", 418 }, + { "Pacific/Saipan", 419 }, + { "Etc/GMT-10", 420 }, + { "Australia/Hobart", 421 }, + { "Australia/Currie", 422 }, + { "Asia/Vladivostok", 423 }, + { "Asia/Ust-Nera", 424 }, + { "Australia/Lord_Howe", 425 }, + { "Pacific/Bougainville", 426 }, + { "Asia/Srednekolymsk", 427 }, + { "Asia/Magadan", 428 }, + { "Pacific/Norfolk", 429 }, + { "Asia/Sakhalin", 430 }, + { "Pacific/Guadalcanal", 431 }, + { "Antarctica/Macquarie", 432 }, + { "Pacific/Ponape", 433 }, + { "Pacific/Kosrae", 434 }, + { "Pacific/Noumea", 435 }, + { "Pacific/Efate", 436 }, + { "Etc/GMT-11", 437 }, + { "Asia/Kamchatka", 438 }, + { "Asia/Anadyr", 439 }, + { "Pacific/Auckland", 440 }, + { "Antarctica/McMurdo", 441 }, + { "Etc/GMT-12", 442 }, + { "Pacific/Tarawa", 443 }, + { "Pacific/Majuro", 444 }, + { "Pacific/Kwajalein", 445 }, + { "Pacific/Nauru", 446 }, + { "Pacific/Funafuti", 447 }, + { "Pacific/Wake", 448 }, + { "Pacific/Wallis", 449 }, + { "Pacific/Fiji", 450 }, + { "Pacific/Chatham", 451 }, + { "Etc/GMT-13", 452 }, + { "Pacific/Enderbury", 453 }, + { "Pacific/Fakaofo", 454 }, + { "Pacific/Tongatapu", 455 }, + { "Pacific/Apia", 456 }, + { "Pacific/Kiritimati", 457 }, + { "Etc/GMT-14", 458 }, +}; diff --git a/core/nativeMain/src/Instant.kt b/core/nativeMain/src/Instant.kt index 77ca51282..b3a01ec50 100644 --- a/core/nativeMain/src/Instant.kt +++ b/core/nativeMain/src/Instant.kt @@ -24,9 +24,10 @@ public actual enum class DayOfWeek { SUNDAY; } +// This is a function and not a value due to https://github.com/Kotlin/kotlinx-datetime/issues/5 // org.threeten.bp.format.DateTimeFormatterBuilder.InstantPrinterParser#parse -private val instantParser: Parser = - localDateParser +private val instantParser: Parser + get() = localDateParser .chainIgnoring(concreteCharParser('T').or(concreteCharParser('t'))) .chain(intParser(2, 2)) // hour .chainIgnoring(concreteCharParser(':')) @@ -90,8 +91,13 @@ public actual class Instant internal constructor(internal val epochSeconds: Long (this.epochSeconds - other.epochSeconds).seconds + // won't overflow given the instant bounds (this.nanos - other.nanos).nanoseconds - actual override fun compareTo(other: Instant): Int = - compareBy({ it.epochSeconds }, { it.nanos }).compare(this, other) + actual override fun compareTo(other: Instant): Int { + val s = epochSeconds.compareTo(other.epochSeconds) + if (s != 0) { + return s + } + return nanos.compareTo(other.nanos) + } override fun equals(other: Any?): Boolean = this === other || other is Instant && this.epochSeconds == other.epochSeconds && this.nanos == other.nanos diff --git a/core/nativeMain/src/LocalDate.kt b/core/nativeMain/src/LocalDate.kt index 04cdd4d73..494ecc4bd 100644 --- a/core/nativeMain/src/LocalDate.kt +++ b/core/nativeMain/src/LocalDate.kt @@ -10,6 +10,7 @@ package kotlinx.datetime import kotlin.math.* +// This is a function and not a value due to https://github.com/Kotlin/kotlinx-datetime/issues/5 // org.threeten.bp.format.DateTimeFormatter#ISO_LOCAL_DATE internal val localDateParser: Parser get() = intParser(4, 10) @@ -23,14 +24,27 @@ internal val localDateParser: Parser LocalDate(year, month, day) } -public actual class LocalDate private constructor(actual val year: Int, actual val month: Month, actual val dayOfMonth: Int) : Comparable { +public actual class LocalDate actual constructor(actual val year: Int, actual val monthNumber: Int, actual val dayOfMonth: Int) : Comparable { + + init { + // org.threeten.bp.LocalDate#create + if (dayOfMonth > 28 && dayOfMonth > monthNumber.monthLength(isLeapYear(year))) { + if (dayOfMonth == 29) { + throw IllegalArgumentException("Invalid date 'February 29' as '$year' is not a leap year") + } else { + throw IllegalArgumentException("Invalid date '${month.name} $dayOfMonth'") + } + } + } + actual companion object { actual fun parse(isoString: String): LocalDate = localDateParser.parse(isoString) // org.threeten.bp.LocalDate#toEpochDay internal fun ofEpochDay(epochDay: Long): LocalDate { - require(epochDay in -365243219162L..365241780471L) + // Unidiomatic code due to https://github.com/Kotlin/kotlinx-datetime/issues/5 + require(epochDay >= -365243219162L && epochDay <= 365241780471L) var zeroDay: Long = epochDay + DAYS_0000_TO_1970 // find the march-based year zeroDay -= 60 // adjust to 0000-03-01 so leap day is at end of four year cycle @@ -62,17 +76,6 @@ public actual class LocalDate private constructor(actual val year: Int, actual v } } - // org.threeten.bp.LocalDate#create - actual constructor(year: Int, monthNumber: Int, dayOfMonth: Int) : this(year, Month(monthNumber), dayOfMonth) { - if (dayOfMonth > 28 && dayOfMonth > month.length(isLeapYear(year))) { - if (dayOfMonth == 29) { - throw IllegalArgumentException("Invalid date 'February 29' as '$year' is not a leap year") - } else { - throw IllegalArgumentException("Invalid date '${month.name} $dayOfMonth'") - } - } - } - // org.threeten.bp.LocalDate#toEpochDay internal fun toEpochDay(): Long { val y = year @@ -97,23 +100,36 @@ public actual class LocalDate private constructor(actual val year: Int, actual v internal fun withYear(newYear: Int): LocalDate = LocalDate(newYear, monthNumber, dayOfMonth) - actual val monthNumber: Int = month.number + actual val month: Month + get() = Month(monthNumber) // org.threeten.bp.LocalDate#getDayOfWeek - actual val dayOfWeek: DayOfWeek = run { - val dow0 = floorMod(toEpochDay() + 3, 7).toInt() - DayOfWeek(dow0 + 1) - } + actual val dayOfWeek: DayOfWeek + get() { + val dow0 = floorMod(toEpochDay() + 3, 7).toInt() + return DayOfWeek(dow0 + 1) + } // org.threeten.bp.LocalDate#getDayOfYear - actual val dayOfYear: Int = month.firstDayOfYear(isLeapYear(year)) + dayOfMonth - 1 - - actual override fun compareTo(other: LocalDate): Int = - compareBy({ it.year }, { it.monthNumber }, { it.dayOfMonth }).compare(this, other) + actual val dayOfYear: Int + get() = month.firstDayOfYear(isLeapYear(year)) + dayOfMonth - 1 + + // Several times faster than using `compareBy` + actual override fun compareTo(other: LocalDate): Int { + val y = year.compareTo(other.year) + if (y != 0) { + return y + } + val m = monthNumber.compareTo(other.monthNumber) + if (m != 0) { + return m + } + return dayOfMonth.compareTo(dayOfMonth) + } // org.threeten.bp.LocalDate#resolvePreviousValid - private fun resolvePreviousValid(year: Int, month: Month, day: Int): LocalDate { - val newDay = min(day, month.length(isLeapYear(year))) + private fun resolvePreviousValid(year: Int, month: Int, day: Int): LocalDate { + val newDay = min(day, month.monthLength(isLeapYear(year))) return LocalDate(year, month, newDay) } @@ -123,7 +139,7 @@ public actual class LocalDate private constructor(actual val year: Int, actual v this } else { val newYear = safeAdd(year.toLong(), yearsToAdd).toInt() - resolvePreviousValid(newYear, month, dayOfMonth) + resolvePreviousValid(newYear, monthNumber, dayOfMonth) } // org.threeten.bp.LocalDate#plusMonths @@ -134,7 +150,7 @@ public actual class LocalDate private constructor(actual val year: Int, actual v val monthCount: Long = year * 12L + (monthNumber - 1) val calcMonths = monthCount + monthsToAdd // safe overflow val newYear: Int = /* YEAR.checkValidIntValue( */ floorDiv(calcMonths, 12).toInt() - val newMonth = Month(floorMod(calcMonths, 12).toInt() + 1) + val newMonth = floorMod(calcMonths, 12).toInt() + 1 return resolvePreviousValid(newYear, newMonth, dayOfMonth) } diff --git a/core/nativeMain/src/LocalDateTime.kt b/core/nativeMain/src/LocalDateTime.kt index 20a9ea1f8..3c2e27714 100644 --- a/core/nativeMain/src/LocalDateTime.kt +++ b/core/nativeMain/src/LocalDateTime.kt @@ -8,6 +8,7 @@ package kotlinx.datetime +// This is a function and not a value due to https://github.com/Kotlin/kotlinx-datetime/issues/5 // org.threeten.bp.format.DateTimeFormatter#ISO_LOCAL_DATE_TIME internal val localDateTimeParser: Parser get() = localDateParser @@ -38,8 +39,14 @@ public actual class LocalDateTime internal constructor( actual val second: Int get() = time.second actual val nanosecond: Int get() = time.nanosecond - actual override fun compareTo(other: LocalDateTime): Int = - compareBy({ it.date }, { it.time }).compare(this, other) + // Several times faster than using `compareBy` + actual override fun compareTo(other: LocalDateTime): Int { + val d = date.compareTo(other.date) + if (d != 0) { + return d + } + return time.compareTo(other.time) + } override fun equals(other: Any?): Boolean = this === other || (other is LocalDateTime && compareTo(other) == 0) diff --git a/core/nativeMain/src/LocalTime.kt b/core/nativeMain/src/LocalTime.kt index ac8f7cbf4..cd0254260 100644 --- a/core/nativeMain/src/LocalTime.kt +++ b/core/nativeMain/src/LocalTime.kt @@ -8,6 +8,7 @@ package kotlinx.datetime +// This is a function and not a value due to https://github.com/Kotlin/kotlinx-datetime/issues/5 // org.threeten.bp.format.DateTimeFormatter#ISO_LOCAL_TIME internal val localTimeParser: Parser get() = intParser(2, 2) // hour @@ -39,8 +40,9 @@ internal class LocalTime(val hour: Int, val minute: Int, val second: Int, val na // org.threeten.bp.LocalTime#ofSecondOfDay(long, int) internal fun ofSecondOfDay(secondOfDay: Long, nanoOfSecond: Int): LocalTime { - require(secondOfDay in 0..SECONDS_PER_DAY) - require(nanoOfSecond in 0..1_000_000_000) + // Unidiomatic code due to https://github.com/Kotlin/kotlinx-datetime/issues/5 + require(secondOfDay >= 0 && secondOfDay <= SECONDS_PER_DAY) + require(nanoOfSecond >= 0 && nanoOfSecond <= 1_000_000_000) val hours = (secondOfDay / SECONDS_PER_HOUR).toInt() val secondWithoutHours = secondOfDay - hours * SECONDS_PER_HOUR.toLong() val minutes = (secondWithoutHours / SECONDS_PER_MINUTE).toInt() @@ -61,8 +63,22 @@ internal class LocalTime(val hour: Int, val minute: Int, val second: Int, val na } } - override fun compareTo(other: LocalTime): Int = - compareBy({ it.hour }, { it.minute }, { it.second }, { it.nanosecond }).compare(this, other) + // Several times faster than using `compareBy` + override fun compareTo(other: LocalTime): Int { + val h = hour.compareTo(other.hour) + if (h != 0) { + return h + } + val m = minute.compareTo(other.minute) + if (m != 0) { + return m + } + val s = second.compareTo(other.second) + if (s != 0) { + return s + } + return nanosecond.compareTo(other.nanosecond) + } override fun hashCode(): Int { val nod: Long = toNanoOfDay() diff --git a/core/nativeMain/src/Month.kt b/core/nativeMain/src/Month.kt index 915342059..2eb243e8b 100644 --- a/core/nativeMain/src/Month.kt +++ b/core/nativeMain/src/Month.kt @@ -29,10 +29,10 @@ internal fun Month.firstDayOfYear(leapYear: Boolean): Int { } // From threetenbp -internal fun Month.length(leapYear: Boolean): Int { +internal fun Int.monthLength(leapYear: Boolean): Int { return when (this) { - Month.FEBRUARY -> if (leapYear) 29 else 28 - Month.APRIL, Month.JUNE, Month.SEPTEMBER, Month.NOVEMBER -> 30 + 2 -> if (leapYear) 29 else 28 + 4, 6, 9, 11 -> 30 else -> 31 } } diff --git a/core/nativeMain/src/Parser.kt b/core/nativeMain/src/Parser.kt index 0714f91c1..b93444c4c 100644 --- a/core/nativeMain/src/Parser.kt +++ b/core/nativeMain/src/Parser.kt @@ -26,6 +26,7 @@ internal fun Parser.chainIgnoring(other: Parser): Parser = internal fun Parser.chainSkipping(other: Parser): Parser = chain(other).map { (_, s) -> s } +@SharedImmutable internal val eofParser: Parser = { str, pos -> if (str.length > pos) { throw ParseException("extraneous input", pos) diff --git a/core/nativeMain/src/TimeZone.kt b/core/nativeMain/src/TimeZone.kt index 52652c060..be0f0293b 100644 --- a/core/nativeMain/src/TimeZone.kt +++ b/core/nativeMain/src/TimeZone.kt @@ -10,20 +10,26 @@ package kotlinx.datetime import kotlinx.cinterop.* import platform.posix.* +import kotlin.native.concurrent.* class DateTimeException(str: String? = null) : Exception(str) -public actual open class TimeZone internal constructor(actual val id: String) { +public actual open class TimeZone internal constructor(val tzid: TZID, actual val id: String) { + + private val asciiName = id.cstr actual companion object { - actual val SYSTEM: TimeZone - get() = memScoped { - val string = get_system_timezone() ?: throw RuntimeException("Failed to get the system timezone.") - val kotlinString = string.toKString() - free(string) - TimeZone(kotlinString) - } + @SharedImmutable + actual val SYSTEM: TimeZone = memScoped { + val tzid = alloc() + val string = get_system_timezone(tzid.ptr) ?: throw RuntimeException("Failed to get the system timezone.") + val kotlinString = string.toKString() + free(string) + TimeZone(tzid.value, kotlinString) + } + + @SharedImmutable actual val UTC: TimeZone = ZoneOffset.UTC // org.threeten.bp.ZoneId#of(java.lang.String) @@ -54,15 +60,16 @@ public actual open class TimeZone internal constructor(actual val id: String) { ZoneOffset(0, "UT") } else ZoneOffset(offset.totalSeconds, "UT" + offset.id) } - if (!is_known_timezone(zoneId)) { + val tzid = timezone_by_name(zoneId) + if (tzid == TZID_INVALID) { throw IllegalArgumentException("Invalid timezone '$zoneId'") } - return TimeZone(zoneId) + return TimeZone(tzid, zoneId) } actual val availableZoneIds: Set get() { - val set = mutableSetOf("UTC") + val set = mutableSetOf("UTC") val zones = available_zone_ids() ?: throw RuntimeException("Failed to get the list of available timezones") var ptr = zones @@ -82,26 +89,26 @@ public actual open class TimeZone internal constructor(actual val id: String) { actual open val Instant.offset: ZoneOffset get() { - val offset = offset_at_instant(id, epochSeconds) + val offset = offset_at_instant(tzid, epochSeconds) if (offset == INT_MAX) { throw RuntimeException("Unable to acquire the offset at instant $this for zone ${this@TimeZone}") } - return ZoneOffset(offset) + return ZoneOffset.ofSeconds(offset) } actual fun LocalDateTime.toInstant(): Instant = atZone().toInstant() internal open fun LocalDateTime.atZone(preferred: ZoneOffset? = null): ZonedDateTime = memScoped { - val epochSeconds = toEpochSecond(ZoneOffset(0)) + val epochSeconds = toEpochSecond(ZoneOffset.UTC) val offset = alloc() offset.value = preferred?.totalSeconds ?: INT_MAX - val transitionDuration = offset_at_datetime(id, epochSeconds, offset.ptr) + val transitionDuration = offset_at_datetime(tzid, epochSeconds, offset.ptr) if (offset.value == INT_MAX) { throw RuntimeException("Unable to acquire the offset at ${this@atZone} for zone ${this@TimeZone}") } val dateTime = this@atZone.plusSeconds(transitionDuration.toLong()) - ZonedDateTime(dateTime, this@TimeZone, ZoneOffset(offset.value)) + ZonedDateTime(dateTime, this@TimeZone, ZoneOffset.ofSeconds(offset.value)) } // org.threeten.bp.ZoneId#equals @@ -116,12 +123,15 @@ public actual open class TimeZone internal constructor(actual val id: String) { } -public actual class ZoneOffset internal constructor(actual val totalSeconds: Int, id: String? = null) : TimeZone(id - ?: zoneIdByOffset(totalSeconds)) { +@ThreadLocal +private var zoneOffsetCache: MutableMap = mutableMapOf() + +public actual class ZoneOffset internal constructor(actual val totalSeconds: Int, id: String) : TimeZone(TZID_INVALID, id) { companion object { // org.threeten.bp.ZoneOffset#UTC - val UTC = ZoneOffset(0) + @SharedImmutable + val UTC = ZoneOffset(0, "Z") // org.threeten.bp.ZoneOffset#of internal fun of(offsetId: String): ZoneOffset { @@ -207,9 +217,18 @@ public actual class ZoneOffset internal constructor(actual val totalSeconds: Int internal fun ofHoursMinutesSeconds(hours: Int, minutes: Int, seconds: Int): ZoneOffset { validate(hours, minutes, seconds) return if (hours == 0 && minutes == 0 && seconds == 0) UTC - else ZoneOffset(hours * SECONDS_PER_HOUR + minutes * SECONDS_PER_MINUTE + seconds) + else ofSeconds(hours * SECONDS_PER_HOUR + minutes * SECONDS_PER_MINUTE + seconds) } + // org.threeten.bp.ZoneOffset#ofTotalSeconds + internal fun ofSeconds(seconds: Int): ZoneOffset = + if (seconds % (15 * SECONDS_PER_MINUTE) == 0) { + zoneOffsetCache[seconds] ?: + ZoneOffset(seconds, zoneIdByOffset(seconds)).also { zoneOffsetCache[seconds] = it } + } else { + ZoneOffset(seconds, zoneIdByOffset(seconds)) + } + // org.threeten.bp.ZoneOffset#parseNumber private fun parseNumber(offsetId: CharSequence, pos: Int, precededByColon: Boolean): Int { if (precededByColon && offsetId[pos - 1] != ':') { diff --git a/core/nativeTest/src/ThreeTenBpInstantTest.kt b/core/nativeTest/src/ThreeTenBpInstantTest.kt index d414822ba..a77a3c08f 100644 --- a/core/nativeTest/src/ThreeTenBpInstantTest.kt +++ b/core/nativeTest/src/ThreeTenBpInstantTest.kt @@ -6,8 +6,9 @@ * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos */ -package kotlinx.datetime +package kotlinx.datetime.test +import kotlinx.datetime.* import kotlin.test.* class ThreeTenBpInstantTest { diff --git a/core/nativeTest/src/ThreeTenBpLocalDateTest.kt b/core/nativeTest/src/ThreeTenBpLocalDateTest.kt index a2f048720..e382b6880 100644 --- a/core/nativeTest/src/ThreeTenBpLocalDateTest.kt +++ b/core/nativeTest/src/ThreeTenBpLocalDateTest.kt @@ -6,8 +6,9 @@ * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos */ -package kotlinx.datetime +package kotlinx.datetime.test +import kotlinx.datetime.* import kotlin.test.* class ThreeTenBpLocalDateTest { @@ -31,9 +32,9 @@ class ThreeTenBpLocalDateTest { @Test fun toEpochDay() { - val date_0000_01_01 = -678941 - 40587.toLong() + val date_0000_01_01 = -678941 - 40587L - var test: LocalDate = LocalDate(0, 1, 1) + var test = LocalDate(0, 1, 1) for (i in date_0000_01_01..699999) { assertEquals(i, test.toEpochDay()) test = next(test) @@ -54,10 +55,10 @@ class ThreeTenBpLocalDateTest { @Test fun dayOfWeek() { var dow = DayOfWeek.MONDAY - for (month in Month.values()) { - val length = month.length(false) + for (month in 1..12) { + val length = month.monthLength(false) for (i in 1..length) { - val d = LocalDate(2007, month.number, i) + val d = LocalDate(2007, month, i) assertSame(d.dayOfWeek, dow) dow = DayOfWeek(dow.number % 7 + 1) } @@ -77,7 +78,7 @@ class ThreeTenBpLocalDateTest { val a: LocalDate = LocalDate(y, m, d) var total = 0 for (i in 1 until m) { - total += Month(i).length(isLeapYear(y)) + total += i.monthLength(isLeapYear(y)) } val doy = total + d assertEquals(a.dayOfYear, doy) @@ -178,7 +179,7 @@ class ThreeTenBpLocalDateTest { private fun next(localDate: LocalDate): LocalDate { var date = localDate val newDayOfMonth: Int = date.dayOfMonth + 1 - if (newDayOfMonth <= date.month.length(isLeapYear(date.year))) { + if (newDayOfMonth <= date.monthNumber.monthLength(isLeapYear(date.year))) { return LocalDate(date.year, date.monthNumber, newDayOfMonth) } date = LocalDate(date.year, date.monthNumber, 1) @@ -198,6 +199,6 @@ class ThreeTenBpLocalDateTest { if (date.month === Month.DECEMBER) { date = date.withYear(date.year - 1) } - return LocalDate(date.year, date.monthNumber, date.month.length(isLeapYear(date.year))) + return LocalDate(date.year, date.monthNumber, date.monthNumber.monthLength(isLeapYear(date.year))) } } diff --git a/core/nativeTest/src/ThreeTenBpLocalDateTimeTest.kt b/core/nativeTest/src/ThreeTenBpLocalDateTimeTest.kt index 68061ad6a..1c110e764 100644 --- a/core/nativeTest/src/ThreeTenBpLocalDateTimeTest.kt +++ b/core/nativeTest/src/ThreeTenBpLocalDateTimeTest.kt @@ -5,8 +5,9 @@ /* Based on the ThreeTenBp project. * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos */ -package kotlinx.datetime +package kotlinx.datetime.test +import kotlinx.datetime.* import kotlin.test.* class ThreeTenBpLocalDateTimeTest { @@ -14,10 +15,11 @@ class ThreeTenBpLocalDateTimeTest { fun toSecondsAfterEpoch() { for (i in -5..4) { val iHours = i * 3600 - val offset = ZoneOffset(iHours) + val offset = ZoneOffset.ofSeconds(iHours) + val iHoursL = iHours.toLong() for (j in 0..99999L) { val a = LocalDateTime(1970, 1, 1, 0, 0, 0, 0).plusSeconds(j) - assertEquals(j - iHours, a.toEpochSecond(offset), "$i, $j") + assertEquals(j - iHoursL, a.toEpochSecond(offset)) } } } @@ -26,7 +28,7 @@ class ThreeTenBpLocalDateTimeTest { fun toSecondsBeforeEpoch() { for (i in 0..99999L) { val a: LocalDateTime = LocalDateTime(1970, 1, 1, 0, 0, 0, 0).plusSeconds(-i) - assertEquals(-i, a.toEpochSecond(ZoneOffset.UTC), "$i") + assertEquals(-i, a.toEpochSecond(ZoneOffset.UTC)) } } diff --git a/core/nativeTest/src/ThreeTenBpLocalTimeTest.kt b/core/nativeTest/src/ThreeTenBpLocalTimeTest.kt index 0dbc1f17f..1df8e36a6 100644 --- a/core/nativeTest/src/ThreeTenBpLocalTimeTest.kt +++ b/core/nativeTest/src/ThreeTenBpLocalTimeTest.kt @@ -6,8 +6,9 @@ * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos */ -package kotlinx.datetime +package kotlinx.datetime.test +import kotlinx.datetime.* import kotlin.test.* class ThreeTenBpLocalTimeTest { diff --git a/core/nativeTest/src/ThreeTenBpTimeZoneTest.kt b/core/nativeTest/src/ThreeTenBpTimeZoneTest.kt index 4b25c0a9f..becf9243e 100644 --- a/core/nativeTest/src/ThreeTenBpTimeZoneTest.kt +++ b/core/nativeTest/src/ThreeTenBpTimeZoneTest.kt @@ -134,7 +134,7 @@ class ThreeTenBpTimeZoneTest { val t = LocalDateTime(2007, 10, 28, 2, 30, 0, 0) val zone = TimeZone.of("Europe/Paris") assertEquals(ZonedDateTime(LocalDateTime(2007, 10, 28, 2, 30, 0, 0), - zone, ZoneOffset(2 * 3600)), with(zone) { t.atZone() }) + zone, ZoneOffset.ofSeconds(2 * 3600)), with(zone) { t.atZone() }) } } diff --git a/core/nativeTest/src/ThreeTenBpUtilTest.kt b/core/nativeTest/src/ThreeTenBpUtilTest.kt index e55e8bdff..ec2f67ac8 100644 --- a/core/nativeTest/src/ThreeTenBpUtilTest.kt +++ b/core/nativeTest/src/ThreeTenBpUtilTest.kt @@ -6,8 +6,9 @@ * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos */ -package kotlinx.datetime +package kotlinx.datetime.test +import kotlinx.datetime.* import kotlin.test.* class ThreeTenBpUtilTest {