From 399b594999e0079dd0b25d1ee437abdf773bad36 Mon Sep 17 00:00:00 2001 From: Jeff Lockhart Date: Mon, 17 Feb 2025 11:52:57 -0700 Subject: [PATCH] Use millisecond precision for NSDate conversion Fixes tests, since CBL only stores milliseconds, but kotlinx-datetime now supports higher precision NSDate conversion --- .../appleMain/ee/kotbase/TLSIdentity.apple.kt | 4 +-- .../appleMain/kotlin/kotbase/Array.apple.kt | 4 +-- .../kotlin/kotbase/Collection.apple.kt | 4 +-- .../kotlin/kotbase/Database.apple.kt | 4 +-- .../kotlin/kotbase/Dictionary.apple.kt | 4 +-- .../kotlin/kotbase/Document.apple.kt | 4 +-- .../appleMain/kotlin/kotbase/Result.apple.kt | 6 ++-- .../appleMain/kotlin/kotbase/Utils.apple.kt | 4 +-- .../appleMain/kotlin/kotbase/ext/DateExt.kt | 28 ++++++++++++++++ .../kotlin/kotbase/ext/DateExtTest.kt | 32 +++++++++++++++++++ .../commonMain/kotlin/kotbase/ext/DateExt.kt | 11 ++++++- 11 files changed, 87 insertions(+), 18 deletions(-) create mode 100644 couchbase-lite/src/appleMain/kotlin/kotbase/ext/DateExt.kt create mode 100644 couchbase-lite/src/appleTest/kotlin/kotbase/ext/DateExtTest.kt diff --git a/couchbase-lite-ee/src/appleMain/ee/kotbase/TLSIdentity.apple.kt b/couchbase-lite-ee/src/appleMain/ee/kotbase/TLSIdentity.apple.kt index dc66a277b..3411051bf 100644 --- a/couchbase-lite-ee/src/appleMain/ee/kotbase/TLSIdentity.apple.kt +++ b/couchbase-lite-ee/src/appleMain/ee/kotbase/TLSIdentity.apple.kt @@ -18,10 +18,10 @@ package kotbase import cocoapods.CouchbaseLite.CBLTLSIdentity import kotbase.internal.DelegatedClass import kotbase.ext.toByteArray +import kotbase.ext.toKotlinInstantMillis import kotbase.ext.toNSData import kotbase.ext.wrapCBLError import kotlinx.datetime.Instant -import kotlinx.datetime.toKotlinInstant import kotlinx.datetime.toNSDate import platform.Security.SecCertificateRef import platform.Security.SecIdentityRef @@ -34,7 +34,7 @@ internal constructor(actual: CBLTLSIdentity) : DelegatedClass(ac get() = (actual.certs as List).map { it.toByteArray() } public actual val expiration: Instant - get() = actual.expiration.toKotlinInstant() + get() = actual.expiration.toKotlinInstantMillis() public actual companion object { diff --git a/couchbase-lite/src/appleMain/kotlin/kotbase/Array.apple.kt b/couchbase-lite/src/appleMain/kotlin/kotbase/Array.apple.kt index c1b392773..17cee17cd 100644 --- a/couchbase-lite/src/appleMain/kotlin/kotbase/Array.apple.kt +++ b/couchbase-lite/src/appleMain/kotlin/kotbase/Array.apple.kt @@ -17,10 +17,10 @@ package kotbase import cocoapods.CouchbaseLite.CBLArray import kotbase.ext.asNumber +import kotbase.ext.toKotlinInstantMillis import kotbase.internal.DelegatedClass import kotlinx.cinterop.convert import kotlinx.datetime.Instant -import kotlinx.datetime.toKotlinInstant public actual open class Array internal constructor(actual: CBLArray) : DelegatedClass(actual), Iterable { @@ -82,7 +82,7 @@ internal constructor(actual: CBLArray) : DelegatedClass(actual), Itera public actual fun getDate(index: Int): Instant? { checkIndex(index) - return actual.dateAtIndex(index.convert())?.toKotlinInstant() + return actual.dateAtIndex(index.convert())?.toKotlinInstantMillis() } public actual open fun getArray(index: Int): Array? { diff --git a/couchbase-lite/src/appleMain/kotlin/kotbase/Collection.apple.kt b/couchbase-lite/src/appleMain/kotlin/kotbase/Collection.apple.kt index 1a97bc89b..22c26c23e 100644 --- a/couchbase-lite/src/appleMain/kotlin/kotbase/Collection.apple.kt +++ b/couchbase-lite/src/appleMain/kotlin/kotbase/Collection.apple.kt @@ -17,6 +17,7 @@ package kotbase import cocoapods.CouchbaseLite.CBLCollection import kotbase.ext.asDispatchQueue +import kotbase.ext.toKotlinInstantMillis import kotbase.ext.wrapCBLError import kotbase.internal.DelegatedClass import kotlinx.coroutines.CoroutineDispatcher @@ -24,7 +25,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.datetime.Instant -import kotlinx.datetime.toKotlinInstant import kotlinx.datetime.toNSDate import kotlin.coroutines.CoroutineContext import kotlin.experimental.ExperimentalObjCRefinement @@ -150,7 +150,7 @@ internal constructor( public actual fun getDocumentExpiration(id: String): Instant? { return wrapCBLError { error -> actual.getDocumentExpirationWithID(id, error) - }?.toKotlinInstant() + }?.toKotlinInstantMillis() } public actual fun addChangeListener(listener: CollectionChangeListener): ListenerToken { diff --git a/couchbase-lite/src/appleMain/kotlin/kotbase/Database.apple.kt b/couchbase-lite/src/appleMain/kotlin/kotbase/Database.apple.kt index ad18ed6fd..db4343884 100644 --- a/couchbase-lite/src/appleMain/kotlin/kotbase/Database.apple.kt +++ b/couchbase-lite/src/appleMain/kotlin/kotbase/Database.apple.kt @@ -18,6 +18,7 @@ package kotbase import cocoapods.CouchbaseLite.* import kotbase.internal.DelegatedClass import kotbase.ext.asDispatchQueue +import kotbase.ext.toKotlinInstantMillis import kotbase.ext.wrapCBLError import kotlinx.atomicfu.locks.reentrantLock import kotlinx.atomicfu.locks.withLock @@ -26,7 +27,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.datetime.Instant -import kotlinx.datetime.toKotlinInstant import kotlinx.datetime.toNSDate import kotlin.coroutines.CoroutineContext import kotlin.experimental.ExperimentalObjCRefinement @@ -430,7 +430,7 @@ internal constructor(actual: CBLDatabase) : DelegatedClass(actual), @Throws(CouchbaseLiteException::class) public actual fun getDocumentExpiration(id: String): Instant? { return mustBeOpen { - actual.getDocumentExpirationWithID(id)?.toKotlinInstant() + actual.getDocumentExpirationWithID(id)?.toKotlinInstantMillis() } } diff --git a/couchbase-lite/src/appleMain/kotlin/kotbase/Dictionary.apple.kt b/couchbase-lite/src/appleMain/kotlin/kotbase/Dictionary.apple.kt index 864452cd7..9b21a53c0 100644 --- a/couchbase-lite/src/appleMain/kotlin/kotbase/Dictionary.apple.kt +++ b/couchbase-lite/src/appleMain/kotlin/kotbase/Dictionary.apple.kt @@ -17,9 +17,9 @@ package kotbase import cocoapods.CouchbaseLite.CBLDictionary import kotbase.ext.asNumber +import kotbase.ext.toKotlinInstantMillis import kotbase.internal.DelegatedClass import kotlinx.datetime.Instant -import kotlinx.datetime.toKotlinInstant public actual open class Dictionary internal constructor(actual: CBLDictionary) : DelegatedClass(actual), Iterable { @@ -67,7 +67,7 @@ internal constructor(actual: CBLDictionary) : DelegatedClass(actu actual.blobForKey(key)?.asBlob() public actual fun getDate(key: String): Instant? = - actual.dateForKey(key)?.toKotlinInstant() + actual.dateForKey(key)?.toKotlinInstantMillis() public actual open fun getArray(key: String): Array? { return getInternalCollection(key) diff --git a/couchbase-lite/src/appleMain/kotlin/kotbase/Document.apple.kt b/couchbase-lite/src/appleMain/kotlin/kotbase/Document.apple.kt index 47fff7bec..70a8a23cd 100644 --- a/couchbase-lite/src/appleMain/kotlin/kotbase/Document.apple.kt +++ b/couchbase-lite/src/appleMain/kotlin/kotbase/Document.apple.kt @@ -18,9 +18,9 @@ package kotbase import cocoapods.CouchbaseLite.CBLDocument import com.couchbase.lite.database import kotbase.ext.asNumber +import kotbase.ext.toKotlinInstantMillis import kotbase.internal.DelegatedClass import kotlinx.datetime.Instant -import kotlinx.datetime.toKotlinInstant public actual open class Document internal constructor( @@ -90,7 +90,7 @@ internal constructor( actual.blobForKey(key)?.asBlob() public actual fun getDate(key: String): Instant? = - actual.dateForKey(key)?.toKotlinInstant() + actual.dateForKey(key)?.toKotlinInstantMillis() public actual open fun getArray(key: String): Array? { return getInternalCollection(key) diff --git a/couchbase-lite/src/appleMain/kotlin/kotbase/Result.apple.kt b/couchbase-lite/src/appleMain/kotlin/kotbase/Result.apple.kt index c61c3ab97..8ebfe6c93 100644 --- a/couchbase-lite/src/appleMain/kotlin/kotbase/Result.apple.kt +++ b/couchbase-lite/src/appleMain/kotlin/kotbase/Result.apple.kt @@ -18,9 +18,9 @@ package kotbase import cocoapods.CouchbaseLite.CBLQueryResult import kotbase.internal.DelegatedClass import kotbase.ext.asNumber +import kotbase.ext.toKotlinInstantMillis import kotlinx.cinterop.convert import kotlinx.datetime.Instant -import kotlinx.datetime.toKotlinInstant public actual class Result internal constructor(actual: CBLQueryResult) : DelegatedClass(actual), Iterable { @@ -75,7 +75,7 @@ internal constructor(actual: CBLQueryResult) : DelegatedClass(ac public actual fun getDate(index: Int): Instant? { assertInBounds(index) - return actual.dateAtIndex(index.convert())?.toKotlinInstant() + return actual.dateAtIndex(index.convert())?.toKotlinInstantMillis() } public actual fun getArray(index: Int): Array? { @@ -123,7 +123,7 @@ internal constructor(actual: CBLQueryResult) : DelegatedClass(ac actual.blobForKey(key)?.asBlob() public actual fun getDate(key: String): Instant? = - actual.dateForKey(key)?.toKotlinInstant() + actual.dateForKey(key)?.toKotlinInstantMillis() public actual fun getArray(key: String): Array? = actual.arrayForKey(key)?.asArray() diff --git a/couchbase-lite/src/appleMain/kotlin/kotbase/Utils.apple.kt b/couchbase-lite/src/appleMain/kotlin/kotbase/Utils.apple.kt index feba220dd..1702880a4 100644 --- a/couchbase-lite/src/appleMain/kotlin/kotbase/Utils.apple.kt +++ b/couchbase-lite/src/appleMain/kotlin/kotbase/Utils.apple.kt @@ -16,9 +16,9 @@ package kotbase import cocoapods.CouchbaseLite.* +import kotbase.ext.toKotlinInstantMillis import kotbase.internal.DelegatedClass import kotlinx.datetime.Instant -import kotlinx.datetime.toKotlinInstant import kotlinx.datetime.toNSDate import platform.Foundation.NSDate import platform.Foundation.NSNull @@ -30,7 +30,7 @@ internal fun Any.delegateIfNecessary(): Any? = when (this) { is CBLArray -> asArray() is CBLMutableDictionary -> asMutableDictionary() is CBLDictionary -> asDictionary() - is NSDate -> toKotlinInstant() + is NSDate -> toKotlinInstantMillis() is List<*> -> delegateIfNecessary() is Map<*, *> -> delegateIfNecessary() else -> this diff --git a/couchbase-lite/src/appleMain/kotlin/kotbase/ext/DateExt.kt b/couchbase-lite/src/appleMain/kotlin/kotbase/ext/DateExt.kt new file mode 100644 index 000000000..01a025453 --- /dev/null +++ b/couchbase-lite/src/appleMain/kotlin/kotbase/ext/DateExt.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2025 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase.ext + +import kotlinx.datetime.Instant +import platform.Foundation.NSDate +import platform.Foundation.timeIntervalSince1970 +import kotlin.math.roundToLong + +internal fun NSDate.toKotlinInstantMillis(): Instant { + val secs = timeIntervalSince1970() + val fullSeconds = secs.toLong() + val millis = (secs - fullSeconds) * MILLIS_PER_ONE + return Instant.fromEpochMilliseconds(fullSeconds * MILLIS_PER_ONE + millis.roundToLong()) +} diff --git a/couchbase-lite/src/appleTest/kotlin/kotbase/ext/DateExtTest.kt b/couchbase-lite/src/appleTest/kotlin/kotbase/ext/DateExtTest.kt new file mode 100644 index 000000000..d1644b8d3 --- /dev/null +++ b/couchbase-lite/src/appleTest/kotlin/kotbase/ext/DateExtTest.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2025 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase.ext + +import kotlinx.datetime.Clock +import kotlinx.datetime.toNSDate +import kotlin.test.Test +import kotlin.test.assertEquals + +class DateExtTest { + + @Test + fun testToKotlinInstantMillis() { + val now = Clock.System.nowMillis() + val nsDate = now.toNSDate() + val result = nsDate.toKotlinInstantMillis() + assertEquals(now, result) + } +} diff --git a/couchbase-lite/src/commonMain/kotlin/kotbase/ext/DateExt.kt b/couchbase-lite/src/commonMain/kotlin/kotbase/ext/DateExt.kt index c5ae55a33..08e827700 100644 --- a/couchbase-lite/src/commonMain/kotlin/kotbase/ext/DateExt.kt +++ b/couchbase-lite/src/commonMain/kotlin/kotbase/ext/DateExt.kt @@ -16,6 +16,7 @@ package kotbase.ext import kotlinx.datetime.Instant +import kotlin.math.roundToLong /** * An ISO 8601 UTC string with milliseconds @@ -23,7 +24,7 @@ import kotlinx.datetime.Instant * can be seconds or nanoseconds precision. */ internal fun Instant.toStringMillis(): String { - return toString().let { + return roundToMillis().toString().let { if (it.length == 20) { it.dropLast(1) + ".000Z" } else if (it.length > 24) { @@ -33,3 +34,11 @@ internal fun Instant.toStringMillis(): String { } } } + +internal const val NANOS_PER_MILLI = 1_000_000 +internal const val MILLIS_PER_ONE = 1_000 + +internal fun Instant.roundToMillis(): Instant { + val millis = nanosecondsOfSecond.toFloat() / NANOS_PER_MILLI + return Instant.fromEpochMilliseconds(epochSeconds * MILLIS_PER_ONE + millis.roundToLong()) +}