Skip to content

Commit c757dbf

Browse files
authored
Implement Comparable time marks in a time source returned by Clock.asTimeSource() (#271)
1 parent 8d26763 commit c757dbf

File tree

2 files changed

+125
-6
lines changed

2 files changed

+125
-6
lines changed

core/common/src/Clock.kt

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,50 @@ public fun Clock.todayIn(timeZone: TimeZone): LocalDate =
4040
* Returns a [TimeSource] that uses this [Clock] to mark a time instant and to find the amount of time elapsed since that mark.
4141
*/
4242
@ExperimentalTime
43-
public fun Clock.asTimeSource(): TimeSource = object : TimeSource {
44-
override fun markNow(): TimeMark = InstantTimeMark(now(), this@asTimeSource)
43+
public fun Clock.asTimeSource(): TimeSource.WithComparableMarks = object : TimeSource.WithComparableMarks {
44+
override fun markNow(): ComparableTimeMark = InstantTimeMark(now(), this@asTimeSource)
4545
}
4646

4747
@ExperimentalTime
48-
private class InstantTimeMark(private val instant: Instant, private val clock: Clock) : TimeMark {
49-
override fun elapsedNow(): Duration = clock.now() - instant
48+
private class InstantTimeMark(private val instant: Instant, private val clock: Clock) : ComparableTimeMark {
49+
override fun elapsedNow(): Duration = saturatingDiff(clock.now(), instant)
5050

51-
override fun plus(duration: Duration): TimeMark = InstantTimeMark(instant + duration, clock)
51+
override fun plus(duration: Duration): ComparableTimeMark = InstantTimeMark(instant.saturatingAdd(duration), clock)
52+
override fun minus(duration: Duration): ComparableTimeMark = InstantTimeMark(instant.saturatingAdd(-duration), clock)
5253

53-
override fun minus(duration: Duration): TimeMark = InstantTimeMark(instant - duration, clock)
54+
override fun minus(other: ComparableTimeMark): Duration {
55+
if (other !is InstantTimeMark || other.clock != this.clock) {
56+
throw IllegalArgumentException("Subtracting or comparing time marks from different time sources is not possible: $this and $other")
57+
}
58+
return saturatingDiff(this.instant, other.instant)
59+
}
60+
61+
override fun equals(other: Any?): Boolean {
62+
return other is InstantTimeMark && this.clock == other.clock && this.instant == other.instant
63+
}
64+
65+
override fun hashCode(): Int = instant.hashCode()
66+
67+
override fun toString(): String = "InstantTimeMark($instant, $clock)"
68+
69+
private fun Instant.isSaturated() = this == Instant.MAX || this == Instant.MIN
70+
private fun Instant.saturatingAdd(duration: Duration): Instant {
71+
if (isSaturated()) {
72+
if (duration.isInfinite() && duration.isPositive() != this.isDistantFuture) {
73+
throw IllegalArgumentException("Summing infinities of different signs")
74+
}
75+
return this
76+
}
77+
return this + duration
78+
}
79+
private fun saturatingDiff(instant1: Instant, instant2: Instant): Duration = when {
80+
instant1 == instant2 ->
81+
Duration.ZERO
82+
instant1.isSaturated() || instant2.isSaturated() ->
83+
(instant1 - instant2) * Double.POSITIVE_INFINITY
84+
else ->
85+
instant1 - instant2
86+
}
5487
}
5588

5689
@Deprecated("Use Clock.todayIn instead", ReplaceWith("this.todayIn(timeZone)"), DeprecationLevel.WARNING)
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2019-2023 JetBrains s.r.o. and contributors.
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.test
7+
8+
import kotlinx.datetime.*
9+
import kotlin.test.*
10+
import kotlin.time.*
11+
import kotlin.time.Duration.Companion.days
12+
import kotlin.time.Duration.Companion.nanoseconds
13+
14+
@OptIn(ExperimentalTime::class)
15+
class ClockTimeSourceTest {
16+
@Test
17+
fun arithmetic() {
18+
val timeSource = Clock.System.asTimeSource()
19+
val mark0 = timeSource.markNow()
20+
21+
val markPast = mark0 - 1.days
22+
val markFuture = mark0 + 1.days
23+
24+
assertTrue(markPast < mark0)
25+
assertTrue(markFuture > mark0)
26+
assertEquals(mark0, markPast + 1.days)
27+
assertEquals(2.days, markFuture - markPast)
28+
}
29+
30+
@Test
31+
fun elapsed() {
32+
val clock = object : Clock {
33+
var instant = Clock.System.now()
34+
override fun now(): Instant = instant
35+
}
36+
val timeSource = clock.asTimeSource()
37+
val mark = timeSource.markNow()
38+
assertEquals(Duration.ZERO, mark.elapsedNow())
39+
40+
clock.instant += 1.days
41+
assertEquals(1.days, mark.elapsedNow())
42+
43+
clock.instant -= 2.days
44+
assertEquals(-1.days, mark.elapsedNow())
45+
46+
clock.instant = Instant.MAX
47+
assertEquals(Duration.INFINITE, mark.elapsedNow())
48+
}
49+
50+
@Test
51+
fun differentSources() {
52+
val mark1 = Clock.System.asTimeSource().markNow()
53+
val mark2 = object : Clock {
54+
override fun now(): Instant = Instant.DISTANT_FUTURE
55+
}.asTimeSource().markNow()
56+
assertNotEquals(mark1, mark2)
57+
assertFailsWith<IllegalArgumentException> { mark1 - mark2 }
58+
assertFailsWith<IllegalArgumentException> { mark1 compareTo mark2 }
59+
}
60+
61+
@Test
62+
fun saturation() {
63+
val mark0 = Clock.System.asTimeSource().markNow()
64+
65+
val markFuture = mark0 + Duration.INFINITE
66+
val markPast = mark0 - Duration.INFINITE
67+
68+
for (delta in listOf(Duration.ZERO, 1.nanoseconds, 1.days)) {
69+
assertEquals(markFuture, markFuture - delta)
70+
assertEquals(markFuture, markFuture + delta)
71+
72+
assertEquals(markPast, markPast - delta)
73+
assertEquals(markPast, markPast + delta)
74+
}
75+
val infinitePairs = listOf(markFuture to markPast, markFuture to mark0, mark0 to markPast)
76+
for ((later, earlier) in infinitePairs) {
77+
assertEquals(Duration.INFINITE, later - earlier)
78+
assertEquals(-Duration.INFINITE, earlier - later)
79+
}
80+
assertEquals(Duration.ZERO, markFuture - markFuture)
81+
assertEquals(Duration.ZERO, markPast - markPast)
82+
83+
assertFailsWith<IllegalArgumentException> { markFuture - Duration.INFINITE }
84+
assertFailsWith<IllegalArgumentException> { markPast + Duration.INFINITE }
85+
}
86+
}

0 commit comments

Comments
 (0)