Repository: kevincianfarini/cardiologist
Pull request #136 (2025-06-25 00:55:30)

Update to kotlinx-datetime 0.7.0


Patch:
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 5b76d39..24cb2de 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -3,7 +3,7 @@ dokka = "2.0.0"
 kmpmt = "0.1.1"
 kotlin = "2.2.0"
 kotlinx-coroutines = "1.10.2"
-kotlinx-datetime = "0.6.2"
+kotlinx-datetime = "0.7.0"
 poko = "0.18.7"
 publish = "0.33.0"
 
diff --git a/src/commonMain/kotlin/io/github/kevincianfarini/cardiologist/Pulse.kt b/src/commonMain/kotlin/io/github/kevincianfarini/cardiologist/Pulse.kt
index 4900797..5099f1c 100644
--- a/src/commonMain/kotlin/io/github/kevincianfarini/cardiologist/Pulse.kt
+++ b/src/commonMain/kotlin/io/github/kevincianfarini/cardiologist/Pulse.kt
@@ -7,12 +7,14 @@ import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.take
 import kotlinx.coroutines.flow.takeWhile
 import kotlinx.coroutines.launch
-import kotlinx.datetime.Instant
+import kotlin.time.Instant
 import kotlin.jvm.JvmInline
+import kotlin.time.ExperimentalTime
 
 /**
  * A [Pulse] is a cadence which informs consumers when to execute work by calling [Pulse.beat].
  */
