ComparableTimeMark and specialized TimeSource KT-54082

- Introduce ComparableTimeMark interface extending TimeMark,
and TimeSource.WithComparableMarks - a time source that returns such time marks.
- Implement ComparableTimeMark in ValueTimeMark and AbstractLong/DoubleTimeMark classes.
This commit is contained in:
Ilya Gorbunov
2022-06-27 23:31:36 +03:00
committed by Space
parent a4b2a8c4ea
commit 57c9d61291
11 changed files with 534 additions and 87 deletions
+90 -22
View File
@@ -26,6 +26,15 @@ public interface TimeSource {
*/
public fun markNow(): TimeMark
/**
* A [TimeSource] that returns [time marks][ComparableTimeMark] that can be compared for difference with each other.
*/
@SinceKotlin("1.8")
@ExperimentalTime
public interface WithComparableMarks : TimeSource {
override fun markNow(): ComparableTimeMark
}
/**
* The most precise time source available in the platform.
*
@@ -35,28 +44,60 @@ public interface TimeSource {
* The function [markNow] of this time source returns the specialized [ValueTimeMark] that is an inline value class
* wrapping a platform-dependent time reading value.
*/
public object Monotonic : TimeSource {
public object Monotonic : TimeSource.WithComparableMarks {
override fun markNow(): ValueTimeMark = MonotonicTimeSource.markNow()
override fun toString(): String = MonotonicTimeSource.toString()
/**
* A specialized [kotlin.time.TimeMark] returned by [TimeSource.Monotonic].
* A specialized [kotlin.time.TimeMark] returned by [TimeSource.Monotonic] time source.
*
* This time mark is implemented as an inline value class wrapping a platform-dependent
* time reading value of the default monotonic time source, thus allowing to avoid additional boxing
* of that value.
*
* The operations [plus] and [minus] are also specialized to return [ValueTimeMark] type.
*
* This time mark implements [ComparableTimeMark] and therefore is comparable with other time marks
* obtained from the same [TimeSource.Monotonic] time source.
*/
@ExperimentalTime
@SinceKotlin("1.7")
@JvmInline
public value class ValueTimeMark internal constructor(internal val reading: ValueTimeMarkReading) : TimeMark {
public value class ValueTimeMark internal constructor(internal val reading: ValueTimeMarkReading) : ComparableTimeMark {
override fun elapsedNow(): Duration = MonotonicTimeSource.elapsedFrom(this)
override fun plus(duration: Duration): ValueTimeMark = MonotonicTimeSource.adjustReading(this, duration)
override fun minus(duration: Duration): ValueTimeMark = MonotonicTimeSource.adjustReading(this, -duration)
override fun hasPassedNow(): Boolean = !elapsedNow().isNegative()
override fun hasNotPassedNow(): Boolean = elapsedNow().isNegative()
override fun minus(other: ComparableTimeMark): Duration {
if (other !is ValueTimeMark)
throw IllegalArgumentException("Subtracting or comparing time marks from different time sources is not possible: $this and $other")
return this.minus(other)
}
/**
* Returns the duration elapsed between the [other] time mark obtained from the same [TimeSource.Monotonic] time source and `this` time mark.
*
* The returned duration can be infinite if the time marks are far away from each other and
* the result doesn't fit into [Duration] type,
* or if one time mark is infinitely distant, or if both `this` and [other] time marks
* lie infinitely distant on the opposite sides of the time scale.
*
* Two infinitely distant time marks on the same side of the time scale are considered equal and
* the duration between them is [Duration.ZERO].
*/
public operator fun minus(other: ValueTimeMark): Duration = MonotonicTimeSource.differenceBetween(this, other)
/**
* Compares this time mark with the [other] time mark for order.
*
* - Returns zero if this time mark represents *the same moment* of time as the [other] time mark.
* - Returns a negative number if this time mark is *earlier* than the [other] time mark.
* - Returns a positive number if this time mark is *later* than the [other] time mark.
*/
public operator fun compareTo(other: ValueTimeMark): Int =
(this - other).compareTo(Duration.ZERO)
}
}
@@ -98,7 +139,7 @@ public interface TimeMark {
* @throws IllegalArgumentException an implementation may throw if a positive infinite duration is added to an infinitely distant past time mark or
* a negative infinite duration is added to an infinitely distant future time mark.
*/
public open operator fun plus(duration: Duration): TimeMark = AdjustedTimeMark(this, duration)
public operator fun plus(duration: Duration): TimeMark = AdjustedTimeMark(this, duration)
/**
* Returns a time mark on the same time source that is behind this time mark by the specified [duration].
@@ -131,26 +172,53 @@ public interface TimeMark {
public fun hasNotPassedNow(): Boolean = elapsedNow().isNegative()
}
/**
* A [TimeMark] that can be compared for difference with other time marks obtained from the same [TimeSource.WithComparableMarks] time source.
*/
@SinceKotlin("1.8")
@ExperimentalTime
@SinceKotlin("1.3")
@kotlin.internal.InlineOnly
@Deprecated(
"Subtracting one TimeMark from another is not a well defined operation because these time marks could have been obtained from the different time sources.",
level = DeprecationLevel.ERROR
)
@Suppress("UNUSED_PARAMETER")
public inline operator fun TimeMark.minus(other: TimeMark): Duration = throw Error("Operation is disallowed.")
public interface ComparableTimeMark : TimeMark, Comparable<ComparableTimeMark> {
public abstract override operator fun plus(duration: Duration): ComparableTimeMark
public open override operator fun minus(duration: Duration): ComparableTimeMark = plus(-duration)
@ExperimentalTime
@SinceKotlin("1.3")
@kotlin.internal.InlineOnly
@Deprecated(
"Comparing one TimeMark to another is not a well defined operation because these time marks could have been obtained from the different time sources.",
level = DeprecationLevel.ERROR
)
@Suppress("UNUSED_PARAMETER")
public inline operator fun TimeMark.compareTo(other: TimeMark): Int = throw Error("Operation is disallowed.")
/**
* Returns the duration elapsed between the [other] time mark and `this` time mark.
*
* The returned duration can be infinite if the time marks are far away from each other and
* the result doesn't fit into [Duration] type,
* or if one time mark is infinitely distant, or if both `this` and [other] time marks
* lie infinitely distant on the opposite sides of the time scale.
*
* Two infinitely distant time marks on the same side of the time scale are considered equal and
* the duration between them is [Duration.ZERO].
*
* Note that the other time mark must be obtained from the same time source as this one.
*
* @throws IllegalArgumentException if time marks were obtained from different time sources.
*/
public operator fun minus(other: ComparableTimeMark): Duration
/**
* Compares this time mark with the [other] time mark for order.
*
* - Returns zero if this time mark represents *the same moment* of time as the [other] time mark.
* - Returns a negative number if this time mark is *earlier* than the [other] time mark.
* - Returns a positive number if this time mark is *later* than the [other] time mark.
*
* Note that the other time mark must be obtained from the same time source as this one.
*
* @throws IllegalArgumentException if time marks were obtained from different time sources.
*/
public override operator fun compareTo(other: ComparableTimeMark): Int =
(this - other).compareTo(Duration.ZERO)
/**
* Returns `true` if two time marks from the same time source represent the same moment of time, and `false` otherwise,
* including the situation when the time marks were obtained from different time sources.
*/
override fun equals(other: Any?): Boolean
override fun hashCode(): Int
}
@ExperimentalTime
+79 -11
View File
@@ -5,11 +5,16 @@
package kotlin.time
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.nanoseconds
import kotlin.time.Duration.Companion.seconds
@SinceKotlin("1.3")
@ExperimentalTime
internal expect object MonotonicTimeSource : TimeSource {
internal expect object MonotonicTimeSource : TimeSource.WithComparableMarks {
override fun markNow(): TimeSource.Monotonic.ValueTimeMark
fun elapsedFrom(timeMark: TimeSource.Monotonic.ValueTimeMark): Duration
fun differenceBetween(one: TimeSource.Monotonic.ValueTimeMark, another: TimeSource.Monotonic.ValueTimeMark): Duration
fun adjustReading(timeMark: TimeSource.Monotonic.ValueTimeMark, duration: Duration): TimeSource.Monotonic.ValueTimeMark
}
@@ -20,19 +25,62 @@ internal expect object MonotonicTimeSource : TimeSource {
*/
@SinceKotlin("1.3")
@ExperimentalTime
public abstract class AbstractLongTimeSource(protected val unit: DurationUnit) : TimeSource {
public abstract class AbstractLongTimeSource(protected val unit: DurationUnit) : TimeSource.WithComparableMarks {
/**
* This protected method should be overridden to return the current reading of the time source expressed as a [Long] number
* in the unit specified by the [unit] property.
*/
protected abstract fun read(): Long
private class LongTimeMark(private val startedAt: Long, private val timeSource: AbstractLongTimeSource, private val offset: Duration) : TimeMark {
override fun elapsedNow(): Duration = (timeSource.read() - startedAt).toDuration(timeSource.unit) - offset
override fun plus(duration: Duration): TimeMark = LongTimeMark(startedAt, timeSource, offset + duration)
private class LongTimeMark(private val startedAt: Long, private val timeSource: AbstractLongTimeSource, private val offset: Duration) : ComparableTimeMark {
override fun elapsedNow(): Duration = if (offset.isInfinite()) -offset else (timeSource.read() - startedAt).toDuration(timeSource.unit) - offset
override fun plus(duration: Duration): ComparableTimeMark = LongTimeMark(startedAt, timeSource, offset + duration)
override fun minus(other: ComparableTimeMark): Duration {
if (other !is LongTimeMark || this.timeSource != other.timeSource)
throw IllegalArgumentException("Subtracting or comparing time marks from different time sources is not possible: $this and $other")
// val thisValue = this.effectiveDuration()
// val otherValue = other.effectiveDuration()
// if (thisValue == otherValue && thisValue.isInfinite()) return Duration.ZERO
// return thisValue - otherValue
if (this.offset == other.offset && this.offset.isInfinite()) return Duration.ZERO
val offsetDiff = this.offset - other.offset
val startedAtDiff = (this.startedAt - other.startedAt).toDuration(timeSource.unit)
// println("$startedAtDiff, $offsetDiff")
return if (startedAtDiff == -offsetDiff) Duration.ZERO else startedAtDiff + offsetDiff
}
override fun equals(other: Any?): Boolean =
other is LongTimeMark && this.timeSource == other.timeSource && (this - other) == Duration.ZERO
internal fun effectiveDuration(): Duration {
if (offset.isInfinite()) return offset
val unit = timeSource.unit
if (unit >= DurationUnit.MILLISECONDS) {
return startedAt.toDuration(unit) + offset
}
val scale = convertDurationUnit(1L, DurationUnit.MILLISECONDS, unit)
val startedAtMillis = startedAt / scale
val startedAtRem = startedAt % scale
return offset.toComponents { offsetSeconds, offsetNanoseconds ->
val offsetMillis = offsetNanoseconds / NANOS_IN_MILLIS
val offsetRemNanos = offsetNanoseconds % NANOS_IN_MILLIS
// add component-wise
(startedAtRem.toDuration(unit) + offsetRemNanos.nanoseconds) +
(startedAtMillis + offsetMillis).milliseconds +
offsetSeconds.seconds
}
}
override fun hashCode(): Int = effectiveDuration().hashCode()
override fun toString(): String = "LongTimeMark($startedAt${timeSource.unit.shortName()} + $offset (=${effectiveDuration()}), $timeSource)"
}
override fun markNow(): TimeMark = LongTimeMark(read(), this, Duration.ZERO)
override fun markNow(): ComparableTimeMark = LongTimeMark(read(), this, Duration.ZERO)
}
/**
@@ -42,19 +90,39 @@ public abstract class AbstractLongTimeSource(protected val unit: DurationUnit) :
*/
@SinceKotlin("1.3")
@ExperimentalTime
public abstract class AbstractDoubleTimeSource(protected val unit: DurationUnit) : TimeSource {
public abstract class AbstractDoubleTimeSource(protected val unit: DurationUnit) : TimeSource.WithComparableMarks {
/**
* This protected method should be overridden to return the current reading of the time source expressed as a [Double] number
* in the unit specified by the [unit] property.
*/
protected abstract fun read(): Double
private class DoubleTimeMark(private val startedAt: Double, private val timeSource: AbstractDoubleTimeSource, private val offset: Duration) : TimeMark {
private class DoubleTimeMark(private val startedAt: Double, private val timeSource: AbstractDoubleTimeSource, private val offset: Duration) : ComparableTimeMark {
override fun elapsedNow(): Duration = (timeSource.read() - startedAt).toDuration(timeSource.unit) - offset
override fun plus(duration: Duration): TimeMark = DoubleTimeMark(startedAt, timeSource, offset + duration)
override fun plus(duration: Duration): ComparableTimeMark = DoubleTimeMark(startedAt, timeSource, offset + duration)
override fun minus(other: ComparableTimeMark): Duration {
if (other !is DoubleTimeMark || this.timeSource != other.timeSource)
throw IllegalArgumentException("Subtracting or comparing time marks from different time sources is not possible: $this and $other")
if (this.offset == other.offset && this.offset.isInfinite()) return Duration.ZERO
val offsetDiff = this.offset - other.offset
val startedAtDiff = (this.startedAt - other.startedAt).toDuration(timeSource.unit)
return if (startedAtDiff == -offsetDiff) Duration.ZERO else startedAtDiff + offsetDiff
}
override fun equals(other: Any?): Boolean {
return other is DoubleTimeMark && this.timeSource == other.timeSource && (this - other) == Duration.ZERO
}
override fun hashCode(): Int {
return (startedAt.toDuration(timeSource.unit) + offset).hashCode()
}
override fun toString(): String = "DoubleTimeMark($startedAt${timeSource.unit.shortName()} + $offset, $timeSource)"
}
override fun markNow(): TimeMark = DoubleTimeMark(read(), this, Duration.ZERO)
override fun markNow(): ComparableTimeMark = DoubleTimeMark(read(), this, Duration.ZERO)
}
/**
@@ -104,6 +172,6 @@ public class TestTimeSource : AbstractLongTimeSource(unit = DurationUnit.NANOSEC
}
private fun overflow(duration: Duration) {
throw IllegalStateException("TestTimeSource will overflow if its reading ${reading}ns is advanced by $duration.")
throw IllegalStateException("TestTimeSource will overflow if its reading ${reading}${unit.shortName()} is advanced by $duration.")
}
}
@@ -45,10 +45,25 @@ internal fun saturatingDiff(valueNs: Long, originNs: Long): Duration {
if (originNs.isSaturated()) { // MIN_VALUE or MAX_VALUE
return -(originNs.toDuration(DurationUnit.DAYS)) // saturate to infinity
}
val result = valueNs - originNs
if ((result xor valueNs) and (result xor originNs).inv() < 0) {
val resultMs = valueNs / NANOS_IN_MILLIS - originNs / NANOS_IN_MILLIS
val resultNs = valueNs % NANOS_IN_MILLIS - originNs % NANOS_IN_MILLIS
return saturatingFiniteDiff(valueNs, originNs)
}
internal fun saturatingOriginsDiff(origin1Ns: Long, origin2Ns: Long): Duration {
if (origin2Ns.isSaturated()) { // MIN_VALUE or MAX_VALUE
if (origin1Ns == origin2Ns) return Duration.ZERO // saturated values of the same sign are considered equal
return -(origin2Ns.toDuration(DurationUnit.DAYS)) // saturate to infinity
}
if (origin1Ns.isSaturated()) {
return origin1Ns.toDuration(DurationUnit.DAYS)
}
return saturatingFiniteDiff(origin1Ns, origin2Ns)
}
private fun saturatingFiniteDiff(value1Ns: Long, value2Ns: Long): Duration {
val result = value1Ns - value2Ns
if ((result xor value1Ns) and (result xor value2Ns).inv() < 0) {
val resultMs = value1Ns / NANOS_IN_MILLIS - value2Ns / NANOS_IN_MILLIS
val resultNs = value1Ns % NANOS_IN_MILLIS - value2Ns % NANOS_IN_MILLIS
return resultMs.milliseconds + resultNs.nanoseconds
}
return result.nanoseconds