Skip to content

Commit 6c22ce6

Browse files
authored
Native (Apple): Provide integrations with the native datetime (#10)
For now, these are provided as a separate artifact. This is cumbersome for the end user, but currently, since there is no ability yet to specify common native code, we would have to duplicate much between every available platform just to provide some special behavior on Darwin-based devices. There is a clear migration path once common native code becomes possible, so the problem is not significant.
1 parent ef67879 commit 6c22ce6

File tree

6 files changed

+229
-5
lines changed

6 files changed

+229
-5
lines changed

build.gradle.kts

+5
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,18 @@ project(":kotlinx-datetime") {
1717
// pluginManager.apply("maven-publish")
1818
}
1919

20+
project(":kotlinx-datetime-darwin") {
21+
pluginManager.apply("kotlin-multiplatform")
22+
}
23+
2024
infra {
2125
teamcity {
2226
bintrayUser = "%env.BINTRAY_USER%"
2327
bintrayToken = "%env.BINTRAY_API_KEY%"
2428
}
2529
publishing {
2630
include(":kotlinx-datetime")
31+
include(":kotlinx-datetime-darwin")
2732

2833
bintray {
2934
organization = "kotlin"

core/build.gradle.kts

+5-5
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,6 @@ val JDK_8: String by project
2525
// }
2626
//}
2727

28-
tasks.withType<org.jetbrains.kotlin.gradle.tasks.CInteropProcess> {
29-
// dependsOn(":date-cpp-c-wrapper:assembleRelease")
30-
// dependsOn(":date-cpp-library:assembleRelease")
31-
}
32-
3328
kotlin {
3429
infra {
3530
target("macosX64")
@@ -38,6 +33,11 @@ kotlin {
3833
target("iosArm32")
3934
target("linuxX64")
4035
target("mingwX64")
36+
target("watchosArm32")
37+
target("watchosArm64")
38+
target("watchosX86")
39+
target("tvosArm64")
40+
target("tvosX64")
4141
}
4242

4343
jvm {

darwin-integration/build.gradle.kts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
plugins {
2+
id("kotlin-multiplatform")
3+
}
4+
5+
kotlin {
6+
infra {
7+
target("macosX64")
8+
target("iosX64")
9+
target("iosArm64")
10+
target("iosArm32")
11+
target("watchosArm32")
12+
target("watchosArm64")
13+
target("watchosX86")
14+
target("tvosArm64")
15+
target("tvosX64")
16+
}
17+
18+
sourceSets.all {
19+
kotlin.setSrcDirs(listOf("$name/src"))
20+
}
21+
22+
sourceSets {
23+
val nativeMain by getting {
24+
dependencies {
25+
implementation(project(":kotlinx-datetime"))
26+
}
27+
}
28+
val nativeTest by getting {
29+
}
30+
}
31+
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright 2019-2020 JetBrains s.r.o.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package kotlinx.datetime
7+
import kotlinx.cinterop.*
8+
import platform.Foundation.*
9+
10+
/**
11+
* Converts the [Instant] to an instance of [NSDate].
12+
*
13+
* The conversion is lossy: Darwin uses millisecond precision to represent dates, and [Instant] allows for nanosecond
14+
* resolution.
15+
*/
16+
public fun Instant.toNSDate(): NSDate {
17+
val secs = epochSeconds * 1.0 + nanosecondsOfSecond / 1.0e9
18+
if (secs < NSDate.distantPast.timeIntervalSince1970 || secs > NSDate.distantFuture.timeIntervalSince1970) {
19+
throw DateTimeException("Boundaries of NSDate exceeded")
20+
}
21+
return NSDate.dateWithTimeIntervalSince1970(secs)
22+
}
23+
24+
/**
25+
* Converts the [NSDate] to the corresponding [Instant].
26+
*
27+
* Even though Darwin only uses millisecond precision, it is possible that [date] uses larger resolution, storing
28+
* microseconds or even nanoseconds. In this case, the sub-millisecond parts of [date] are rounded to the nearest
29+
* millisecond, given that they are likely to be conversion artifacts.
30+
*/
31+
public fun NSDate.toKotlinInstant(): Instant {
32+
val secs = timeIntervalSince1970()
33+
val millis = secs * 1000 + if (secs > 0) 0.5 else -0.5
34+
return Instant.fromEpochMilliseconds(millis.toLong())
35+
}
36+
37+
/**
38+
* Converts the [TimeZone] to [NSTimeZone].
39+
*
40+
* If the time zone is represented as a fixed number of seconds from UTC+0 (for example, if it is the result of a call
41+
* to [TimeZone.offset]) and the offset is not given in even minutes but also includes seconds, this method throws
42+
* [DateTimeException] to denote that lossy conversion would happen, as Darwin internally rounds the offsets to the
43+
* nearest minute.
44+
*/
45+
public fun TimeZone.toNSTimeZone(): NSTimeZone = if (this is ZoneOffset) {
46+
if (totalSeconds % 60 == 0) {
47+
NSTimeZone.timeZoneForSecondsFromGMT(totalSeconds.convert())
48+
} else {
49+
throw DateTimeException("Lossy conversion: Darwin uses minute precision for fixed-offset time zones")
50+
}
51+
} else {
52+
NSTimeZone.timeZoneWithName(id) ?: NSTimeZone.timeZoneWithAbbreviation(id)!!
53+
}
54+
55+
/**
56+
* Converts the [NSTimeZone] to the corresponding [TimeZone].
57+
*/
58+
public fun NSTimeZone.toKotlinTimeZone(): TimeZone = TimeZone.of(name)
59+
60+
/**
61+
* Converts the given [LocalDate] to [NSDateComponents].
62+
*
63+
* Of all the fields, only the bare minimum required for uniquely identifying the date are set.
64+
*/
65+
public fun LocalDate.toNSDateComponents(): NSDateComponents {
66+
val components = NSDateComponents()
67+
components.year = year.convert()
68+
components.month = monthNumber.convert()
69+
components.day = dayOfMonth.convert()
70+
return components
71+
}
72+
73+
/**
74+
* Converts the given [LocalDate] to [NSDateComponents].
75+
*
76+
* Of all the fields, only the bare minimum required for uniquely identifying the date and time are set.
77+
*/
78+
public fun LocalDateTime.toNSDateComponents(): NSDateComponents {
79+
val components = date.toNSDateComponents()
80+
components.hour = hour.convert()
81+
components.minute = minute.convert()
82+
components.second = second.convert()
83+
components.nanosecond = nanosecond.convert()
84+
return components
85+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2019-2020 JetBrains s.r.o.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
package kotlinx.datetime
6+
import kotlinx.cinterop.*
7+
import platform.Foundation.*
8+
import kotlin.math.*
9+
import kotlin.random.*
10+
import kotlin.test.*
11+
12+
class ConvertersTest {
13+
14+
private val dateFormatter = NSDateFormatter()
15+
private val locale = NSLocale.localeWithLocaleIdentifier("en_US_POSIX")
16+
private val gregorian = NSCalendar.calendarWithIdentifier(NSCalendarIdentifierGregorian)!!
17+
private val utc = NSTimeZone.timeZoneForSecondsFromGMT(0)
18+
19+
init {
20+
dateFormatter.locale = locale
21+
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
22+
dateFormatter.calendar = gregorian
23+
dateFormatter.timeZone = utc
24+
}
25+
26+
@Test
27+
fun testToFromNSDate() {
28+
// The first day on the Gregorian calendar. The day before it is 1582-10-04 in the Julian calendar.
29+
val gregorianCalendarStart = Instant.parse("1582-10-15T00:00:00Z").toEpochMilliseconds()
30+
val minBoundMillis = (NSDate.distantPast.timeIntervalSince1970 * 1000 + 0.5).toLong()
31+
val maxBoundMillis = (NSDate.distantFuture.timeIntervalSince1970 * 1000 - 0.5).toLong()
32+
repeat (1000) {
33+
val millis = Random.nextLong(minBoundMillis, maxBoundMillis)
34+
val instant = Instant.fromEpochMilliseconds(millis)
35+
val date = instant.toNSDate()
36+
// Darwin's date printer dynamically adjusts to switching between calendars, while our Instant does not.
37+
if (millis >= gregorianCalendarStart) {
38+
assertEquals(instant, Instant.parse(dateFormatter.stringFromDate(date)))
39+
}
40+
assertEquals(instant, date.toKotlinInstant())
41+
}
42+
}
43+
44+
@Test
45+
fun availableZoneIdsToNSTimeZone() {
46+
for (id in TimeZone.availableZoneIds) {
47+
val normalizedId = (NSTimeZone.abbreviationDictionary[id] ?: id) as String
48+
val timeZone = TimeZone.of(normalizedId)
49+
if (timeZone is ZoneOffset) {
50+
continue
51+
}
52+
val nsTimeZone = timeZone.toNSTimeZone()
53+
assertEquals(normalizedId, nsTimeZone.name)
54+
assertEquals(timeZone, nsTimeZone.toKotlinTimeZone())
55+
}
56+
}
57+
58+
// from threetenbp's tests for time zones
59+
@Test
60+
fun zoneOffsetToNSTimeZone() {
61+
for (i in -18 * 60..18 * 60) {
62+
val hours = i / 60
63+
val minutes = i % 60
64+
val str = (if (i < 0) "-" else "+") +
65+
(abs(hours) + 100).toString().substring(1) + ":" +
66+
(abs(minutes) + 100).toString().substring(1) + ":" +
67+
"00"
68+
val test = TimeZone.of(str)
69+
zoneOffsetCheck(test, hours, minutes)
70+
}
71+
}
72+
73+
@Test
74+
fun localDateToNSDateComponentsTest() {
75+
val date = LocalDate.parse("2019-02-04")
76+
val components = date.toNSDateComponents()
77+
components.timeZone = utc
78+
val nsDate = gregorian.dateFromComponents(components)!!
79+
val formatter = NSDateFormatter()
80+
formatter.dateFormat = "yyyy-MM-dd"
81+
assertEquals("2019-02-04", formatter.stringFromDate(nsDate))
82+
}
83+
84+
@Test
85+
fun localDateTimeToNSDateComponentsTest() {
86+
val str = "2019-02-04T23:59:30.543"
87+
val dateTime = LocalDateTime.parse(str)
88+
val components = dateTime.toNSDateComponents()
89+
components.timeZone = utc
90+
val nsDate = gregorian.dateFromComponents(components)!!
91+
assertEquals(str + "Z", dateFormatter.stringFromDate(nsDate))
92+
}
93+
94+
private fun zoneOffsetCheck(timeZone: TimeZone, hours: Int, minutes: Int) {
95+
val nsTimeZone = timeZone.toNSTimeZone()
96+
assertEquals(hours * 3600 + minutes * 60, nsTimeZone.secondsFromGMT.convert())
97+
assertEquals(timeZone, nsTimeZone.toKotlinTimeZone())
98+
}
99+
}

settings.gradle

+3
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,6 @@ rootProject.name = 'Kotlin-DateTime-library'
1313

1414
include ':core'
1515
project(":core").name='kotlinx-datetime'
16+
17+
include ':darwin-integration'
18+
project(":darwin-integration").name='kotlinx-datetime-darwin'

0 commit comments

Comments
 (0)