+@ExperimentalTime
 @JvmInline
 public value class Pulse internal constructor(private val flow: Flow<Instant>) {
 
diff --git a/src/commonMain/kotlin/io/github/kevincianfarini/cardiologist/impl/LocalDateTime.kt b/src/commonMain/kotlin/io/github/kevincianfarini/cardiologist/impl/LocalDateTime.kt
index f61b827..4c08a13 100644
--- a/src/commonMain/kotlin/io/github/kevincianfarini/cardiologist/impl/LocalDateTime.kt
+++ b/src/commonMain/kotlin/io/github/kevincianfarini/cardiologist/impl/LocalDateTime.kt
@@ -36,8 +36,8 @@ private fun LocalDateTime.matchesDay(schedule: PulseSchedule): Boolean {
     val matchesDayOfWeekOnly = schedule.onDaysOfMonth == WILDCARD_DAYS_OF_MONTH && dayOfWeek in schedule.onDaysOfWeek
     val matchesDayOfMonthAndDayOfWeek = schedule.onDaysOfMonth != WILDCARD_DAYS_OF_MONTH &&
             schedule.onDaysOfWeek.isNotEmpty() &&
-            (dayOfMonth in schedule.onDaysOfMonth || dayOfWeek in schedule.onDaysOfWeek)
-    val matchesDayOfMonthOnly = schedule.onDaysOfWeek.isEmpty() && dayOfMonth in schedule.onDaysOfMonth
+            (day in schedule.onDaysOfMonth || dayOfWeek in schedule.onDaysOfWeek)
+    val matchesDayOfMonthOnly = schedule.onDaysOfWeek.isEmpty() && day in schedule.onDaysOfMonth
     return matchesDayOfWeekOnly || matchesDayOfMonthAndDayOfWeek || matchesDayOfMonthOnly
 }
 
@@ -102,12 +102,12 @@ private fun LocalDateTime.nextDayOfMonth(
     val maxDayOfMonth = onDaysOfMonth.maxOrNull()!!
     require(minDayOfMonth >= 1 && maxDayOfMonth <= 31) { "onDaysOfMonth $onDaysOfMonth not in range 1..31." }
     val incrementedDay = when {
-        increment && dayOfMonth + 1 <= month.numberOfDays(year) -> dayOfMonth + 1
+        increment && day + 1 <= month.numberOfDays(year) -> day + 1
         increment -> 1
-        else -> dayOfMonth
+        else -> day
     }
     return when {
-        incrementedDay < dayOfMonth -> copy(dayOfMonth = incrementedDay).nextMonth(inMonths, increment = true)
+        incrementedDay < day -> copy(dayOfMonth = incrementedDay).nextMonth(inMonths, increment = true)
         incrementedDay in onDaysOfMonth -> copy(dayOfMonth = incrementedDay)
         incrementedDay < minDayOfMonth -> {
             if (minDayOfMonth <= month.numberOfDays(year)) {
@@ -138,7 +138,7 @@ private fun LocalDateTime.nextDayOfWeek(
                 incrementedDayOfWeek in onDaysOfWeek -> incrementedDayOfWeek
                 else -> incrementedDayOfWeek.incrementUntilMatch(onDaysOfWeek)
             }
-            val futureDayOfMonth = dayOfMonth + dayOfWeek.daysUntil(nextMatchedDayOfWeek)
+            val futureDayOfMonth = day + dayOfWeek.daysUntil(nextMatchedDayOfWeek)
             when {
                 futureDayOfMonth <= month.numberOfDays(year) -> copy(dayOfMonth = futureDayOfMonth)
                 else -> copy(dayOfMonth = 1).nextMonth(inMonths, increment = true).nextDayOfWeek(onDaysOfWeek, inMonths)
@@ -235,7 +235,6 @@ private fun LocalDateTime.nextSecond(
     }.copy(nanosecond = 0)
 }
 
-@Suppress("REDUNDANT_ELSE_IN_WHEN")
 private fun Month.numberOfDays(year: Int) = when (this) {
     Month.JANUARY -> 31
     Month.FEBRUARY -> if (year.isLeapYear) 29 else 28
@@ -249,7 +248,6 @@ private fun Month.numberOfDays(year: Int) = when (this) {
     Month.OCTOBER -> 31
     Month.NOVEMBER -> 30
     Month.DECEMBER -> 31
-    else -> error("This is impossible.")
 }
 
 private val Int.isLeapYear: Boolean get() {
@@ -259,8 +257,8 @@ private val Int.isLeapYear: Boolean get() {
 
 internal fun LocalDateTime.copy(
     year: Int = this.year,
-    monthNumber: Int = this.monthNumber,
-    dayOfMonth: Int = this.dayOfMonth,
+    monthNumber: Int = this.month.number,
+    dayOfMonth: Int = this.day,
     hour: Int = this.hour,
     minute: Int = this.minute,
     second: Int = this.second,
diff --git a/src/commonMain/kotlin/io/github/kevincianfarini/cardiologist/schedule.kt b/src/commonMain/kotlin/io/github/kevincianfarini/cardiologist/schedule.kt
index 225b9af..b9ef690 100644
--- a/src/commonMain/kotlin/io/github/kevincianfarini/cardiologist/schedule.kt
+++ b/src/commonMain/kotlin/io/github/kevincianfarini/cardiologist/schedule.kt
@@ -2,18 +2,19 @@ package io.github.kevincianfarini.cardiologist
 
 import io.github.kevincianfarini.cardiologist.impl.nextMatch
 import io.github.kevincianfarini.cardiologist.impl.parseCronExpression
+import kotlin.time.Clock
 import kotlin.time.Duration
+import kotlin.time.Instant
 import kotlinx.coroutines.flow.flow
-import kotlinx.datetime.Clock
 import kotlinx.datetime.DateTimePeriod
 import kotlinx.datetime.DayOfWeek
-import kotlinx.datetime.Instant
 import kotlinx.datetime.LocalDateTime
 import kotlinx.datetime.Month
 import kotlinx.datetime.TimeZone
 import kotlinx.datetime.plus
 import kotlinx.datetime.toInstant
 import kotlinx.datetime.toLocalDateTime
+import kotlin.time.ExperimentalTime
 
 /**
  * Return a [Pulse] which beats every [period].
@@ -22,6 +23,7 @@ import kotlinx.datetime.toLocalDateTime
  * scheduled start of the subsequent [beat][Pulse.beat]. The returned [Pulse] will delay for [period] prior to its first
  * [beat][Pulse.beat].
  */
+@ExperimentalTime
 public fun Clock.fixedPeriodPulse(period: Duration): Pulse {
     val flow = flow {
         var nextPulse: Instant = now() + period
@@ -41,6 +43,7 @@ public fun Clock.fixedPeriodPulse(period: Duration): Pulse {
  * scheduled start of the subsequent [beat][Pulse.beat]. The returned [Pulse] will delay for [period] prior to its first
  * [beat][Pulse.beat].
  */
+@ExperimentalTime
 public fun Clock.fixedPeriodPulse(period: DateTimePeriod, timeZone: TimeZone): Pulse {
     val flow = flow {
         var nextPulse: Instant = now().plus(period, timeZone)
@@ -83,6 +86,7 @@ public fun Clock.fixedPeriodPulse(period: DateTimePeriod, timeZone: TimeZone): P
  * @param timeZone The TimeZone to schedule pulses in.
  * @throws [IllegalArgumentException] if any parameter is out of the above range.
  */
+@ExperimentalTime
 public fun Clock.schedulePulse(
     timeZone: TimeZone = TimeZone.UTC,
     atSecond: Int? = null,
@@ -121,6 +125,7 @@ public fun Clock.schedulePulse(
  * @param timeZone The TimeZone to schedule pulses in.
  * @throws IllegalArgumentException if [scheduleBuilder] is not valid.
  */
+@ExperimentalTime
 public fun Clock.schedulePulse(
     timeZone: TimeZone = TimeZone.UTC,
     scheduleBuilder: PulseScheduleBuilder.() -> Unit,
@@ -155,6 +160,7 @@ public fun Clock.schedulePulse(
  * @param timeZone The TimeZone to schedule pulses in.
  * @throws IllegalArgumentException if [cronExpression] is not valid.
  */
+@ExperimentalTime
 public fun Clock.schedulePulse(
     cronExpression: String,
     timeZone: TimeZone = TimeZone.UTC,
@@ -173,6 +179,7 @@ public fun Clock.schedulePulse(
  * @param schedule The pre-built [PulseSchedule].
  * @param timeZone The TimeZone to schedule pulses in.
  */
+@ExperimentalTime
 public fun Clock.schedulePulse(
     schedule: PulseSchedule,
     timeZone: TimeZone = TimeZone.UTC,
diff --git a/src/commonMain/kotlin/io/github/kevincianfarini/cardiologist/suspend.kt b/src/commonMain/kotlin/io/github/kevincianfarini/cardiologist/suspend.kt
index fd49e69..7324ab2 100644
--- a/src/commonMain/kotlin/io/github/kevincianfarini/cardiologist/suspend.kt
+++ b/src/commonMain/kotlin/io/github/kevincianfarini/cardiologist/suspend.kt
@@ -1,12 +1,13 @@
 package io.github.kevincianfarini.cardiologist
 
+import kotlin.time.Clock
 import kotlin.time.Duration.Companion.minutes
+import kotlin.time.Instant
 import kotlinx.coroutines.delay
-import kotlinx.datetime.Clock
-import kotlinx.datetime.Instant
 import kotlinx.datetime.LocalDateTime
 import kotlinx.datetime.TimeZone
 import kotlinx.datetime.toInstant
+import kotlin.time.ExperimentalTime
 
 /**
  * Execute the provided [action] at or after [instant].
@@ -14,6 +15,7 @@ import kotlinx.datetime.toInstant
  * The coroutine will be suspended until [instant] without blocking the thread. If [Clock.now] returns an [Instant]
  * greater than the supplied [instant], this function executes [action] without suspending.
  */
+@ExperimentalTime
 public suspend fun <T> Clock.executeAt(
     instant: Instant,
     action: suspend () -> T,
@@ -32,6 +34,7 @@ public suspend fun <T> Clock.executeAt(
  * Local time conversion is sometimes ambiguous, and therefore it's recommended to schedule execution in a
  * fixed UTC offset timezone. See [LocalDateTime.toInstant] for more details.
  */
+@ExperimentalTime
 public suspend fun <T> Clock.executeAt(
     dateTime: LocalDateTime,
     timeZone: TimeZone,
@@ -47,6 +50,7 @@ public suspend fun <T> Clock.executeAt(
  *
  * Coroutines delayed for more than one minute will occasionally be resumed to account for clock drift.
  */
+@ExperimentalTime
 internal suspend fun Clock.delayUntil(instant: Instant) {
     var now = now()
     while (now < instant) {
@@ -63,6 +67,7 @@ internal suspend fun Clock.delayUntil(instant: Instant) {
  *
  * Coroutines delayed for more than one minute will occasionally be resumed to account for clock drift.
  */
+@ExperimentalTime
 internal suspend fun Clock.delayUntil(dateTime: LocalDateTime, timeZone: TimeZone) {
     delayUntil(instant = dateTime.toInstant(timeZone))
 }


Test patch:
diff --git a/src/commonTest/kotlin/io/github/kevincianfarini/cardiologist/FakeClock.kt b/src/commonTest/kotlin/io/github/kevincianfarini/cardiologist/FakeClock.kt
index a92e03b..b3c963c 100644
--- a/src/commonTest/kotlin/io/github/kevincianfarini/cardiologist/FakeClock.kt
+++ b/src/commonTest/kotlin/io/github/kevincianfarini/cardiologist/FakeClock.kt
@@ -1,21 +1,26 @@
 package io.github.kevincianfarini.cardiologist
 
+import kotlin.time.Clock
+import kotlin.time.ExperimentalTime
+import kotlin.time.Instant
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.TestCoroutineScheduler
 import kotlinx.coroutines.test.TestScope
-import kotlinx.datetime.Clock
-import kotlinx.datetime.Instant
 
 @OptIn(ExperimentalCoroutinesApi::class)
+@ExperimentalTime
 private class TestClock(private val scheduler: TestCoroutineScheduler) : Clock {
     override fun now() = Instant.fromEpochMilliseconds(scheduler.currentTime)
 }
 
+@ExperimentalTime
 val TestScope.testClock: Clock get() = TestClock(testScheduler)
 
+@ExperimentalTime
 private class QueueClock(private val instants: List<Instant>) : Clock {
     private var index = 0
     override fun now(): Instant = instants[index++]
 }
 
+@ExperimentalTime
 fun List<Instant>.asClock(): Clock = QueueClock(this)
\ No newline at end of file
diff --git a/src/commonTest/kotlin/io/github/kevincianfarini/cardiologist/PulseTests.kt b/src/commonTest/kotlin/io/github/kevincianfarini/cardiologist/PulseTests.kt
index e5c14b6..4bd0b98 100644
--- a/src/commonTest/kotlin/io/github/kevincianfarini/cardiologist/PulseTests.kt
+++ b/src/commonTest/kotlin/io/github/kevincianfarini/cardiologist/PulseTests.kt
@@ -13,9 +13,11 @@ import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.time.Duration.Companion.minutes
 import kotlin.time.Duration.Companion.seconds
+import kotlin.time.ExperimentalTime
+import kotlin.time.Instant
 import kotlin.time.measureTime
 
-@OptIn(ExperimentalCoroutinesApi::class)
+@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class)
 class PulseTests {
 
     @Test
diff --git a/src/commonTest/kotlin/io/github/kevincianfarini/cardiologist/SuspendingTests.kt b/src/commonTest/kotlin/io/github/kevincianfarini/cardiologist/SuspendingTests.kt
index f97f2cd..c83dbcc 100644
--- a/src/commonTest/kotlin/io/github/kevincianfarini/cardiologist/SuspendingTests.kt
+++ b/src/commonTest/kotlin/io/github/kevincianfarini/cardiologist/SuspendingTests.kt
@@ -1,17 +1,19 @@
 package io.github.kevincianfarini.cardiologist
 
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runTest
-import kotlinx.coroutines.test.testTimeSource
-import kotlinx.datetime.*
 import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.time.Duration.Companion.hours
 import kotlin.time.Duration.Companion.minutes
 import kotlin.time.Duration.Companion.seconds
+import kotlin.time.ExperimentalTime
+import kotlin.time.Instant
 import kotlin.time.measureTime
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.testTimeSource
+import kotlinx.datetime.*
 
-@OptIn(ExperimentalCoroutinesApi::class)
+@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class)
 class SuspendingTests {
 
     @Test
diff --git a/src/commonTest/kotlin/io/github/kevincianfarini/cardiologist/impl/LocalDateTimeTest.kt b/src/commonTest/kotlin/io/github/kevincianfarini/cardiologist/impl/LocalDateTimeTest.kt
index b26af4d..ab49cb9 100644
--- a/src/commonTest/kotlin/io/github/kevincianfarini/cardiologist/impl/LocalDateTimeTest.kt
+++ b/src/commonTest/kotlin/io/github/kevincianfarini/cardiologist/impl/LocalDateTimeTest.kt
@@ -5,13 +5,15 @@ import io.github.kevincianfarini.cardiologist.buildPulseSchedule
 import kotlinx.datetime.*
 import kotlin.test.Test
 import kotlin.test.assertEquals
+import kotlin.time.ExperimentalTime
 
+@OptIn(ExperimentalTime::class)
 class LocalDateTimeTest {
 
     private val stubDatetime = LocalDateTime(
         year = 2023,
-        monthNumber = 10,
-        dayOfMonth = 4,
+        month = Month.OCTOBER,
+        day = 4,
         hour = 0,
         minute = 0,
         second = 0,
@@ -101,8 +103,8 @@ class LocalDateTimeTest {
         expected = stubDatetime.copy(year = 2024, monthNumber = 1, dayOfMonth = 1),
         actual = LocalDateTime(
             year = 2023,
-            monthNumber = 12,
-            dayOfMonth = 31,
+            month = Month.DECEMBER,
+            day = 31,
             hour = 23,
             minute = 59,
             second = 59,
@@ -123,16 +125,16 @@ class LocalDateTimeTest {
     fun matches_up_to_next_minute_complex() = assertEquals(
         expected = LocalDateTime(
             year = 2023,
-            monthNumber = 10,
-            dayOfMonth = 5,
+            month = Month.OCTOBER,
+            day = 5,
             hour = 8,
             minute = 58,
             second = 0,
         ),
         actual = LocalDateTime(
             year = 2023,
-            monthNumber = 10,
-            dayOfMonth = 5,
+            month = Month.OCTOBER,
+            day = 5,
             hour = 8,
             minute = 56,
             second = 17,
@@ -201,8 +203,8 @@ class LocalDateTimeTest {
 
     @Test
     fun matches_jan_18_2024_to_jan_19_2024() = assertEquals(
-        expected = LocalDateTime(year = 2024, monthNumber = 1, dayOfMonth = 19, hour = 0, minute = 0),
-        actual = LocalDateTime(year = 2024, monthNumber = 1, dayOfMonth = 18, hour = 23, minute = 59, second = 48)
+        expected = LocalDateTime(year = 2024, month = Month.JANUARY, day = 19, hour = 0, minute = 0),
+        actual = LocalDateTime(year = 2024, month = Month.JANUARY, day = 18, hour = 23, minute = 59, second = 48)
             .nextMatch {
                 atSeconds(0)
                 atMinutes(0)
@@ -212,7 +214,7 @@ class LocalDateTimeTest {
 
     @Test
     fun one_month_gap_for_two_years() {
-        LocalDateTime(year = 2023, monthNumber = 1, dayOfMonth = 1, hour = 0, minute = 0).assertGap(
+        LocalDateTime(year = 2023, month = Month.JANUARY, day = 1, hour = 0, minute = 0).assertGap(
             assertPeriod = DateTimePeriod(months = 1),
             schedule = buildPulseSchedule {
                 atSeconds(0)
@@ -225,7 +227,7 @@ class LocalDateTimeTest {
 
     @Test
     fun one_month_gap_for_a_year() {
-        LocalDateTime(year = 2023, monthNumber = 1, dayOfMonth = 1, hour = 0, minute = 0).assertGap(
+        LocalDateTime(year = 2023, month = Month.JANUARY, day = 1, hour = 0, minute = 0).assertGap(
             assertPeriod = DateTimePeriod(months = 1),
             schedule = buildPulseSchedule {
                 atSeconds(0)
@@ -238,7 +240,7 @@ class LocalDateTimeTest {
 
     @Test
     fun one_day_gap_for_a_year() {
-        LocalDateTime(year = 2023, monthNumber = 1, dayOfMonth = 1, hour = 0, minute = 0).assertGap(
+        LocalDateTime(year = 2023, month = Month.JANUARY, day = 1, hour = 0, minute = 0).assertGap(
             assertPeriod = DateTimePeriod(days = 1),
             schedule = buildPulseSchedule {
                 atSeconds(0)
@@ -250,7 +252,7 @@ class LocalDateTimeTest {
 
     @Test
     fun one_day_gap_for_two_years() {
-        LocalDateTime(year = 2023, monthNumber = 1, dayOfMonth = 1, hour = 0, minute = 0).assertGap(
+        LocalDateTime(year = 2023, month = Month.JANUARY, day = 1, hour = 0, minute = 0).assertGap(
             assertPeriod = DateTimePeriod(days = 1),
             schedule = buildPulseSchedule {
                 atSeconds(0)
@@ -262,7 +264,7 @@ class LocalDateTimeTest {
 
     @Test
     fun one_hour_gap_for_a_year() {
-        LocalDateTime(year = 2023, monthNumber = 1, dayOfMonth = 1, hour = 0, minute = 0).assertGap(
+        LocalDateTime(year = 2023, month = Month.JANUARY, day = 1, hour = 0, minute = 0).assertGap(
             assertPeriod = DateTimePeriod(hours = 1),
             schedule = buildPulseSchedule {
                 atSeconds(0)
@@ -273,7 +275,7 @@ class LocalDateTimeTest {
 
     @Test
     fun one_minute_gap_for_a_year() {
-        LocalDateTime(year = 2023, monthNumber = 1, dayOfMonth = 1, hour = 0, minute = 0).assertGap(
+        LocalDateTime(year = 2023, month = Month.JANUARY, day = 1, hour = 0, minute = 0).assertGap(
             assertPeriod = DateTimePeriod(minutes = 1),
             schedule = buildPulseSchedule { atSeconds(0) },
         ) { it.year < 2024 }
@@ -281,7 +283,7 @@ class LocalDateTimeTest {
 
     @Test
     fun one_second_gap_for_a_year() {
-        LocalDateTime(year = 2023, monthNumber = 1, dayOfMonth = 1, hour = 0, minute = 0).assertGap(
+        LocalDateTime(year = 2023, month = Month.JANUARY, day = 1, hour = 0, minute = 0).assertGap(
             assertPeriod = DateTimePeriod(seconds = 1),
         ) { it.year < 2024 }
     }
@@ -467,6 +469,7 @@ class LocalDateTimeTest {
     )
 }
 
+@ExperimentalTime
 private fun LocalDateTime.assertGap(
     assertPeriod: DateTimePeriod,
     schedule: PulseSchedule = buildPulseSchedule {  },