Store Duration value as a multi-range Long

Now precision loss happens at bigger durations.

This changes the binary API due different underlying type of Duration.
This commit is contained in:
Ilya Gorbunov
2021-02-17 20:15:18 +03:00
parent 3f4fa7eb98
commit fa7460ba48
9 changed files with 596 additions and 182 deletions
+258 -41
View File
@@ -8,9 +8,20 @@ package kotlin.time
import kotlin.contracts.*
import kotlin.jvm.JvmInline
import kotlin.math.abs
import kotlin.math.roundToInt
import kotlin.math.sign
@OptIn(ExperimentalTime::class)
private inline val storageUnit get() = DurationUnit.NANOSECONDS
private const val MAX_LONG_63 = Long.MAX_VALUE / 2
private const val MIN_LONG_63 = Long.MIN_VALUE / 2
private const val NANOS_IN_MILLIS = 1_000_000
private const val MAX_MILLIS_IN_NANOS = MAX_LONG_63 / NANOS_IN_MILLIS
private const val MIN_MILLIS_IN_NANOS = MIN_LONG_63 / NANOS_IN_MILLIS
private fun nanosToMillis(value: Long): Long = value / NANOS_IN_MILLIS
private fun millisToNanos(value: Long): Long = value * NANOS_IN_MILLIS
private fun storeAsMillis(value: Long): Long = (value shl 1) + 1
private fun storeAsNanos(value: Long): Long = value shl 1
private fun storeAs(value: Long, unitValue: Long): Long = (value shl 1) + (unitValue and 1)
/**
* Represents the amount of time one instant of time is away from another instant.
@@ -18,7 +29,10 @@ private inline val storageUnit get() = DurationUnit.NANOSECONDS
* A negative duration is possible in a situation when the second instant is earlier than the first one.
* An infinite duration value [Duration.INFINITE] can be used to represent infinite timeouts.
*
* To construct a duration use either the extension function [toDuration] available on [Int], [Long], and [Double] numeric types,
* The type can store duration values up to ±146 years with nanosecond precision,
* and up to ±146 million years with millisecond precision.
*
* To construct a duration, use either the extension function [toDuration] available on [Int], [Long], and [Double] numeric types,
* or the `Duration` companion object functions [Duration.hours], [Duration.minutes], [Duration.seconds], and so on,
* taking [Int], [Long], or [Double] numbers as parameters.
*
@@ -29,17 +43,23 @@ private inline val storageUnit get() = DurationUnit.NANOSECONDS
@SinceKotlin("1.3")
@ExperimentalTime
@JvmInline
public value class Duration internal constructor(internal val value: Double) : Comparable<Duration> {
public value class Duration internal constructor(internal val rawValue: Long) : Comparable<Duration> {
private fun isInNanos() = rawValue and 1L == 0L
private fun isInMillis() = rawValue and 1L == 1L
private val value: Long get() = rawValue shr 1
private val storageUnit get() = if (isInNanos()) DurationUnit.NANOSECONDS else DurationUnit.MILLISECONDS
init {
require(!value.isNaN()) { "Duration value cannot be NaN." }
// TODO: assert invariants
}
companion object {
/** The duration equal to exactly 0 seconds. */
public val ZERO: Duration = Duration(0.0)
public val ZERO: Duration = Duration(0L)
/** The duration whose value is positive infinity. It is useful for representing timeouts that should never expire. */
public val INFINITE: Duration = Duration(Double.POSITIVE_INFINITY)
public val INFINITE: Duration = Duration(storeAsMillis(MAX_LONG_63))
/** Converts the given time duration [value] expressed in the specified [sourceUnit] into the specified [targetUnit]. */
public fun convert(value: Double, sourceUnit: DurationUnit, targetUnit: DurationUnit): Double =
@@ -162,69 +182,218 @@ public value class Duration internal constructor(internal val value: Double) : C
// arithmetic operators
/** Returns the negative of this value. */
public operator fun unaryMinus(): Duration = Duration((-value).normalizeZero())
public operator fun unaryMinus(): Duration {
val value = value
return when {
rawValue == INFINITE.rawValue -> // negate positive infinite to negative infinite
Duration(storeAsMillis(MIN_LONG_63))
value != MIN_LONG_63 ->
Duration(storeAs(-value, rawValue))
isInNanos() -> // negating MIN_VALUE in nanos overflows to millisecond units
Duration(storeAsMillis(-nanosToMillis(value)))
else -> // negating negative infinity overflows to positive infinity
INFINITE
}
}
/**
* Returns a duration whose value is the sum of this and [other] duration values.
*
* @throws IllegalArgumentException if the operation results in a `NaN` value.
* @throws IllegalArgumentException if the operation results in an undefined value for the given arguments,
* e.g. when adding infinite durations of different sign.
*/
public operator fun plus(other: Duration): Duration = Duration(value + other.value)
public operator fun plus(other: Duration): Duration {
when {
this.isInfinite() -> {
if (other.isFinite() || (this.rawValue xor other.rawValue >= 0))
return this
else
throw IllegalArgumentException("Sum of infinite durations of different signs is undefined.")
}
other.isInfinite() -> return other
}
return if (this.isInNanos() == other.isInNanos()) {
when (val newValue = this.value + other.value) { // never overflows long, but can overflow long63
in MIN_LONG_63..MAX_LONG_63 -> {
if (isInMillis() && newValue in MIN_MILLIS_IN_NANOS..MAX_MILLIS_IN_NANOS)
Duration(storeAsNanos(millisToNanos(newValue)))
else
Duration(storeAs(newValue, this.rawValue))
}
else -> {
val millisValue = if (isInNanos()) nanosToMillis(newValue) else newValue.coerceIn(MIN_LONG_63, MAX_LONG_63)
Duration(storeAsMillis(millisValue))
}
}
} else {
val a: Long
val b: Long
if (this.isInMillis()) {
a = this.value
b = other.value
} else {
a = other.value
b = this.value
}
val bMillis = nanosToMillis(b)
val resultMillis = a + bMillis
return if (resultMillis in MIN_MILLIS_IN_NANOS..MAX_MILLIS_IN_NANOS) {
val bRemainder = b - millisToNanos(bMillis)
Duration(storeAsNanos(millisToNanos(resultMillis) + bRemainder)) // can it overflow nanos?
} else {
Duration(storeAsMillis(resultMillis.coerceIn(MIN_LONG_63, MAX_LONG_63)))
}
}
}
/**
* Returns a duration whose value is the difference between this and [other] duration values.
*
* @throws IllegalArgumentException if the operation results in a `NaN` value.
* @throws IllegalArgumentException if the operation results in an undefined value for the given arguments,
* e.g. when subtracting infinite durations of the same sign.
*/
public operator fun minus(other: Duration): Duration = Duration(value - other.value)
// TODO: negation may overflow
public operator fun minus(other: Duration): Duration = this + (-other)
/**
* Returns a duration whose value is this duration value multiplied by the given [scale] number.
*
* @throws IllegalArgumentException if the operation results in a `NaN` value.
* @throws IllegalArgumentException if the operation results in an undefined value for the given arguments,
* e.g. when multiplying an infinite duration by zero.
*/
public operator fun times(scale: Int): Duration = Duration((value * scale).normalizeZero())
public operator fun times(scale: Int): Duration {
if (isInfinite()) {
return when {
scale == 0 -> throw IllegalArgumentException("Multiplying infinite duration by zero is undefined.")
scale > 0 -> this
else -> -this
}
}
if (scale == 0) return ZERO
val result = value * scale
if (value >= Int.MIN_VALUE && value <= Int.MAX_VALUE) {
return Duration(storeAs(result, rawValue))
}
if (result / scale != value && isInNanos()) { // Long overflow
val rawMillis = nanosToMillis(value)
val resultMillis = rawMillis * scale
if (resultMillis / scale != rawMillis) return Duration.INFINITE
return when {
resultMillis in MIN_LONG_63..MAX_LONG_63 -> Duration(storeAsMillis(resultMillis))
resultMillis > 0 -> Duration.INFINITE
else -> -Duration.INFINITE
}
}
return when {
result in MIN_LONG_63..MAX_LONG_63 -> {
Duration(storeAs(result, rawValue))
}
isInNanos() -> {
Duration(storeAsMillis(nanosToMillis(result)))
}
result > 0 -> Duration.INFINITE
else -> -Duration.INFINITE
}
}
/**
* Returns a duration whose value is this duration value multiplied by the given [scale] number.
*
* @throws IllegalArgumentException if the operation results in a `NaN` value.
* @throws IllegalArgumentException if the operation results in an undefined value for the given arguments,
* e.g. when multiplying an infinite duration by zero.
*/
public operator fun times(scale: Double): Duration = Duration((value * scale).normalizeZero())
public operator fun times(scale: Double): Duration {
val intScale = scale.roundToInt()
if (intScale.toDouble() == scale) {
return times(intScale)
}
val unit = storageUnit
val result = toDouble(unit) * scale
return result.toDuration(unit)
}
/**
* Returns a duration whose value is this duration value divided by the given [scale] number.
*
* @throws IllegalArgumentException if the operation results in a `NaN` value.
* @throws IllegalArgumentException if the operation results in an undefined value for the given arguments,
* e.g. when dividing zero duration by zero.
*/
public operator fun div(scale: Int): Duration = Duration((value / scale).normalizeZero())
public operator fun div(scale: Int): Duration {
if (scale == 0) {
return when {
isPositive() -> Duration.INFINITE
isNegative() -> -Duration.INFINITE
else -> throw IllegalArgumentException("Dividing zero duration by zero is undefined")
}
}
if (isInNanos()) {
return Duration(storeAsNanos(value / scale))
} else {
if (isInfinite())
return this * scale.sign
val result = value / scale
if (result in MIN_MILLIS_IN_NANOS..MAX_MILLIS_IN_NANOS) {
// TODO: more precise
return Duration(storeAsNanos(millisToNanos(result)))
}
return Duration(storeAsMillis(result))
}
}
/**
* Returns a duration whose value is this duration value divided by the given [scale] number.
*
* @throws IllegalArgumentException if the operation results in a `NaN` value.
* @throws IllegalArgumentException if the operation results in an undefined value for the given arguments,
* e.g. when dividing an infinite duration by infinity or zero duration by zero.
*/
public operator fun div(scale: Double): Duration = Duration((value / scale).normalizeZero())
public operator fun div(scale: Double): Duration {
if (scale != 0.0) {
val intScale = scale.roundToInt()
if (intScale.toDouble() == scale) {
return div(intScale)
}
}
val unit = storageUnit
val result = toDouble(unit) / scale
return result.toDuration(unit)
}
/** Returns a number that is the ratio of this and [other] duration values. */
public operator fun div(other: Duration): Double = this.value / other.value
public operator fun div(other: Duration): Double {
val coarserUnit = maxOf(this.storageUnit, other.storageUnit)
return this.toDouble(coarserUnit) / other.toDouble(coarserUnit)
}
/** Returns true, if the duration value is less than zero. */
public fun isNegative(): Boolean = value < 0
public fun isNegative(): Boolean = rawValue < 0
/** Returns true, if the duration value is greater than zero. */
public fun isPositive(): Boolean = value > 0
public fun isPositive(): Boolean = rawValue > 0
/** Returns true, if the duration value is infinite. */
public fun isInfinite(): Boolean = value.isInfinite()
public fun isInfinite(): Boolean = isInMillis() && value.let { it == MAX_LONG_63 || it == MIN_LONG_63 }
/** Returns true, if the duration value is finite. */
public fun isFinite(): Boolean = value.isFinite()
public fun isFinite(): Boolean = !isInfinite()
/** Returns the absolute value of this value. The returned value is always non-negative. */
public val absoluteValue: Duration get() = if (isNegative()) -this else this
override fun compareTo(other: Duration): Int = this.value.compareTo(other.value)
override fun compareTo(other: Duration): Int {
val compareBits = this.rawValue xor other.rawValue
if (compareBits < 0 || compareBits and 1L == 0L) // different signs or same sign, same scale
return this.rawValue.compareTo(other.rawValue)
// same sign, different scales
val r = (this.rawValue and 1L).toInt() - (other.rawValue and 1L).toInt()
return if (this.rawValue < 0) -r else r
}
// splitting to components
@@ -304,21 +473,37 @@ public value class Duration internal constructor(internal val value: Double) : C
// conversion to units
/** Returns the value of this duration expressed as a [Double] number of the specified [unit]. */
public fun toDouble(unit: DurationUnit): Double = convertDurationUnit(value, storageUnit, unit)
public fun toDouble(unit: DurationUnit): Double {
return if (isInfinite()) {
if (isNegative()) Double.NEGATIVE_INFINITY else Double.POSITIVE_INFINITY
} else {
// TODO: whether it's ok to convert to Double before scaling
convertDurationUnit(value.toDouble(), storageUnit, unit)
}
}
/**
* Returns the value of this duration expressed as a [Long] number of the specified [unit].
*
* If the value doesn't fit in the range of [Long] type, it is coerced into that range, see the conversion [Double.toLong] for details.
*/
public fun toLong(unit: DurationUnit): Long = toDouble(unit).toLong()
public fun toLong(unit: DurationUnit): Long {
return if (isInfinite()) {
if (isNegative()) Long.MIN_VALUE else Long.MAX_VALUE
} else {
convertDurationUnit(value, storageUnit, unit)
}
}
/**
* Returns the value of this duration expressed as an [Int] number of the specified [unit].
*
* If the value doesn't fit in the range of [Int] type, it is coerced into that range, see the conversion [Double.toInt] for details.
*/
public fun toInt(unit: DurationUnit): Int = toDouble(unit).toInt()
public fun toInt(unit: DurationUnit): Int =
toLong(unit).coerceIn(Int.MIN_VALUE.toLong(), Int.MAX_VALUE.toLong()).toInt()
// TODO: inWholeUnits conversions, deprecate inUnits
/** The value of this duration expressed as a [Double] number of days. */
public val inDays: Double get() = toDouble(DurationUnit.DAYS)
@@ -350,7 +535,16 @@ public value class Duration internal constructor(internal val value: Double) : C
*
* The range of durations that can be expressed as a `Long` number of nanoseconds is approximately ±292 years.
*/
public fun toLongNanoseconds(): Long = toLong(DurationUnit.NANOSECONDS)
// TODO: Deprecate in favor inWholeNanoseconds
public fun toLongNanoseconds(): Long {
val value = value
return when {
isInNanos() -> value
value > Long.MAX_VALUE / NANOS_IN_MILLIS -> Long.MAX_VALUE
value < Long.MIN_VALUE / NANOS_IN_MILLIS -> Long.MIN_VALUE
else -> millisToNanos(value)
}
}
/**
* Returns the value of this duration expressed as a [Long] number of milliseconds.
@@ -359,7 +553,10 @@ public value class Duration internal constructor(internal val value: Double) : C
*
* The range of durations that can be expressed as a `Long` number of milliseconds is approximately ±292 million years.
*/
public fun toLongMilliseconds(): Long = toLong(DurationUnit.MILLISECONDS)
// TODO: Deprecate in favor inWholeMilliseconds
public fun toLongMilliseconds(): Long {
return if (isInMillis() && isFinite()) value else toLong(DurationUnit.MILLISECONDS)
}
/**
* Returns a string representation of this duration value expressed in the unit which yields the most compact and readable number value.
@@ -375,8 +572,8 @@ public value class Duration internal constructor(internal val value: Double) : C
* @sample samples.time.Durations.toStringDefault
*/
override fun toString(): String = when {
isInfinite() -> value.toString()
value == 0.0 -> "0s"
isInfinite() -> if (isNegative()) "-Infinity" else "Infinity"
rawValue == 0L -> "0s"
else -> {
val absNs = absoluteValue.inNanoseconds
var scientific = false
@@ -424,8 +621,8 @@ public value class Duration internal constructor(internal val value: Double) : C
*/
public fun toString(unit: DurationUnit, decimals: Int = 0): String {
require(decimals >= 0) { "decimals must be not negative, but was $decimals" }
if (isInfinite()) return value.toString()
val number = toDouble(unit)
if (number.isInfinite()) return number.toString()
return when {
abs(number) < 1e14 -> formatToExactDecimals(number, decimals.coerceAtMost(12))
else -> formatScientific(number)
@@ -485,12 +682,28 @@ public value class Duration internal constructor(internal val value: Double) : C
/** Returns a [Duration] equal to this [Int] number of the specified [unit]. */
@SinceKotlin("1.3")
@ExperimentalTime
public fun Int.toDuration(unit: DurationUnit): Duration = toDouble().toDuration(unit)
public fun Int.toDuration(unit: DurationUnit): Duration {
return if (unit <= DurationUnit.SECONDS) {
Duration(storeAsNanos(convertDurationUnitOverflow(this.toLong(), unit, DurationUnit.NANOSECONDS)))
} else
toLong().toDuration(unit)
}
/** Returns a [Duration] equal to this [Long] number of the specified [unit]. */
@SinceKotlin("1.3")
@ExperimentalTime
public fun Long.toDuration(unit: DurationUnit): Duration = toDouble().toDuration(unit)
public fun Long.toDuration(unit: DurationUnit): Duration {
val maxInUnit = convertDurationUnitOverflow(MAX_LONG_63, DurationUnit.NANOSECONDS, unit)
if (this in -maxInUnit..maxInUnit) {
return Duration(storeAsNanos(convertDurationUnitOverflow(this, unit, DurationUnit.NANOSECONDS)))
} else {
val max2InUnit = convertDurationUnitOverflow(MAX_LONG_63, DurationUnit.MILLISECONDS, unit)
if (unit < DurationUnit.MILLISECONDS || this in -max2InUnit..max2InUnit) {
return Duration(storeAsMillis(convertDurationUnitOverflow(this, unit, DurationUnit.MILLISECONDS)))
}
return if (this < 0) -Duration.INFINITE else Duration.INFINITE
}
}
/**
* Returns a [Duration] equal to this [Double] number of the specified [unit].
@@ -499,7 +712,15 @@ public fun Long.toDuration(unit: DurationUnit): Duration = toDouble().toDuration
*/
@SinceKotlin("1.3")
@ExperimentalTime
public fun Double.toDuration(unit: DurationUnit): Duration = Duration(convertDurationUnit(this, unit, storageUnit).normalizeZero())
public fun Double.toDuration(unit: DurationUnit): Duration {
val valueInNs = convertDurationUnit(this, unit, DurationUnit.NANOSECONDS)
require(!valueInNs.isNaN()) { "Duration value cannot be NaN." }
return if (valueInNs > MAX_LONG_63 || valueInNs < MIN_LONG_63) {
Duration(storeAsMillis((valueInNs / NANOS_IN_MILLIS).toLong().coerceIn(MIN_LONG_63, MAX_LONG_63)))
} else {
Duration(storeAsNanos(valueInNs.toLong()))
}
}
// constructing from number of units
// extension properties
@@ -675,10 +896,6 @@ public inline operator fun Int.times(duration: Duration): Duration = duration *
@kotlin.internal.InlineOnly
public inline operator fun Double.times(duration: Duration): Duration = duration * this
@kotlin.internal.InlineOnly
private inline fun Double.normalizeZero(): Double = this + 0.0
internal expect fun formatToExactDecimals(value: Double, decimals: Int): String
internal expect fun formatUpToDecimals(value: Double, decimals: Int): String
internal expect fun formatScientific(value: Double): String
@@ -1,5 +1,5 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
@@ -52,6 +52,16 @@ public expect enum class DurationUnit {
@ExperimentalTime
internal expect fun convertDurationUnit(value: Double, sourceUnit: DurationUnit, targetUnit: DurationUnit): Double
// overflown result is unspecified
@SinceKotlin("1.5")
@ExperimentalTime
internal expect fun convertDurationUnitOverflow(value: Long, sourceUnit: DurationUnit, targetUnit: DurationUnit): Long
// overflown result is coerced in the Long range boundaries
@SinceKotlin("1.5")
@ExperimentalTime
internal expect fun convertDurationUnit(value: Long, sourceUnit: DurationUnit, targetUnit: DurationUnit): Long
@SinceKotlin("1.3")
@ExperimentalTime