diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 675644cdf..9108880d7 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -100,33 +100,50 @@ kotlin { } targets.withType { + compilations["test"].kotlinOptions { + freeCompilerArgs += listOf("-trw") + } + if (konanTarget.family.isAppleFamily) { + return@withType + } compilations["main"].cinterops { create("date") { val cinteropDir = "$projectDir/native/cinterop" val dateLibDir = "${project(":").projectDir}/thirdparty/date" headers("$cinteropDir/public/cdate.h") defFile("native/cinterop/date.def") - // common options - extraOpts("-Xsource-compiler-option", "-std=c++17") extraOpts("-Xsource-compiler-option", "-I$cinteropDir/public") - extraOpts("-Xsource-compiler-option", "-include$cinteropDir/cpp/defines.hpp") - // *nix support - extraOpts("-Xcompile-source", "$dateLibDir/src/tz.cpp") - extraOpts("-Xcompile-source", "$dateLibDir/src/ios.mm") - extraOpts("-Xsource-compiler-option", "-I$dateLibDir/include") - extraOpts("-Xcompile-source", "$cinteropDir/cpp/cdate.cpp") - // iOS support - extraOpts("-Xcompile-source", "$cinteropDir/cpp/apple.mm") - // Windows support - extraOpts("-Xcompile-source", "$cinteropDir/cpp/windows.cpp") + extraOpts("-Xsource-compiler-option", "-DONLY_C_LOCALE=1") + when { + konanTarget.family == org.jetbrains.kotlin.konan.target.Family.LINUX -> { + // needed for the date library so that it does not try to download the timezone database + extraOpts("-Xsource-compiler-option", "-DUSE_OS_TZDB=1") + /* using a more modern C++ version causes the date library to use features that are not + * present in the currently outdated GCC root shipped with Kotlin/Native for Linux. */ + extraOpts("-Xsource-compiler-option", "-std=c++11") + // the date library and its headers + extraOpts("-Xcompile-source", "$dateLibDir/src/tz.cpp") + extraOpts("-Xsource-compiler-option", "-I$dateLibDir/include") + // the main source for the platform bindings. + extraOpts("-Xcompile-source", "$cinteropDir/cpp/cdate.cpp") + } + konanTarget.family == org.jetbrains.kotlin.konan.target.Family.MINGW -> { + // needed to be able to use std::shared_mutex to implement caching. + extraOpts("-Xsource-compiler-option", "-std=c++17") + // the date library headers, needed for some pure calculations. + extraOpts("-Xsource-compiler-option", "-I$dateLibDir/include") + // the main source for the platform bindings. + extraOpts("-Xcompile-source", "$cinteropDir/cpp/windows.cpp") + } + else -> { + throw IllegalArgumentException("Unknown native target ${this@withType}") + } + } } } compilations["main"].defaultSourceSet { kotlin.srcDir("native/cinterop_actuals") } - compilations["test"].kotlinOptions { - freeCompilerArgs += listOf("-trw") - } } diff --git a/core/darwin/src/TimeZoneNative.kt b/core/darwin/src/TimeZoneNative.kt new file mode 100644 index 000000000..ca6d104bf --- /dev/null +++ b/core/darwin/src/TimeZoneNative.kt @@ -0,0 +1,177 @@ +/* + * Copyright 2019-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. + */ + +package kotlinx.datetime + +import platform.Foundation.* + +private fun dateWithTimeIntervalSince1970Saturating(epochSeconds: Long): NSDate { + val date = NSDate.dateWithTimeIntervalSince1970(epochSeconds.toDouble()) + return when { + date.timeIntervalSinceDate(NSDate.distantPast) < 0 -> NSDate.distantPast + date.timeIntervalSinceDate(NSDate.distantFuture) > 0 -> NSDate.distantFuture + else -> date + } +} + +private fun systemDateByLocalDate(zone: NSTimeZone, localDate: NSDate): NSDate? { + val iso8601 = NSCalendar.calendarWithIdentifier(NSCalendarIdentifierISO8601)!! + val utc = NSTimeZone.timeZoneForSecondsFromGMT(0) + /* Now, we say that the date that we initially meant is `date`, only with + the context of being in a timezone `zone`. */ + val dateComponents = iso8601.componentsInTimeZone(utc, localDate) + dateComponents.timeZone = zone + return iso8601.dateFromComponents(dateComponents) +} + +internal actual class PlatformTimeZoneImpl(private val value: NSTimeZone, override val id: String): TimeZoneImpl { + actual companion object { + actual fun of(zoneId: String): PlatformTimeZoneImpl { + val abbreviations = NSTimeZone.abbreviationDictionary + val trueZoneId = abbreviations[zoneId] as String? ?: zoneId + val zone = NSTimeZone.timeZoneWithName(trueZoneId) + ?: throw IllegalTimeZoneException("No timezone found with zone ID '$zoneId'") + return PlatformTimeZoneImpl(zone, zoneId) + } + + actual fun currentSystemDefault(): PlatformTimeZoneImpl { + /* The framework has its own cache of the system timezone. Calls to + [NSTimeZone systemTimeZone] do not reflect changes to the system timezone + and instead just return the cached value. Thus, to acquire the current + system timezone, first, the cache should be cleared. + + This solution is not without flaws, however. In particular, resetting the + system timezone also resets the default timezone ([NSTimeZone default]) if + it's the same as the cached system timezone: + + NSTimeZone.defaultTimeZone = [NSTimeZone + timeZoneWithName: [[NSTimeZone systemTimeZone] name]]; + NSLog(@"%@", NSTimeZone.defaultTimeZone.name); + NSLog(@"Change the system time zone, then press Enter"); + getchar(); + [NSTimeZone resetSystemTimeZone]; + NSLog(@"%@", NSTimeZone.defaultTimeZone.name); // will also change + + This is a fairly marginal problem: + * It is only a problem when the developer deliberately sets the default + timezone to the region that just happens to be the one that the user + is in, and then the user moves to another region, and the app also + uses the system timezone. + * Since iOS 11, the significance of the default timezone has been + de-emphasized. In particular, it is not included in the API for + Swift: https://forums.swift.org/t/autoupdating-type-properties/4608/4 + + Another possible solution could involve using [NSTimeZone localTimeZone]. + This is documented to reflect the current, uncached system timezone on + iOS 11 and later: + https://developer.apple.com/documentation/foundation/nstimezone/1387209-localtimezone + However: + * Before iOS 11, this was the same as the default timezone and did not + reflect the system timezone. + * Worse, on a Mac (10.15.5), I failed to get it to work as documented. + NSLog(@"%@", NSTimeZone.localTimeZone.name); + NSLog(@"Change the system time zone, then press Enter"); + getchar(); + // [NSTimeZone resetSystemTimeZone]; // uncomment to make it work + NSLog(@"%@", NSTimeZone.localTimeZone.name); + The printed strings are the same even if I wait for good 10 minutes + before pressing Enter, unless the line with "reset" is uncommented-- + then the timezone is updated, as it should be. So, for some reason, + NSTimeZone.localTimeZone, too, is cached. + With no iOS device to test this on, it doesn't seem worth the effort + to avoid just resetting the system timezone due to one edge case + that's hard to avoid. + */ + NSTimeZone.resetSystemTimeZone() + val zone = NSTimeZone.systemTimeZone + return PlatformTimeZoneImpl(zone, zone.name) + } + + actual val availableZoneIds: Set + get() { + val set = mutableSetOf("UTC") + val zones = NSTimeZone.knownTimeZoneNames + for (zone in zones) { + if (zone is NSString) { + set.add(zone as String) + } else throw RuntimeException("$zone is expected to be NSString") + } + val abbrevs = NSTimeZone.abbreviationDictionary + for ((key, value) in abbrevs) { + if (key is NSString && value is NSString) { + if (set.contains(value as String)) { + set.add(key as String) + } + } else throw RuntimeException("$key and $value are expected to be NSString") + } + return set + } + } + + override fun atStartOfDay(date: LocalDate): Instant { + val ldt = LocalDateTime(date, LocalTime.MIN) + val epochSeconds = ldt.toEpochSecond(ZoneOffsetImpl.UTC) + // timezone + val nsDate = NSDate.dateWithTimeIntervalSince1970(epochSeconds.toDouble()) + val newDate = systemDateByLocalDate(value, nsDate) + ?: throw RuntimeException("Unable to acquire the time of start of day at $nsDate for zone $this") + val offset = value.secondsFromGMTForDate(newDate).toInt() + /* if `epoch_sec` is not in the range supported by Darwin, assume that it + is the correct local time for the midnight and just convert it to + the system time. */ + if (nsDate.timeIntervalSinceDate(NSDate.distantPast) < 0 || + nsDate.timeIntervalSinceDate(NSDate.distantFuture) > 0) + return Instant(epochSeconds - offset, 0) + // The ISO-8601 calendar. + val iso8601 = NSCalendar.calendarWithIdentifier(NSCalendarIdentifierISO8601)!! + iso8601.timeZone = value + // start of the day denoted by `newDate` + val midnight = iso8601.startOfDayForDate(newDate) + return Instant(midnight.timeIntervalSince1970.toLong(), 0) + } + + override fun LocalDateTime.atZone(preferred: ZoneOffsetImpl?): ZonedDateTime { + val epochSeconds = toEpochSecond(ZoneOffsetImpl.UTC) + var offset = preferred?.totalSeconds ?: Int.MAX_VALUE + val transitionDuration = run { + /* a date in an unspecified timezone, defined by the number of seconds since + the start of the epoch in *that* unspecified timezone */ + val date = dateWithTimeIntervalSince1970Saturating(epochSeconds) + val newDate = systemDateByLocalDate(value, date) + ?: throw RuntimeException("Unable to acquire the offset at ${this@atZone} for zone ${this@PlatformTimeZoneImpl}") + // we now know the offset of that timezone at this time. + offset = value.secondsFromGMTForDate(newDate).toInt() + /* `dateFromComponents` automatically corrects the date to avoid gaps. We + need to learn which adjustments it performed. */ + (newDate.timeIntervalSince1970.toLong() + + offset.toLong() - date.timeIntervalSince1970.toLong()).toInt() + } + val dateTime = try { + this@atZone.plusSeconds(transitionDuration) + } catch (e: IllegalArgumentException) { + throw DateTimeArithmeticException("Overflow whet correcting the date-time to not be in the transition gap", e) + } catch (e: ArithmeticException) { + throw RuntimeException("Anomalously long timezone transition gap reported", e) + } + return ZonedDateTime(dateTime, TimeZone(this@PlatformTimeZoneImpl), ZoneOffset.ofSeconds(offset).offset) + } + + override fun offsetAt(instant: Instant): ZoneOffsetImpl { + val date = dateWithTimeIntervalSince1970Saturating(instant.epochSeconds) + return ZoneOffset.ofSeconds(value.secondsFromGMTForDate(date).toInt()).offset + } + + // org.threeten.bp.ZoneId#equals + override fun equals(other: Any?): Boolean = + this === other || other is PlatformTimeZoneImpl && this.id == other.id + + // org.threeten.bp.ZoneId#hashCode + override fun hashCode(): Int = id.hashCode() + + // org.threeten.bp.ZoneId#toString + override fun toString(): String = id +} + +internal actual fun currentTime(): Instant = NSDate.date().toKotlinInstant() diff --git a/core/darwin/test/ConvertersTest.kt b/core/darwin/test/ConvertersTest.kt index 78fc3cb8f..35a4d144b 100644 --- a/core/darwin/test/ConvertersTest.kt +++ b/core/darwin/test/ConvertersTest.kt @@ -48,7 +48,7 @@ class ConvertersTest { for (id in TimeZone.availableZoneIds) { val normalizedId = (NSTimeZone.abbreviationDictionary[id] ?: id) as String val timeZone = TimeZone.of(normalizedId) - if (timeZone is ZoneOffset) { + if (timeZone.value is ZoneOffsetImpl) { continue } val nsTimeZone = timeZone.toNSTimeZone() diff --git a/core/native/cinterop/cpp/apple.mm b/core/native/cinterop/cpp/apple.mm deleted file mode 100644 index 2ef474a8f..000000000 --- a/core/native/cinterop/cpp/apple.mm +++ /dev/null @@ -1,244 +0,0 @@ -/* - * 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. - */ -/* This file implements the functions specified in `cdate.h` for Apple-based - OS. This is used for iOS, but can also be used with no changes for MacOS. - For now, MacOS uses the implementation based on the `date` library, along - with Linux, and can be found in `cdate.cpp`. */ -#if TARGET_OS_IPHONE // only enable for iOS. -#import -#import -#import -#import -#import -#import -#import -#import -#include "helper_macros.hpp" - -static NSTimeZone * zone_by_name(NSString *zone_name) -{ - auto abbreviations = NSTimeZone.abbreviationDictionary; - auto true_name = [abbreviations valueForKey: zone_name]; - NSString *name = zone_name; - if (true_name != nil) { - name = true_name; - } - return [NSTimeZone timeZoneWithName: name]; -} - -static NSDate * dateWithTimeIntervalSince1970Saturating(int64_t epoch_sec) -{ - auto date = [NSDate dateWithTimeIntervalSince1970: epoch_sec]; - if ([date timeIntervalSinceDate:[NSDate distantPast]] < 0) - date = [NSDate distantPast]; - else if ([date timeIntervalSinceDate:[NSDate distantFuture]] > 0) - date = [NSDate distantFuture]; - return date; -} - -extern "C" { -#include "cdate.h" -} - -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" { - -bool current_time(int64_t *sec, int32_t *nano) -{ - double current = NSDate.date.timeIntervalSince1970; - double dsec; - double dnano = modf(current, &dsec); - *sec = (int64_t)dsec; - *nano = (int32_t)(dnano * 1000000000); - return true; -} - -char * get_system_timezone(TZID *tzid) -{ - /* The framework has its own cache of the system timezone. Calls to - [NSTimeZone systemTimeZone] do not reflect changes to the system timezone - and instead just return the cached value. Thus, to acquire the current - system timezone, first, the cache should be cleared. - - This solution is not without flaws, however. In particular, resetting the - system timezone also resets the default timezone ([NSTimeZone default]) if - it's the same as the cached system timezone: - - NSTimeZone.defaultTimeZone = [NSTimeZone - timeZoneWithName: [[NSTimeZone systemTimeZone] name]]; - NSLog(@"%@", NSTimeZone.defaultTimeZone.name); - NSLog(@"Change the system time zone, then press Enter"); - getchar(); - [NSTimeZone resetSystemTimeZone]; - NSLog(@"%@", NSTimeZone.defaultTimeZone.name); // will also change - - This is a fairly marginal problem: - * It is only a problem when the developer deliberately sets the default - timezone to the region that just happens to be the one that the user - is in, and then the user moves to another region, and the app also - uses the system timezone. - * Since iOS 11, the significance of the default timezone has been - de-emphasized. In particular, it is not included in the API for - Swift: https://forums.swift.org/t/autoupdating-type-properties/4608/4 - - Another possible solution could involve using [NSTimeZone localTimeZone]. - This is documented to reflect the current, uncached system timezone on - iOS 11 and later: - https://developer.apple.com/documentation/foundation/nstimezone/1387209-localtimezone - However: - * Before iOS 11, this was the same as the default timezone and did not - reflect the system timezone. - * Worse, on a Mac (10.15.5), I failed to get it to work as documented. - NSLog(@"%@", NSTimeZone.localTimeZone.name); - NSLog(@"Change the system time zone, then press Enter"); - getchar(); - // [NSTimeZone resetSystemTimeZone]; // uncomment to make it work - NSLog(@"%@", NSTimeZone.localTimeZone.name); - The printed strings are the same even if I wait for good 10 minutes - before pressing Enter, unless the line with "reset" is uncommented-- - then the timezone is updated, as it should be. So, for some reason, - NSTimeZone.localTimeZone, too, is cached. - With no iOS device to test this on, it doesn't seem worth the effort - to avoid just resetting the system timezone due to one edge case - that's hard to avoid. - */ - [NSTimeZone resetSystemTimeZone]; - NSTimeZone *zone = [NSTimeZone systemTimeZone]; - NSString *name = [zone name]; - *tzid = id_by_name(name); - return strdup([name UTF8String]); -} - -char ** available_zone_ids() -{ - std::set ids; - auto zones = NSTimeZone.knownTimeZoneNames; - for (NSString * zone in zones) { - ids.insert(std::string([zone UTF8String])); - } - auto abbrevs = NSTimeZone.abbreviationDictionary; - for (NSString * key in abbrevs) { - if (ids.count(std::string([abbrevs[key] UTF8String]))) { - ids.insert(std::string([key UTF8String])); - } - } - char ** zones_copy = check_allocation( - (char **)malloc(sizeof(char *) * (ids.size() + 1))); - zones_copy[ids.size()] = nullptr; - unsigned long i = 0; - for (auto it = ids.begin(); it != ids.end(); ++i, ++it) { - zones_copy[i] = check_allocation(strdup(it->c_str())); - } - return zones_copy; -} - -int offset_at_instant(TZID zone_id, int64_t epoch_sec) -{ - auto zone = timezone_by_id(zone_id); - if (zone == nil) { return INT_MAX; } - auto date = dateWithTimeIntervalSince1970Saturating(epoch_sec); - return (int32_t)[zone secondsFromGMTForDate: date]; -} - -TZID timezone_by_name(const char *zone_name) { - return id_by_name([NSString stringWithUTF8String: zone_name]); -} - -static NSDate *system_date_by_local_date(NSTimeZone *zone, NSDate *local_date) { - // The Gregorian calendar. - NSCalendar *iso8601 = [NSCalendar - calendarWithIdentifier: NSCalendarIdentifierISO8601]; - if (iso8601 == nil) { return nil; } - // The UTC time zone - NSTimeZone *utc = [NSTimeZone timeZoneForSecondsFromGMT: 0]; - /* Now, we say that the date that we initially meant is `date`, only with - the context of being in a timezone `zone`. */ - NSDateComponents *dateComponents = [iso8601 - componentsInTimeZone: utc - fromDate: local_date]; - dateComponents.timeZone = zone; - return [iso8601 dateFromComponents:dateComponents]; -} - -int offset_at_datetime(TZID zone_id, int64_t epoch_sec, int *offset) { - *offset = INT_MAX; - // timezone - 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 */ - NSDate *date = dateWithTimeIntervalSince1970Saturating(epoch_sec); - NSDate *newDate = system_date_by_local_date(zone, date); - if (newDate == nil) { return 0; } - // we now know the offset of that timezone at this time. - *offset = (int)[zone secondsFromGMTForDate: newDate]; - /* `dateFromComponents` automatically corrects the date to avoid gaps. We - need to learn which adjustments it performed. */ - int result = (int)((int64_t)[newDate timeIntervalSince1970] + - (int64_t)*offset - (int64_t)[date timeIntervalSince1970]); - return result; -} - -int64_t at_start_of_day(TZID zone_id, int64_t epoch_sec) { - // timezone - auto zone = timezone_by_id(zone_id); - if (zone == nil) { return INT_MAX; } - NSDate *date = [NSDate dateWithTimeIntervalSince1970: epoch_sec]; - NSDate *newDate = system_date_by_local_date(zone, date); - if (newDate == nil) { return INT_MAX; } - int offset = (int)[zone secondsFromGMTForDate: newDate]; - /* if `epoch_sec` is not in the range supported by Darwin, assume that it - is the correct local time for the midnight and just convert it to - the system time. */ - if ([date timeIntervalSinceDate:[NSDate distantPast]] < 0 || - [date timeIntervalSinceDate:[NSDate distantFuture]] > 0) - return epoch_sec - offset; - // The ISO-8601 calendar. - NSCalendar *iso8601 = [NSCalendar - calendarWithIdentifier: NSCalendarIdentifierISO8601]; - iso8601.timeZone = zone; - // start of the day denoted by `newDate` - NSDate *midnight = [iso8601 startOfDayForDate: newDate]; - return (int64_t)([midnight timeIntervalSince1970]); -} - -} -#endif // TARGET_OS_IPHONE diff --git a/core/native/cinterop/cpp/cdate.cpp b/core/native/cinterop/cpp/cdate.cpp index b663ca239..2e1e70014 100644 --- a/core/native/cinterop/cpp/cdate.cpp +++ b/core/native/cinterop/cpp/cdate.cpp @@ -7,8 +7,6 @@ library. This implementation is used for MacOS and Linux, but once is available for all the target platforms, the dependency on `date` can be removed along with the neighboring implementations. */ -#if !TARGET_OS_IPHONE -#if !DATETIME_TARGET_WIN32 #include "date/date.h" #include "date/tz.h" #include "helper_macros.hpp" @@ -210,5 +208,3 @@ int64_t at_start_of_day(TZID zone_id, int64_t epoch_sec) } } -#endif // !DATETIME_TARGET_WIN32 -#endif // !TARGET_OS_IPHONE diff --git a/core/native/cinterop/cpp/defines.hpp b/core/native/cinterop/cpp/defines.hpp deleted file mode 100644 index 2907ab570..000000000 --- a/core/native/cinterop/cpp/defines.hpp +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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. - */ -/* This file is implicitly included at the beginning of every other .cpp file - in the project with the `-include` flag to the compiler. It is used to - define platform-specific constants and bindings, since it is seemingly - difficult to run `cinterop` with different flags depending on the platform. -*/ -#define AUTO_DOWNLOAD 0 -#define HAS_REMOTE_API 0 -#define ONLY_C_LOCALE 1 - -#if __APPLE__ - // Needed to set TARGET_OS_IPHONE - #include -#endif - -#if TARGET_OS_IPHONE - #define DATETIME_TARGET_WIN32 0 -#else - #define TARGET_OS_IPHONE 0 - #ifdef _WIN32 - #define DATETIME_TARGET_WIN32 1 - #else - /* A very dangerous action. This is needed so that we can use C++17 to - have `std::shared_mutex` in the Windows implementation; however, this - has the unfortunate side effect of the `date` library recognizing that - it deals with C++17, which has a lot of additional features compared to - C++11. However, many of these features are actually unavailable due to - an outdated GCC root used for Linux. */ - #undef __cplusplus - #define __cplusplus 201103 - #define DATETIME_TARGET_WIN32 0 - #endif -#endif - -#if DATETIME_TARGET_WIN32 - #define USE_OS_TZDB 0 -#else - #define USE_OS_TZDB 1 -#endif diff --git a/core/native/cinterop/cpp/windows.cpp b/core/native/cinterop/cpp/windows.cpp index f978a3d44..9bfa2fe1b 100644 --- a/core/native/cinterop/cpp/windows.cpp +++ b/core/native/cinterop/cpp/windows.cpp @@ -3,7 +3,6 @@ * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. */ /* This file implements the functions specified in `cdate.h` for Windows. */ -#if DATETIME_TARGET_WIN32 /* only Windows 8 and later is supported. This is needed for `EnumDynamicTimeZoneInformation` to be available. */ #define _WIN32_WINNT _WIN32_WINNT_WIN8 @@ -436,4 +435,3 @@ int64_t at_start_of_day(TZID zone_id, int64_t epoch_sec) } } -#endif // DATETIME_TARGET_WIN32 diff --git a/core/native/cinterop_actuals/TimeZoneNative.kt b/core/native/cinterop_actuals/TimeZoneNative.kt index 368d4b3f3..a958622f2 100644 --- a/core/native/cinterop_actuals/TimeZoneNative.kt +++ b/core/native/cinterop_actuals/TimeZoneNative.kt @@ -4,35 +4,103 @@ */ package kotlinx.datetime +import kotlinx.datetime.internal.* import kotlinx.cinterop.* import platform.posix.free -internal actual fun getCurrentSystemDefaultTimeZone(): TimeZone = memScoped { - val tzid = alloc() - val string = kotlinx.datetime.internal.get_system_timezone(tzid.ptr) - ?: throw RuntimeException("Failed to get the system timezone.") - val kotlinString = string.toKString() - free(string) - TimeZone(tzid.value, kotlinString) -} +internal actual class PlatformTimeZoneImpl(private val tzid: TZID, override val id: String): TimeZoneImpl { + actual companion object { + actual fun of(zoneId: String): PlatformTimeZoneImpl { + val tzid = timezone_by_name(zoneId) + if (tzid == TZID_INVALID) { + throw IllegalTimeZoneException("No timezone found with zone ID '$zoneId'") + } + return PlatformTimeZoneImpl(tzid, zoneId) + } + + actual fun currentSystemDefault(): PlatformTimeZoneImpl = 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) + PlatformTimeZoneImpl(tzid.value, kotlinString) + } -internal actual fun current_time(sec: kotlinx.cinterop.CValuesRef */>?, nano: kotlinx.cinterop.CValuesRef?): kotlin.Boolean = - kotlinx.datetime.internal.current_time(sec, nano) + actual val availableZoneIds: Set + get() { + val set = mutableSetOf("UTC") + val zones = available_zone_ids() + ?: throw RuntimeException("Failed to get the list of available timezones") + var ptr = zones + while (true) { + val cur = ptr.pointed.value ?: break + val zoneName = cur.toKString() + set.add(zoneName) + free(cur) + ptr = (ptr + 1)!! + } + free(zones) + return set + } + } -internal actual fun available_zone_ids(): kotlinx.cinterop.CPointer>? = - kotlinx.datetime.internal.available_zone_ids() + override fun atStartOfDay(date: LocalDate): Instant = memScoped { + val ldt = LocalDateTime(date, LocalTime.MIN) + val epochSeconds = ldt.toEpochSecond(ZoneOffsetImpl.UTC) + val midnightInstantSeconds = at_start_of_day(tzid, epochSeconds) + if (midnightInstantSeconds == Long.MAX_VALUE) { + throw RuntimeException("Unable to acquire the time of start of day at $date for zone $this") + } + Instant(midnightInstantSeconds, 0) + } -internal actual fun offset_at_datetime(zone: kotlinx.datetime.TZID /* = kotlin.ULong */, epoch_sec: platform.posix.int64_t /* = kotlin.Long */, offset: kotlinx.cinterop.CValuesRef */>?): kotlin.Int = - kotlinx.datetime.internal.offset_at_datetime(zone, epoch_sec, offset) + override fun LocalDateTime.atZone(preferred: ZoneOffsetImpl?): ZonedDateTime = memScoped { + val epochSeconds = toEpochSecond(ZoneOffsetImpl.UTC) + val offset = alloc() + offset.value = preferred?.totalSeconds ?: Int.MAX_VALUE + val transitionDuration = offset_at_datetime(tzid, epochSeconds, offset.ptr) + if (offset.value == Int.MAX_VALUE) { + throw RuntimeException("Unable to acquire the offset at ${this@atZone} for zone ${this@PlatformTimeZoneImpl}") + } + val dateTime = try { + this@atZone.plusSeconds(transitionDuration) + } catch (e: IllegalArgumentException) { + throw DateTimeArithmeticException("Overflow whet correcting the date-time to not be in the transition gap", e) + } catch (e: ArithmeticException) { + throw RuntimeException("Anomalously long timezone transition gap reported", e) + } + ZonedDateTime(dateTime, TimeZone(this@PlatformTimeZoneImpl), ZoneOffset.ofSeconds(offset.value).offset) + } -internal actual fun at_start_of_day(zone: kotlinx.datetime.TZID /* = kotlin.ULong */, epoch_sec: platform.posix.int64_t /* = kotlin.Long */): kotlin.Long = - kotlinx.datetime.internal.at_start_of_day(zone, epoch_sec) + override fun offsetAt(instant: Instant): ZoneOffsetImpl { + val offset = offset_at_instant(tzid, instant.epochSeconds) + if (offset == Int.MAX_VALUE) { + throw RuntimeException("Unable to acquire the offset at instant $instant for zone $this") + } + return ZoneOffset.ofSeconds(offset).offset + } -internal actual fun offset_at_instant(zone: kotlinx.datetime.TZID /* = kotlin.ULong */, epoch_sec: platform.posix.int64_t /* = kotlin.Long */): kotlin.Int = - kotlinx.datetime.internal.offset_at_instant(zone, epoch_sec) + // org.threeten.bp.ZoneId#equals + override fun equals(other: Any?): Boolean = + this === other || other is PlatformTimeZoneImpl && this.id == other.id -internal actual fun timezone_by_name(zone_name: kotlin.String?): kotlinx.datetime.TZID /* = kotlin.ULong */ = - kotlinx.datetime.internal.timezone_by_name(zone_name) + // org.threeten.bp.ZoneId#hashCode + override fun hashCode(): Int = id.hashCode() + + // org.threeten.bp.ZoneId#toString + override fun toString(): String = id +} -internal actual val TZID_INVALID: TZID - get() = kotlinx.datetime.internal.TZID_INVALID +internal actual fun currentTime(): Instant = memScoped { + val seconds = alloc() + val nanoseconds = alloc() + val result = current_time(seconds.ptr, nanoseconds.ptr) + try { + require(result) + require(nanoseconds.value >= 0 && nanoseconds.value < NANOS_PER_ONE) + Instant(seconds.value, nanoseconds.value) + } catch (e: IllegalArgumentException) { + throw IllegalStateException("The readings from the system clock are not representable as an Instant") + } +} \ No newline at end of file diff --git a/core/native/src/Instant.kt b/core/native/src/Instant.kt index adf8015de..e0011eb9e 100644 --- a/core/native/src/Instant.kt +++ b/core/native/src/Instant.kt @@ -8,8 +8,6 @@ package kotlinx.datetime -import kotlinx.cinterop.* -import platform.posix.* import kotlin.math.* import kotlin.time.* @@ -83,6 +81,8 @@ private const val MAX_SECOND = 31494816403199L // +1000000-12-31T23:59:59 private fun isValidInstantSecond(second: Long) = second >= MIN_SECOND && second <= MAX_SECOND +internal expect fun currentTime(): Instant + @OptIn(ExperimentalTime::class) public actual class Instant internal constructor(actual val epochSeconds: Long, actual val nanosecondsOfSecond: Int) : Comparable { @@ -208,18 +208,7 @@ public actual class Instant internal constructor(actual val epochSeconds: Long, internal actual val MAX = Instant(MAX_SECOND, 999_999_999) @Deprecated("Use Clock.System.now() instead", ReplaceWith("Clock.System.now()", "kotlinx.datetime.Clock"), level = DeprecationLevel.ERROR) - actual fun now(): Instant = memScoped { - val seconds = alloc() - val nanoseconds = alloc() - val result = current_time(seconds.ptr, nanoseconds.ptr) - try { - require(result) - require(nanoseconds.value >= 0 && nanoseconds.value < NANOS_PER_ONE) - Instant(seconds.value, nanoseconds.value) - } catch (e: IllegalArgumentException) { - throw IllegalStateException("The readings from the system clock are not representable as an Instant") - } - } + actual fun now(): Instant = currentTime() // org.threeten.bp.Instant#ofEpochMilli actual fun fromEpochMilliseconds(epochMilliseconds: Long): Instant = diff --git a/core/native/src/LocalDateTime.kt b/core/native/src/LocalDateTime.kt index 751918633..b2677392a 100644 --- a/core/native/src/LocalDateTime.kt +++ b/core/native/src/LocalDateTime.kt @@ -66,7 +66,7 @@ public actual class LocalDateTime internal constructor( actual override fun toString(): String = date.toString() + 'T' + time.toString() // org.threeten.bp.chrono.ChronoLocalDateTime#toEpochSecond - internal fun toEpochSecond(offset: ZoneOffset): Long { + internal fun toEpochSecond(offset: ZoneOffsetImpl): Long { val epochDay = date.toEpochDay().toLong() var secs: Long = epochDay * 86400 + time.toSecondOfDay() secs -= offset.totalSeconds diff --git a/core/native/src/TimeZone.kt b/core/native/src/TimeZone.kt index 8baed97a2..cb37ada2f 100644 --- a/core/native/src/TimeZone.kt +++ b/core/native/src/TimeZone.kt @@ -9,26 +9,13 @@ package kotlinx.datetime import kotlin.math.abs -import kotlinx.cinterop.* -import platform.posix.* import kotlin.native.concurrent.* -internal expect fun getCurrentSystemDefaultTimeZone(): TimeZone - -internal typealias TZID = platform.posix.size_t -internal expect val TZID_INVALID: TZID -internal expect fun available_zone_ids(): kotlinx.cinterop.CPointer>? -internal expect fun offset_at_datetime(zone: kotlinx.datetime.TZID /* = kotlin.ULong */, epoch_sec: platform.posix.int64_t /* = kotlin.Long */, offset: kotlinx.cinterop.CValuesRef */>?): kotlin.Int -internal expect fun at_start_of_day(zone: kotlinx.datetime.TZID /* = kotlin.ULong */, epoch_sec: platform.posix.int64_t /* = kotlin.Long */): kotlin.Long -internal expect fun offset_at_instant(zone: kotlinx.datetime.TZID /* = kotlin.ULong */, epoch_sec: platform.posix.int64_t /* = kotlin.Long */): kotlin.Int -internal expect fun timezone_by_name(zone_name: kotlin.String?): kotlinx.datetime.TZID /* = kotlin.ULong */ -internal expect fun current_time(sec: kotlinx.cinterop.CValuesRef */>?, nano: kotlinx.cinterop.CValuesRef?): kotlin.Boolean - -public actual open class TimeZone internal constructor(private val tzid: TZID, actual val id: String) { +public actual open class TimeZone internal constructor(internal val value: TimeZoneImpl) { actual companion object { - actual fun currentSystemDefault(): TimeZone = getCurrentSystemDefaultTimeZone() + actual fun currentSystemDefault(): TimeZone = PlatformTimeZoneImpl.currentSystemDefault().let(::TimeZone) actual val UTC: TimeZone = ZoneOffset.UTC @@ -45,110 +32,59 @@ public actual open class TimeZone internal constructor(private val tzid: TZID, a return ZoneOffset.of(zoneId) } if (zoneId == "UTC" || zoneId == "GMT" || zoneId == "UT") { - return ZoneOffset(0, zoneId) + return TimeZone(ZoneOffsetImpl(0, zoneId)) } if (zoneId.startsWith("UTC+") || zoneId.startsWith("GMT+") || zoneId.startsWith("UTC-") || zoneId.startsWith("GMT-")) { val offset = ZoneOffset.of(zoneId.substring(3)) - return if (offset.totalSeconds == 0) { - ZoneOffset(0, zoneId.substring(0, 3)) - } else ZoneOffset(offset.totalSeconds, zoneId.substring(0, 3) + offset.id) + return (if (offset.totalSeconds == 0) ZoneOffsetImpl(0, zoneId.substring(0, 3)) + else ZoneOffsetImpl(offset.totalSeconds, zoneId.substring(0, 3) + offset.id)).let(::TimeZone) } if (zoneId.startsWith("UT+") || zoneId.startsWith("UT-")) { val offset = ZoneOffset.of(zoneId.substring(2)) - return if (offset.totalSeconds == 0) { - ZoneOffset(0, "UT") - } else ZoneOffset(offset.totalSeconds, "UT" + offset.id) - } - val tzid = timezone_by_name(zoneId) - if (tzid == TZID_INVALID) { - throw IllegalTimeZoneException("No timezone found with zone ID '$zoneId'") + return (if (offset.totalSeconds == 0) ZoneOffsetImpl(0, "UT") + else ZoneOffsetImpl(offset.totalSeconds, "UT" + offset.id)).let(::TimeZone) } - return TimeZone(tzid, zoneId) + return TimeZone(PlatformTimeZoneImpl.of(zoneId)) } actual val availableZoneIds: Set - get() { - val set = mutableSetOf("UTC") - val zones = available_zone_ids() - ?: throw RuntimeException("Failed to get the list of available timezones") - var ptr = zones - while (true) { - val cur = ptr.pointed.value ?: break - val zoneName = cur.toKString() - set.add(zoneName) - free(cur) - ptr = (ptr + 1)!! - } - free(zones) - return set - } + get() = PlatformTimeZoneImpl.availableZoneIds } + actual val id + get() = value.id + actual fun Instant.toLocalDateTime(): LocalDateTime = try { toZonedLocalDateTime(this@TimeZone).dateTime } catch (e: IllegalArgumentException) { throw DateTimeArithmeticException("Instant ${this@toLocalDateTime} is not representable as LocalDateTime", e) } - internal open fun offsetAtImpl(instant: Instant): ZoneOffset { - val offset = offset_at_instant(tzid, instant.epochSeconds) - if (offset == Int.MAX_VALUE) { - throw RuntimeException("Unable to acquire the offset at instant $instant for zone $this") - } - return ZoneOffset.ofSeconds(offset) - } - actual fun LocalDateTime.toInstant(): Instant = atZone().toInstant() - internal open fun atStartOfDay(date: LocalDate): Instant = memScoped { - val ldt = LocalDateTime(date, LocalTime.MIN) - val epochSeconds = ldt.toEpochSecond(ZoneOffset.UTC) - val midnightInstantSeconds = at_start_of_day(tzid, epochSeconds) - if (midnightInstantSeconds == Long.MAX_VALUE) { - throw RuntimeException("Unable to acquire the time of start of day at $date for zone $this") - } - Instant(midnightInstantSeconds, 0) - } + internal open fun atStartOfDay(date: LocalDate): Instant = value.atStartOfDay(date) - internal open fun LocalDateTime.atZone(preferred: ZoneOffset? = null): ZonedDateTime = memScoped { - val epochSeconds = toEpochSecond(ZoneOffset.UTC) - val offset = alloc() - offset.value = preferred?.totalSeconds ?: Int.MAX_VALUE - val transitionDuration = offset_at_datetime(tzid, epochSeconds, offset.ptr) - if (offset.value == Int.MAX_VALUE) { - throw RuntimeException("Unable to acquire the offset at ${this@atZone} for zone ${this@TimeZone}") - } - val dateTime = try { - this@atZone.plusSeconds(transitionDuration) - } catch (e: IllegalArgumentException) { - throw DateTimeArithmeticException("Overflow whet correcting the date-time to not be in the transition gap", e) - } catch (e: ArithmeticException) { - throw RuntimeException("Anomalously long timezone transition gap reported", e) - } - ZonedDateTime(dateTime, this@TimeZone, ZoneOffset.ofSeconds(offset.value)) - } + internal open fun LocalDateTime.atZone(preferred: ZoneOffsetImpl? = null): ZonedDateTime = + with(value) { atZone(preferred) } - // org.threeten.bp.ZoneId#equals override fun equals(other: Any?): Boolean = - this === other || other is TimeZone && this.id == other.id - - // org.threeten.bp.ZoneId#hashCode - override fun hashCode(): Int = id.hashCode() + this === other || other is TimeZone && this.value == other.value - // org.threeten.bp.ZoneId#toString - override fun toString(): String = id + override fun hashCode(): Int = value.hashCode() + override fun toString(): String = value.toString() } @ThreadLocal private var zoneOffsetCache: MutableMap = mutableMapOf() -public actual class ZoneOffset internal constructor(actual val totalSeconds: Int, id: String) : TimeZone(TZID_INVALID, id) { +public actual class ZoneOffset internal constructor(internal val offset: ZoneOffsetImpl) : TimeZone(offset) { + + actual val totalSeconds get() = offset.totalSeconds companion object { - // org.threeten.bp.ZoneOffset#UTC - val UTC = ZoneOffset(0, "Z") + val UTC = ZoneOffset(ZoneOffsetImpl.UTC) // org.threeten.bp.ZoneOffset#of internal fun of(offsetId: String): ZoneOffset { @@ -242,9 +178,9 @@ public actual class ZoneOffset internal constructor(actual val totalSeconds: Int internal fun ofSeconds(seconds: Int): ZoneOffset = if (seconds % (15 * SECONDS_PER_MINUTE) == 0) { zoneOffsetCache[seconds] ?: - ZoneOffset(seconds, zoneIdByOffset(seconds)).also { zoneOffsetCache[seconds] = it } + ZoneOffset(ZoneOffsetImpl(seconds, zoneIdByOffset(seconds))).also { zoneOffsetCache[seconds] = it } } else { - ZoneOffset(seconds, zoneIdByOffset(seconds)) + ZoneOffset(ZoneOffsetImpl(seconds, zoneIdByOffset(seconds))) } // org.threeten.bp.ZoneOffset#parseNumber @@ -260,28 +196,10 @@ public actual class ZoneOffset internal constructor(actual val totalSeconds: Int return (ch1.toInt() - 48) * 10 + (ch2.toInt() - 48) } } - - internal override fun atStartOfDay(date: LocalDate): Instant = - LocalDateTime(date, LocalTime.MIN).atZone(null).toInstant() - - internal override fun LocalDateTime.atZone(preferred: ZoneOffset?): ZonedDateTime = - ZonedDateTime(this@atZone, this@ZoneOffset, this@ZoneOffset) - - override fun offsetAtImpl(instant: Instant): ZoneOffset = this - - // org.threeten.bp.ZoneOffset#toString - override fun toString(): String = id - - // org.threeten.bp.ZoneOffset#hashCode - override fun hashCode(): Int = totalSeconds - - // org.threeten.bp.ZoneOffset#equals - override fun equals(other: Any?): Boolean = - this === other || other is ZoneOffset && totalSeconds == other.totalSeconds } public actual fun TimeZone.offsetAt(instant: Instant): ZoneOffset = - offsetAtImpl(instant) + value.offsetAt(instant).let(::ZoneOffset) public actual fun Instant.toLocalDateTime(timeZone: TimeZone): LocalDateTime = with(timeZone) { toLocalDateTime() } diff --git a/core/native/src/TimeZoneImpl.kt b/core/native/src/TimeZoneImpl.kt new file mode 100644 index 000000000..66354ebff --- /dev/null +++ b/core/native/src/TimeZoneImpl.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019-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. + */ +package kotlinx.datetime + +internal interface TimeZoneImpl { + val id: String + fun atStartOfDay(date: LocalDate): Instant + fun LocalDateTime.atZone(preferred: ZoneOffsetImpl?): ZonedDateTime + fun offsetAt(instant: Instant): ZoneOffsetImpl +} + +internal expect class PlatformTimeZoneImpl: TimeZoneImpl { + companion object { + fun of(zoneId: String): PlatformTimeZoneImpl + fun currentSystemDefault(): PlatformTimeZoneImpl + val availableZoneIds: Set + } +} + +internal class ZoneOffsetImpl(val totalSeconds: Int, override val id: String): TimeZoneImpl { + + companion object { + // org.threeten.bp.ZoneOffset#UTC + val UTC = ZoneOffsetImpl(0, "Z") + } + + override fun atStartOfDay(date: LocalDate): Instant = + LocalDateTime(date, LocalTime.MIN).atZone(null).toInstant() + + override fun LocalDateTime.atZone(preferred: ZoneOffsetImpl?): ZonedDateTime { + return ZonedDateTime(this@atZone, ZoneOffset(this@ZoneOffsetImpl), this@ZoneOffsetImpl) + } + + override fun offsetAt(instant: Instant): ZoneOffsetImpl = this + + // org.threeten.bp.ZoneOffset#toString + override fun toString(): String = id + + // org.threeten.bp.ZoneOffset#hashCode + override fun hashCode(): Int = totalSeconds + + // org.threeten.bp.ZoneOffset#equals + override fun equals(other: Any?): Boolean = + this === other || other is ZoneOffsetImpl && totalSeconds == other.totalSeconds +} \ No newline at end of file diff --git a/core/native/src/ZonedDateTime.kt b/core/native/src/ZonedDateTime.kt index a0aef9ae6..86b40de6a 100644 --- a/core/native/src/ZonedDateTime.kt +++ b/core/native/src/ZonedDateTime.kt @@ -8,7 +8,7 @@ package kotlinx.datetime -internal class ZonedDateTime(val dateTime: LocalDateTime, private val zone: TimeZone, val offset: ZoneOffset) { +internal class ZonedDateTime(val dateTime: LocalDateTime, private val zone: TimeZone, val offset: ZoneOffsetImpl) { /** * @throws IllegalArgumentException if the result exceeds the boundaries * @throws ArithmeticException if arithmetic overflow occurs @@ -18,7 +18,7 @@ internal class ZonedDateTime(val dateTime: LocalDateTime, private val zone: Time // Never throws in practice private fun LocalDateTime.resolve(): ZonedDateTime = // workaround for https://github.com/Kotlin/kotlinx-datetime/issues/51 - if (toInstant(offset).toLocalDateTime(zone) == this@resolve) { + if (with(offset) { atZone(null).toInstant() }.toLocalDateTime(zone) == this@resolve) { // this LocalDateTime is valid in these timezone and offset. ZonedDateTime(this, zone, offset) } else { @@ -38,7 +38,7 @@ internal class ZonedDateTime(val dateTime: LocalDateTime, private val zone: Time override fun toString(): String { var str = dateTime.toString() + offset.toString() - if (offset !== zone) { + if (offset !== zone.value) { str += "[$zone]" } return str @@ -53,7 +53,7 @@ internal fun ZonedDateTime.toInstant(): Instant = * @throws IllegalArgumentException if the [Instant] exceeds the boundaries of [LocalDateTime] */ internal fun Instant.toZonedLocalDateTime(zone: TimeZone): ZonedDateTime { - val currentOffset = offsetIn(zone) + val currentOffset = zone.value.offsetAt(this) val localSecond: Long = epochSeconds + currentOffset.totalSeconds // overflow caught later val localEpochDay = floorDiv(localSecond, SECONDS_PER_DAY.toLong()).toInt() val secsOfDay = floorMod(localSecond, SECONDS_PER_DAY.toLong()).toInt() diff --git a/core/native/test/ThreeTenBpLocalDateTimeTest.kt b/core/native/test/ThreeTenBpLocalDateTimeTest.kt index c50a82ec2..8c423c7ba 100644 --- a/core/native/test/ThreeTenBpLocalDateTimeTest.kt +++ b/core/native/test/ThreeTenBpLocalDateTimeTest.kt @@ -18,7 +18,7 @@ class ThreeTenBpLocalDateTimeTest { val offset = ZoneOffset.ofSeconds(iHours) for (j in 0..99999) { val a = LocalDateTime(1970, 1, 1, 0, 0, 0, 0).plusSeconds(j) - assertEquals((j - iHours).toLong(), a.toEpochSecond(offset)) + assertEquals((j - iHours).toLong(), a.toEpochSecond(offset.offset)) } } } @@ -27,7 +27,7 @@ class ThreeTenBpLocalDateTimeTest { fun toSecondsBeforeEpoch() { for (i in 0..99999) { val a = LocalDateTime(1970, 1, 1, 0, 0, 0, 0).plusSeconds(-i) - assertEquals(-i.toLong(), a.toEpochSecond(ZoneOffset.UTC)) + assertEquals(-i.toLong(), a.toEpochSecond(ZoneOffsetImpl.UTC)) } } diff --git a/core/native/test/ThreeTenBpTimeZoneTest.kt b/core/native/test/ThreeTenBpTimeZoneTest.kt index e23c3ad03..a6cf02056 100644 --- a/core/native/test/ThreeTenBpTimeZoneTest.kt +++ b/core/native/test/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.ofSeconds(2 * 3600)), with(zone) { t.atZone() }) + zone, ZoneOffset.ofSeconds(2 * 3600).offset), with(zone) { t.atZone() }) } }