diff --git a/core/common/src/Clock.kt b/core/common/src/Clock.kt index 342888e1b..7849d6700 100644 --- a/core/common/src/Clock.kt +++ b/core/common/src/Clock.kt @@ -40,17 +40,50 @@ public fun Clock.todayIn(timeZone: TimeZone): LocalDate = * Returns a [TimeSource] that uses this [Clock] to mark a time instant and to find the amount of time elapsed since that mark. */ @ExperimentalTime -public fun Clock.asTimeSource(): TimeSource = object : TimeSource { - override fun markNow(): TimeMark = InstantTimeMark(now(), this@asTimeSource) +public fun Clock.asTimeSource(): TimeSource.WithComparableMarks = object : TimeSource.WithComparableMarks { + override fun markNow(): ComparableTimeMark = InstantTimeMark(now(), this@asTimeSource) } @ExperimentalTime -private class InstantTimeMark(private val instant: Instant, private val clock: Clock) : TimeMark { - override fun elapsedNow(): Duration = clock.now() - instant +private class InstantTimeMark(private val instant: Instant, private val clock: Clock) : ComparableTimeMark { + override fun elapsedNow(): Duration = saturatingDiff(clock.now(), instant) - override fun plus(duration: Duration): TimeMark = InstantTimeMark(instant + duration, clock) + override fun plus(duration: Duration): ComparableTimeMark = InstantTimeMark(instant.saturatingAdd(duration), clock) + override fun minus(duration: Duration): ComparableTimeMark = InstantTimeMark(instant.saturatingAdd(-duration), clock) - override fun minus(duration: Duration): TimeMark = InstantTimeMark(instant - duration, clock) + override fun minus(other: ComparableTimeMark): Duration { + if (other !is InstantTimeMark || other.clock != this.clock) { + throw IllegalArgumentException("Subtracting or comparing time marks from different time sources is not possible: $this and $other") + } + return saturatingDiff(this.instant, other.instant) + } + + override fun equals(other: Any?): Boolean { + return other is InstantTimeMark && this.clock == other.clock && this.instant == other.instant + } + + override fun hashCode(): Int = instant.hashCode() + + override fun toString(): String = "InstantTimeMark($instant, $clock)" + + private fun Instant.isSaturated() = this == Instant.MAX || this == Instant.MIN + private fun Instant.saturatingAdd(duration: Duration): Instant { + if (isSaturated()) { + if (duration.isInfinite() && duration.isPositive() != this.isDistantFuture) { + throw IllegalArgumentException("Summing infinities of different signs") + } + return this + } + return this + duration + } + private fun saturatingDiff(instant1: Instant, instant2: Instant): Duration = when { + instant1 == instant2 -> + Duration.ZERO + instant1.isSaturated() || instant2.isSaturated() -> + (instant1 - instant2) * Double.POSITIVE_INFINITY + else -> + instant1 - instant2 + } } @Deprecated("Use Clock.todayIn instead", ReplaceWith("this.todayIn(timeZone)"), DeprecationLevel.WARNING) diff --git a/core/common/test/ClockTimeSourceTest.kt b/core/common/test/ClockTimeSourceTest.kt new file mode 100644 index 000000000..ae2261727 --- /dev/null +++ b/core/common/test/ClockTimeSourceTest.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2019-2023 JetBrains s.r.o. and contributors. + * 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.test + +import kotlinx.datetime.* +import kotlin.test.* +import kotlin.time.* +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.nanoseconds + +@OptIn(ExperimentalTime::class) +class ClockTimeSourceTest { + @Test + fun arithmetic() { + val timeSource = Clock.System.asTimeSource() + val mark0 = timeSource.markNow() + + val markPast = mark0 - 1.days + val markFuture = mark0 + 1.days + + assertTrue(markPast < mark0) + assertTrue(markFuture > mark0) + assertEquals(mark0, markPast + 1.days) + assertEquals(2.days, markFuture - markPast) + } + + @Test + fun elapsed() { + val clock = object : Clock { + var instant = Clock.System.now() + override fun now(): Instant = instant + } + val timeSource = clock.asTimeSource() + val mark = timeSource.markNow() + assertEquals(Duration.ZERO, mark.elapsedNow()) + + clock.instant += 1.days + assertEquals(1.days, mark.elapsedNow()) + + clock.instant -= 2.days + assertEquals(-1.days, mark.elapsedNow()) + + clock.instant = Instant.MAX + assertEquals(Duration.INFINITE, mark.elapsedNow()) + } + + @Test + fun differentSources() { + val mark1 = Clock.System.asTimeSource().markNow() + val mark2 = object : Clock { + override fun now(): Instant = Instant.DISTANT_FUTURE + }.asTimeSource().markNow() + assertNotEquals(mark1, mark2) + assertFailsWith { mark1 - mark2 } + assertFailsWith { mark1 compareTo mark2 } + } + + @Test + fun saturation() { + val mark0 = Clock.System.asTimeSource().markNow() + + val markFuture = mark0 + Duration.INFINITE + val markPast = mark0 - Duration.INFINITE + + for (delta in listOf(Duration.ZERO, 1.nanoseconds, 1.days)) { + assertEquals(markFuture, markFuture - delta) + assertEquals(markFuture, markFuture + delta) + + assertEquals(markPast, markPast - delta) + assertEquals(markPast, markPast + delta) + } + val infinitePairs = listOf(markFuture to markPast, markFuture to mark0, mark0 to markPast) + for ((later, earlier) in infinitePairs) { + assertEquals(Duration.INFINITE, later - earlier) + assertEquals(-Duration.INFINITE, earlier - later) + } + assertEquals(Duration.ZERO, markFuture - markFuture) + assertEquals(Duration.ZERO, markPast - markPast) + + assertFailsWith { markFuture - Duration.INFINITE } + assertFailsWith { markPast + Duration.INFINITE } + } +} \ No newline at end of file