Make Duration storage ranges symmetric and non-overlapping

Increase accuracy of multiplication and division.
This commit is contained in:
Ilya Gorbunov
2021-03-28 07:47:04 +03:00
parent 4fd2254f3f
commit 11d15f3343
2 changed files with 221 additions and 153 deletions
+140 -126
View File
@@ -11,18 +11,6 @@ import kotlin.math.abs
import kotlin.math.roundToInt
import kotlin.math.sign
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.
*
@@ -43,15 +31,22 @@ private fun storeAs(value: Long, unitValue: Long): Long = (value shl 1) + (unitV
@SinceKotlin("1.3")
@ExperimentalTime
@JvmInline
public value class Duration internal constructor(internal val rawValue: Long) : Comparable<Duration> {
public value class Duration internal constructor(private 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 inline val unitDiscriminator: Int get() = rawValue.toInt() and 1
private fun isInNanos() = unitDiscriminator == 0
private fun isInMillis() = unitDiscriminator == 1
private val storageUnit get() = if (isInNanos()) DurationUnit.NANOSECONDS else DurationUnit.MILLISECONDS
init {
// TODO: assert invariants
// TODO: disable assertions in final version
if (isInNanos()) {
if (value !in -MAX_NANOS..MAX_NANOS) throw AssertionError("$value ns is out of nanoseconds range")
} else {
if (value !in -MAX_MILLIS..MAX_MILLIS) throw AssertionError("$value ms is out of milliseconds range")
if (value in -MAX_NANOS_IN_MILLIS..MAX_NANOS_IN_MILLIS) throw AssertionError("$value ms is denormalized")
}
}
companion object {
@@ -59,7 +54,8 @@ public value class Duration internal constructor(internal val rawValue: Long) :
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(storeAsMillis(MAX_LONG_63))
public val INFINITE: Duration = durationOfMillis(MAX_MILLIS)
internal val NEG_INFINITE: Duration = durationOfMillis(-MAX_MILLIS)
/** 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 =
@@ -182,19 +178,7 @@ public value class Duration internal constructor(internal val rawValue: Long) :
// arithmetic operators
/** Returns the negative of this value. */
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
}
}
public operator fun unaryMinus(): Duration = durationOf(-value, unitDiscriminator)
/**
* Returns a duration whose value is the sum of this and [other] duration values.
@@ -208,43 +192,36 @@ public value class Duration internal constructor(internal val rawValue: Long) :
if (other.isFinite() || (this.rawValue xor other.rawValue >= 0))
return this
else
throw IllegalArgumentException("Sum of infinite durations of different signs is undefined.")
throw IllegalArgumentException("Summing infinite durations of different signs yields an undefined result.")
}
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))
return when {
this.unitDiscriminator == other.unitDiscriminator -> {
val result = this.value + other.value // never overflows long, but can overflow long63
when {
isInNanos() ->
durationOfNanosNormalized(result)
else ->
durationOfMillisNormalized(result)
}
}
} else {
val a: Long
val b: Long
if (this.isInMillis()) {
a = this.value
b = other.value
} else {
a = other.value
b = this.value
}
this.isInMillis() ->
addValuesMixedRanges(this.value, other.value)
else ->
addValuesMixedRanges(other.value, 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)))
}
private fun addValuesMixedRanges(thisMillis: Long, otherNanos: Long): Duration {
val otherMillis = nanosToMillis(otherNanos)
val resultMillis = thisMillis + otherMillis
return if (resultMillis in -MAX_NANOS_IN_MILLIS..MAX_NANOS_IN_MILLIS) {
val otherNanoRemainder = otherNanos - millisToNanos(otherMillis)
durationOfNanos(millisToNanos(resultMillis) + otherNanoRemainder)
} else {
durationOfMillis(resultMillis.coerceIn(-MAX_MILLIS, MAX_MILLIS))
}
}
@@ -254,7 +231,6 @@ public value class Duration internal constructor(internal val rawValue: Long) :
* @throws IllegalArgumentException if the operation results in an undefined value for the given arguments,
* e.g. when subtracting infinite durations of the same sign.
*/
// TODO: negation may overflow
public operator fun minus(other: Duration): Duration = this + (-other)
/**
@@ -266,37 +242,40 @@ public value class Duration internal constructor(internal val rawValue: Long) :
public operator fun times(scale: Int): Duration {
if (isInfinite()) {
return when {
scale == 0 -> throw IllegalArgumentException("Multiplying infinite duration by zero is undefined.")
scale == 0 -> throw IllegalArgumentException("Multiplying infinite duration by zero yields an undefined result.")
scale > 0 -> this
else -> -this
}
}
if (scale == 0) return ZERO
val value = value
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 if (isInNanos()) {
if (value in (MAX_NANOS / Int.MIN_VALUE)..(-MAX_NANOS / Int.MIN_VALUE)) {
// can't overflow nanos range for any scale
durationOfNanos(result)
} else {
if (result / scale == value) {
durationOfNanosNormalized(result)
} else {
val millis = nanosToMillis(value)
val remNanos = value - millisToNanos(millis)
val resultMillis = millis * scale
val totalMillis = resultMillis + nanosToMillis(remNanos * scale)
if (resultMillis / scale == millis && totalMillis xor resultMillis >= 0) {
durationOfMillis(totalMillis.coerceIn(-MAX_MILLIS..MAX_MILLIS))
} else {
if (value.sign * scale.sign > 0) INFINITE else NEG_INFINITE
}
}
}
}
return when {
result in MIN_LONG_63..MAX_LONG_63 -> {
Duration(storeAs(result, rawValue))
} else {
if (result / scale == value) {
durationOfMillis(result.coerceIn(-MAX_MILLIS..MAX_MILLIS))
} else {
if (value.sign * scale.sign > 0) INFINITE else NEG_INFINITE
}
isInNanos() -> {
Duration(storeAsMillis(nanosToMillis(result)))
}
result > 0 -> Duration.INFINITE
else -> -Duration.INFINITE
}
}
@@ -326,24 +305,24 @@ public value class Duration internal constructor(internal val rawValue: Long) :
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")
isPositive() -> INFINITE
isNegative() -> NEG_INFINITE
else -> throw IllegalArgumentException("Dividing zero duration by zero yields an undefined result.")
}
}
if (isInNanos()) {
return Duration(storeAsNanos(value / scale))
return durationOfNanos(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)))
if (result in -MAX_NANOS_IN_MILLIS..MAX_NANOS_IN_MILLIS) {
val rem = millisToNanos(value - (result * scale)) / scale
return durationOfNanos(millisToNanos(result) + rem)
}
return Duration(storeAsMillis(result))
return durationOfMillis(result)
}
}
@@ -354,12 +333,11 @@ public value class Duration internal constructor(internal val rawValue: Long) :
* e.g. when dividing an infinite duration by infinity or zero duration by zero.
*/
public operator fun div(scale: Double): Duration {
if (scale != 0.0) {
val intScale = scale.roundToInt()
if (intScale.toDouble() == scale) {
return div(intScale)
}
val intScale = scale.roundToInt()
if (intScale.toDouble() == scale && intScale != 0) {
return div(intScale)
}
val unit = storageUnit
val result = toDouble(unit) / scale
return result.toDuration(unit)
@@ -378,7 +356,7 @@ public value class Duration internal constructor(internal val rawValue: Long) :
public fun isPositive(): Boolean = rawValue > 0
/** Returns true, if the duration value is infinite. */
public fun isInfinite(): Boolean = isInMillis() && value.let { it == MAX_LONG_63 || it == MIN_LONG_63 }
public fun isInfinite(): Boolean = rawValue == INFINITE.rawValue || rawValue == NEG_INFINITE.rawValue
/** Returns true, if the duration value is finite. */
public fun isFinite(): Boolean = !isInfinite()
@@ -388,11 +366,11 @@ public value class Duration internal constructor(internal val rawValue: Long) :
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
if (compareBits < 0 || compareBits.toInt() and 1 == 0) // different signs or same sign/same range
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
// same sign/different ranges
val r = this.unitDiscriminator - other.unitDiscriminator // compare ranges
return if (isNegative()) -r else r
}
@@ -474,11 +452,13 @@ public value class Duration internal constructor(internal val rawValue: Long) :
/** Returns the value of this duration expressed as a [Double] number of the specified [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)
return when (rawValue) {
INFINITE.rawValue -> Double.POSITIVE_INFINITY
NEG_INFINITE.rawValue -> Double.NEGATIVE_INFINITY
else -> {
// TODO: whether it's ok to convert to Double before scaling
convertDurationUnit(value.toDouble(), storageUnit, unit)
}
}
}
@@ -488,10 +468,10 @@ public value class Duration internal constructor(internal val rawValue: Long) :
* 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 {
return if (isInfinite()) {
if (isNegative()) Long.MIN_VALUE else Long.MAX_VALUE
} else {
convertDurationUnit(value, storageUnit, unit)
return when (rawValue) {
INFINITE.rawValue -> Long.MAX_VALUE
NEG_INFINITE.rawValue -> Long.MIN_VALUE
else -> convertDurationUnit(value, storageUnit, unit)
}
}
@@ -555,7 +535,7 @@ public value class Duration internal constructor(internal val rawValue: Long) :
*/
// TODO: Deprecate in favor inWholeMilliseconds
public fun toLongMilliseconds(): Long {
return if (isInMillis() && isFinite()) value else toLong(DurationUnit.MILLISECONDS)
return if (isInMillis() && isFinite()) rawValue else toLong(DurationUnit.MILLISECONDS)
}
/**
@@ -571,9 +551,10 @@ public value class Duration internal constructor(internal val rawValue: Long) :
*
* @sample samples.time.Durations.toStringDefault
*/
override fun toString(): String = when {
isInfinite() -> if (isNegative()) "-Infinity" else "Infinity"
rawValue == 0L -> "0s"
override fun toString(): String = when (rawValue) {
0L -> "0s"
INFINITE.rawValue -> "Infinity"
NEG_INFINITE.rawValue -> "-Infinity"
else -> {
val absNs = absoluteValue.inNanoseconds
var scientific = false
@@ -684,7 +665,7 @@ public value class Duration internal constructor(internal val rawValue: Long) :
@ExperimentalTime
public fun Int.toDuration(unit: DurationUnit): Duration {
return if (unit <= DurationUnit.SECONDS) {
Duration(storeAsNanos(convertDurationUnitOverflow(this.toLong(), unit, DurationUnit.NANOSECONDS)))
durationOfNanos(convertDurationUnitOverflow(this.toLong(), unit, DurationUnit.NANOSECONDS))
} else
toLong().toDuration(unit)
}
@@ -693,15 +674,12 @@ public fun Int.toDuration(unit: DurationUnit): Duration {
@SinceKotlin("1.3")
@ExperimentalTime
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)))
val maxNsInUnit = convertDurationUnitOverflow(MAX_NANOS, DurationUnit.NANOSECONDS, unit)
if (this in -maxNsInUnit..maxNsInUnit) {
return durationOfNanos(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
val millis = convertDurationUnit(this, unit, DurationUnit.MILLISECONDS)
return durationOfMillis(millis.coerceIn(-MAX_MILLIS, MAX_MILLIS))
}
}
@@ -715,10 +693,12 @@ public fun Long.toDuration(unit: DurationUnit): Duration {
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)))
val nanos = valueInNs.toLong()
return if (nanos in -MAX_NANOS..MAX_NANOS) {
durationOfNanos(nanos)
} else {
Duration(storeAsNanos(valueInNs.toLong()))
val millis = convertDurationUnit(this, unit, DurationUnit.MILLISECONDS).toLong()
durationOfMillisNormalized(millis)
}
}
@@ -896,6 +876,40 @@ public inline operator fun Int.times(duration: Duration): Duration = duration *
@kotlin.internal.InlineOnly
public inline operator fun Double.times(duration: Duration): Duration = duration * this
// The ranges are chosen so that they are:
// - symmetric relative to zero: this greatly simplifies operations with sign, e.g. unaryMinus and minus.
// - non-overlapping, but adjacent: the first value that doesn't fit in nanos range, can be exactly represented in millis.
internal const val NANOS_IN_MILLIS = 1_000_000
// maximum number duration can store in nanosecond range
internal const val MAX_NANOS = Long.MAX_VALUE / 2 / NANOS_IN_MILLIS * NANOS_IN_MILLIS - 1 // ends in ..._999_999
// maximum number duration can store in millisecond range, also encodes an infinite value
internal const val MAX_MILLIS = Long.MAX_VALUE / 2
// MAX_NANOS expressed in milliseconds
private const val MAX_NANOS_IN_MILLIS = MAX_NANOS / NANOS_IN_MILLIS
private fun nanosToMillis(nanos: Long): Long = nanos / NANOS_IN_MILLIS
private fun millisToNanos(millis: Long): Long = millis * NANOS_IN_MILLIS
@ExperimentalTime private fun durationOfNanos(normalNanos: Long) = Duration(normalNanos shl 1)
@ExperimentalTime private fun durationOfMillis(normalMillis: Long) = Duration((normalMillis shl 1) + 1)
@ExperimentalTime private fun durationOf(normalValue: Long, unitDiscriminator: Int) = Duration((normalValue shl 1) + unitDiscriminator)
@ExperimentalTime private fun durationOfNanosNormalized(nanos: Long) =
if (nanos in -MAX_NANOS..MAX_NANOS) {
durationOfNanos(nanos)
} else {
durationOfMillis(nanosToMillis(nanos))
}
@ExperimentalTime private fun durationOfMillisNormalized(millis: Long) =
if (millis in -MAX_NANOS_IN_MILLIS..MAX_NANOS_IN_MILLIS) {
durationOfNanos(millisToNanos(millis))
} else {
durationOfMillis(millis.coerceIn(-MAX_MILLIS, MAX_MILLIS))
}
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