Make Duration storage ranges symmetric and non-overlapping
Increase accuracy of multiplication and division.
This commit is contained in:
@@ -11,18 +11,6 @@ import kotlin.math.abs
|
|||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlin.math.sign
|
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.
|
* 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")
|
@SinceKotlin("1.3")
|
||||||
@ExperimentalTime
|
@ExperimentalTime
|
||||||
@JvmInline
|
@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 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
|
private val storageUnit get() = if (isInNanos()) DurationUnit.NANOSECONDS else DurationUnit.MILLISECONDS
|
||||||
|
|
||||||
init {
|
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 {
|
companion object {
|
||||||
@@ -59,7 +54,8 @@ public value class Duration internal constructor(internal val rawValue: Long) :
|
|||||||
public val ZERO: Duration = Duration(0L)
|
public val ZERO: Duration = Duration(0L)
|
||||||
|
|
||||||
/** The duration whose value is positive infinity. It is useful for representing timeouts that should never expire. */
|
/** 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]. */
|
/** 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 =
|
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
|
// arithmetic operators
|
||||||
|
|
||||||
/** Returns the negative of this value. */
|
/** Returns the negative of this value. */
|
||||||
public operator fun unaryMinus(): Duration {
|
public operator fun unaryMinus(): Duration = durationOf(-value, unitDiscriminator)
|
||||||
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.
|
* 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))
|
if (other.isFinite() || (this.rawValue xor other.rawValue >= 0))
|
||||||
return this
|
return this
|
||||||
else
|
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
|
other.isInfinite() -> return other
|
||||||
}
|
}
|
||||||
|
|
||||||
return if (this.isInNanos() == other.isInNanos()) {
|
return when {
|
||||||
when (val newValue = this.value + other.value) { // never overflows long, but can overflow long63
|
this.unitDiscriminator == other.unitDiscriminator -> {
|
||||||
in MIN_LONG_63..MAX_LONG_63 -> {
|
val result = this.value + other.value // never overflows long, but can overflow long63
|
||||||
if (isInMillis() && newValue in MIN_MILLIS_IN_NANOS..MAX_MILLIS_IN_NANOS)
|
when {
|
||||||
Duration(storeAsNanos(millisToNanos(newValue)))
|
isInNanos() ->
|
||||||
else
|
durationOfNanosNormalized(result)
|
||||||
Duration(storeAs(newValue, this.rawValue))
|
else ->
|
||||||
}
|
durationOfMillisNormalized(result)
|
||||||
else -> {
|
|
||||||
val millisValue = if (isInNanos()) nanosToMillis(newValue) else newValue.coerceIn(MIN_LONG_63, MAX_LONG_63)
|
|
||||||
Duration(storeAsMillis(millisValue))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
this.isInMillis() ->
|
||||||
val a: Long
|
addValuesMixedRanges(this.value, other.value)
|
||||||
val b: Long
|
else ->
|
||||||
if (this.isInMillis()) {
|
addValuesMixedRanges(other.value, this.value)
|
||||||
a = this.value
|
}
|
||||||
b = other.value
|
}
|
||||||
} else {
|
|
||||||
a = other.value
|
|
||||||
b = this.value
|
|
||||||
}
|
|
||||||
|
|
||||||
val bMillis = nanosToMillis(b)
|
private fun addValuesMixedRanges(thisMillis: Long, otherNanos: Long): Duration {
|
||||||
val resultMillis = a + bMillis
|
val otherMillis = nanosToMillis(otherNanos)
|
||||||
return if (resultMillis in MIN_MILLIS_IN_NANOS..MAX_MILLIS_IN_NANOS) {
|
val resultMillis = thisMillis + otherMillis
|
||||||
val bRemainder = b - millisToNanos(bMillis)
|
return if (resultMillis in -MAX_NANOS_IN_MILLIS..MAX_NANOS_IN_MILLIS) {
|
||||||
Duration(storeAsNanos(millisToNanos(resultMillis) + bRemainder)) // can it overflow nanos?
|
val otherNanoRemainder = otherNanos - millisToNanos(otherMillis)
|
||||||
} else {
|
durationOfNanos(millisToNanos(resultMillis) + otherNanoRemainder)
|
||||||
Duration(storeAsMillis(resultMillis.coerceIn(MIN_LONG_63, MAX_LONG_63)))
|
} 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,
|
* @throws IllegalArgumentException if the operation results in an undefined value for the given arguments,
|
||||||
* e.g. when subtracting infinite durations of the same sign.
|
* e.g. when subtracting infinite durations of the same sign.
|
||||||
*/
|
*/
|
||||||
// TODO: negation may overflow
|
|
||||||
public operator fun minus(other: Duration): Duration = this + (-other)
|
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 {
|
public operator fun times(scale: Int): Duration {
|
||||||
if (isInfinite()) {
|
if (isInfinite()) {
|
||||||
return when {
|
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
|
scale > 0 -> this
|
||||||
else -> -this
|
else -> -this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (scale == 0) return ZERO
|
if (scale == 0) return ZERO
|
||||||
|
|
||||||
|
val value = value
|
||||||
val result = value * scale
|
val result = value * scale
|
||||||
if (value >= Int.MIN_VALUE && value <= Int.MAX_VALUE) {
|
return if (isInNanos()) {
|
||||||
return Duration(storeAs(result, rawValue))
|
if (value in (MAX_NANOS / Int.MIN_VALUE)..(-MAX_NANOS / Int.MIN_VALUE)) {
|
||||||
}
|
// can't overflow nanos range for any scale
|
||||||
if (result / scale != value && isInNanos()) { // Long overflow
|
durationOfNanos(result)
|
||||||
val rawMillis = nanosToMillis(value)
|
} else {
|
||||||
val resultMillis = rawMillis * scale
|
if (result / scale == value) {
|
||||||
if (resultMillis / scale != rawMillis) return Duration.INFINITE
|
durationOfNanosNormalized(result)
|
||||||
return when {
|
} else {
|
||||||
resultMillis in MIN_LONG_63..MAX_LONG_63 -> Duration(storeAsMillis(resultMillis))
|
val millis = nanosToMillis(value)
|
||||||
resultMillis > 0 -> Duration.INFINITE
|
val remNanos = value - millisToNanos(millis)
|
||||||
else -> -Duration.INFINITE
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
if (result / scale == value) {
|
||||||
return when {
|
durationOfMillis(result.coerceIn(-MAX_MILLIS..MAX_MILLIS))
|
||||||
result in MIN_LONG_63..MAX_LONG_63 -> {
|
} else {
|
||||||
Duration(storeAs(result, rawValue))
|
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 {
|
public operator fun div(scale: Int): Duration {
|
||||||
if (scale == 0) {
|
if (scale == 0) {
|
||||||
return when {
|
return when {
|
||||||
isPositive() -> Duration.INFINITE
|
isPositive() -> INFINITE
|
||||||
isNegative() -> -Duration.INFINITE
|
isNegative() -> NEG_INFINITE
|
||||||
else -> throw IllegalArgumentException("Dividing zero duration by zero is undefined")
|
else -> throw IllegalArgumentException("Dividing zero duration by zero yields an undefined result.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isInNanos()) {
|
if (isInNanos()) {
|
||||||
return Duration(storeAsNanos(value / scale))
|
return durationOfNanos(value / scale)
|
||||||
} else {
|
} else {
|
||||||
if (isInfinite())
|
if (isInfinite())
|
||||||
return this * scale.sign
|
return this * scale.sign
|
||||||
|
|
||||||
val result = value / scale
|
val result = value / scale
|
||||||
|
|
||||||
if (result in MIN_MILLIS_IN_NANOS..MAX_MILLIS_IN_NANOS) {
|
if (result in -MAX_NANOS_IN_MILLIS..MAX_NANOS_IN_MILLIS) {
|
||||||
// TODO: more precise
|
val rem = millisToNanos(value - (result * scale)) / scale
|
||||||
return Duration(storeAsNanos(millisToNanos(result)))
|
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.
|
* e.g. when dividing an infinite duration by infinity or zero duration by zero.
|
||||||
*/
|
*/
|
||||||
public operator fun div(scale: Double): Duration {
|
public operator fun div(scale: Double): Duration {
|
||||||
if (scale != 0.0) {
|
val intScale = scale.roundToInt()
|
||||||
val intScale = scale.roundToInt()
|
if (intScale.toDouble() == scale && intScale != 0) {
|
||||||
if (intScale.toDouble() == scale) {
|
return div(intScale)
|
||||||
return div(intScale)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val unit = storageUnit
|
val unit = storageUnit
|
||||||
val result = toDouble(unit) / scale
|
val result = toDouble(unit) / scale
|
||||||
return result.toDuration(unit)
|
return result.toDuration(unit)
|
||||||
@@ -378,7 +356,7 @@ public value class Duration internal constructor(internal val rawValue: Long) :
|
|||||||
public fun isPositive(): Boolean = rawValue > 0
|
public fun isPositive(): Boolean = rawValue > 0
|
||||||
|
|
||||||
/** Returns true, if the duration value is infinite. */
|
/** 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. */
|
/** Returns true, if the duration value is finite. */
|
||||||
public fun isFinite(): Boolean = !isInfinite()
|
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 {
|
override fun compareTo(other: Duration): Int {
|
||||||
val compareBits = this.rawValue xor other.rawValue
|
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)
|
return this.rawValue.compareTo(other.rawValue)
|
||||||
// same sign, different scales
|
// same sign/different ranges
|
||||||
val r = (this.rawValue and 1L).toInt() - (other.rawValue and 1L).toInt()
|
val r = this.unitDiscriminator - other.unitDiscriminator // compare ranges
|
||||||
return if (this.rawValue < 0) -r else r
|
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]. */
|
/** Returns the value of this duration expressed as a [Double] number of the specified [unit]. */
|
||||||
public fun toDouble(unit: DurationUnit): Double {
|
public fun toDouble(unit: DurationUnit): Double {
|
||||||
return if (isInfinite()) {
|
return when (rawValue) {
|
||||||
if (isNegative()) Double.NEGATIVE_INFINITY else Double.POSITIVE_INFINITY
|
INFINITE.rawValue -> Double.POSITIVE_INFINITY
|
||||||
} else {
|
NEG_INFINITE.rawValue -> Double.NEGATIVE_INFINITY
|
||||||
// TODO: whether it's ok to convert to Double before scaling
|
else -> {
|
||||||
convertDurationUnit(value.toDouble(), storageUnit, unit)
|
// 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.
|
* 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 {
|
public fun toLong(unit: DurationUnit): Long {
|
||||||
return if (isInfinite()) {
|
return when (rawValue) {
|
||||||
if (isNegative()) Long.MIN_VALUE else Long.MAX_VALUE
|
INFINITE.rawValue -> Long.MAX_VALUE
|
||||||
} else {
|
NEG_INFINITE.rawValue -> Long.MIN_VALUE
|
||||||
convertDurationUnit(value, storageUnit, unit)
|
else -> convertDurationUnit(value, storageUnit, unit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -555,7 +535,7 @@ public value class Duration internal constructor(internal val rawValue: Long) :
|
|||||||
*/
|
*/
|
||||||
// TODO: Deprecate in favor inWholeMilliseconds
|
// TODO: Deprecate in favor inWholeMilliseconds
|
||||||
public fun toLongMilliseconds(): Long {
|
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
|
* @sample samples.time.Durations.toStringDefault
|
||||||
*/
|
*/
|
||||||
override fun toString(): String = when {
|
override fun toString(): String = when (rawValue) {
|
||||||
isInfinite() -> if (isNegative()) "-Infinity" else "Infinity"
|
0L -> "0s"
|
||||||
rawValue == 0L -> "0s"
|
INFINITE.rawValue -> "Infinity"
|
||||||
|
NEG_INFINITE.rawValue -> "-Infinity"
|
||||||
else -> {
|
else -> {
|
||||||
val absNs = absoluteValue.inNanoseconds
|
val absNs = absoluteValue.inNanoseconds
|
||||||
var scientific = false
|
var scientific = false
|
||||||
@@ -684,7 +665,7 @@ public value class Duration internal constructor(internal val rawValue: Long) :
|
|||||||
@ExperimentalTime
|
@ExperimentalTime
|
||||||
public fun Int.toDuration(unit: DurationUnit): Duration {
|
public fun Int.toDuration(unit: DurationUnit): Duration {
|
||||||
return if (unit <= DurationUnit.SECONDS) {
|
return if (unit <= DurationUnit.SECONDS) {
|
||||||
Duration(storeAsNanos(convertDurationUnitOverflow(this.toLong(), unit, DurationUnit.NANOSECONDS)))
|
durationOfNanos(convertDurationUnitOverflow(this.toLong(), unit, DurationUnit.NANOSECONDS))
|
||||||
} else
|
} else
|
||||||
toLong().toDuration(unit)
|
toLong().toDuration(unit)
|
||||||
}
|
}
|
||||||
@@ -693,15 +674,12 @@ public fun Int.toDuration(unit: DurationUnit): Duration {
|
|||||||
@SinceKotlin("1.3")
|
@SinceKotlin("1.3")
|
||||||
@ExperimentalTime
|
@ExperimentalTime
|
||||||
public fun Long.toDuration(unit: DurationUnit): Duration {
|
public fun Long.toDuration(unit: DurationUnit): Duration {
|
||||||
val maxInUnit = convertDurationUnitOverflow(MAX_LONG_63, DurationUnit.NANOSECONDS, unit)
|
val maxNsInUnit = convertDurationUnitOverflow(MAX_NANOS, DurationUnit.NANOSECONDS, unit)
|
||||||
if (this in -maxInUnit..maxInUnit) {
|
if (this in -maxNsInUnit..maxNsInUnit) {
|
||||||
return Duration(storeAsNanos(convertDurationUnitOverflow(this, unit, DurationUnit.NANOSECONDS)))
|
return durationOfNanos(convertDurationUnitOverflow(this, unit, DurationUnit.NANOSECONDS))
|
||||||
} else {
|
} else {
|
||||||
val max2InUnit = convertDurationUnitOverflow(MAX_LONG_63, DurationUnit.MILLISECONDS, unit)
|
val millis = convertDurationUnit(this, unit, DurationUnit.MILLISECONDS)
|
||||||
if (unit < DurationUnit.MILLISECONDS || this in -max2InUnit..max2InUnit) {
|
return durationOfMillis(millis.coerceIn(-MAX_MILLIS, MAX_MILLIS))
|
||||||
return Duration(storeAsMillis(convertDurationUnitOverflow(this, unit, DurationUnit.MILLISECONDS)))
|
|
||||||
}
|
|
||||||
return if (this < 0) -Duration.INFINITE else Duration.INFINITE
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -715,10 +693,12 @@ public fun Long.toDuration(unit: DurationUnit): Duration {
|
|||||||
public fun Double.toDuration(unit: DurationUnit): Duration {
|
public fun Double.toDuration(unit: DurationUnit): Duration {
|
||||||
val valueInNs = convertDurationUnit(this, unit, DurationUnit.NANOSECONDS)
|
val valueInNs = convertDurationUnit(this, unit, DurationUnit.NANOSECONDS)
|
||||||
require(!valueInNs.isNaN()) { "Duration value cannot be NaN." }
|
require(!valueInNs.isNaN()) { "Duration value cannot be NaN." }
|
||||||
return if (valueInNs > MAX_LONG_63 || valueInNs < MIN_LONG_63) {
|
val nanos = valueInNs.toLong()
|
||||||
Duration(storeAsMillis((valueInNs / NANOS_IN_MILLIS).toLong().coerceIn(MIN_LONG_63, MAX_LONG_63)))
|
return if (nanos in -MAX_NANOS..MAX_NANOS) {
|
||||||
|
durationOfNanos(nanos)
|
||||||
} else {
|
} 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
|
@kotlin.internal.InlineOnly
|
||||||
public inline operator fun Double.times(duration: Duration): Duration = duration * this
|
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 formatToExactDecimals(value: Double, decimals: Int): String
|
||||||
internal expect fun formatUpToDecimals(value: Double, decimals: Int): String
|
internal expect fun formatUpToDecimals(value: Double, decimals: Int): String
|
||||||
internal expect fun formatScientific(value: Double): String
|
internal expect fun formatScientific(value: Double): String
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
package test.time
|
package test.time
|
||||||
|
|
||||||
import test.numbers.assertAlmostEquals
|
import test.numbers.assertAlmostEquals
|
||||||
|
import kotlin.math.nextDown
|
||||||
import kotlin.native.concurrent.SharedImmutable
|
import kotlin.native.concurrent.SharedImmutable
|
||||||
import kotlin.test.*
|
import kotlin.test.*
|
||||||
import kotlin.time.*
|
import kotlin.time.*
|
||||||
@@ -19,17 +20,17 @@ private val units = DurationUnit.values()
|
|||||||
class DurationTest {
|
class DurationTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun construction() {
|
fun constructionFromNumber() {
|
||||||
// nanosecond precision
|
// nanosecond precision
|
||||||
val testValues = listOf(0L, 1L, Long.MAX_VALUE / 2) + List(100) { Random.nextLong(0, Long.MAX_VALUE / 2) }
|
val testValues = listOf(0L, 1L, MAX_NANOS) + List(100) { Random.nextLong(0, MAX_NANOS) }
|
||||||
for (value in testValues) {
|
for (value in testValues) {
|
||||||
assertEquals(value, value.toDuration(DurationUnit.NANOSECONDS).toLongNanoseconds())
|
assertEquals(value, value.toDuration(DurationUnit.NANOSECONDS).toLongNanoseconds())
|
||||||
assertEquals(-value, -value.toDuration(DurationUnit.NANOSECONDS).toLongNanoseconds())
|
assertEquals(-value, -value.toDuration(DurationUnit.NANOSECONDS).toLongNanoseconds())
|
||||||
}
|
}
|
||||||
// expressible as long nanoseconds but stored as milliseconds
|
// expressible as long nanoseconds but stored as milliseconds
|
||||||
for (delta in testValues) {
|
for (delta in testValues) {
|
||||||
val value = 1L.shl(62) + delta
|
val value = (MAX_NANOS + 1) + delta
|
||||||
val expected = value - (value % 1_000_000)
|
val expected = value - (value % NANOS_IN_MILLIS)
|
||||||
assertEquals(expected, value.toDuration(DurationUnit.NANOSECONDS).toLongNanoseconds())
|
assertEquals(expected, value.toDuration(DurationUnit.NANOSECONDS).toLongNanoseconds())
|
||||||
assertEquals(-expected, -value.toDuration(DurationUnit.NANOSECONDS).toLongNanoseconds())
|
assertEquals(-expected, -value.toDuration(DurationUnit.NANOSECONDS).toLongNanoseconds())
|
||||||
}
|
}
|
||||||
@@ -42,11 +43,27 @@ class DurationTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (unit in units) {
|
||||||
|
val borderValue = convertDurationUnit(MAX_NANOS, DurationUnit.NANOSECONDS, unit)
|
||||||
|
val d1 = borderValue.toDuration(unit)
|
||||||
|
val d2 = (borderValue + 1).toDuration(unit)
|
||||||
|
assertNotEquals(d1, d1 + Duration.nanoseconds(1))
|
||||||
|
assertEquals(d2, d2 + Duration.nanoseconds(1))
|
||||||
|
}
|
||||||
|
|
||||||
assertEquals(Long.MAX_VALUE / 1000, Long.MAX_VALUE.toDuration(DurationUnit.MICROSECONDS).toLongMilliseconds())
|
assertEquals(Long.MAX_VALUE / 1000, Long.MAX_VALUE.toDuration(DurationUnit.MICROSECONDS).toLongMilliseconds())
|
||||||
assertEquals(Long.MAX_VALUE / 1000 * 1000, Long.MAX_VALUE.toDuration(DurationUnit.MICROSECONDS).toLong(DurationUnit.MICROSECONDS))
|
assertEquals(Long.MAX_VALUE / 1000 * 1000, Long.MAX_VALUE.toDuration(DurationUnit.MICROSECONDS).toLong(DurationUnit.MICROSECONDS))
|
||||||
|
|
||||||
assertEquals(Duration.INFINITE, (Long.MAX_VALUE / 2).toDuration(DurationUnit.MILLISECONDS))
|
assertEquals(Duration.INFINITE, (MAX_MILLIS).toDuration(DurationUnit.MILLISECONDS))
|
||||||
assertEquals(-Duration.INFINITE, (Long.MIN_VALUE / 2).toDuration(DurationUnit.MILLISECONDS))
|
assertEquals(-Duration.INFINITE, (-MAX_MILLIS).toDuration(DurationUnit.MILLISECONDS))
|
||||||
|
|
||||||
|
run {
|
||||||
|
val maxNsDouble = MAX_NANOS.toDouble()
|
||||||
|
val lessThanMaxDouble = maxNsDouble.nextDown()
|
||||||
|
val maxNs = maxNsDouble.toDuration(DurationUnit.NANOSECONDS).toLongNanoseconds()
|
||||||
|
val lessThanMaxNs = lessThanMaxDouble.toDuration(DurationUnit.NANOSECONDS).toLongNanoseconds()
|
||||||
|
assertTrue(maxNs > lessThanMaxNs, "$maxNs should be > $lessThanMaxNs")
|
||||||
|
}
|
||||||
|
|
||||||
assertFailsWith<IllegalArgumentException> { Double.NaN.toDuration(DurationUnit.SECONDS) }
|
assertFailsWith<IllegalArgumentException> { Double.NaN.toDuration(DurationUnit.SECONDS) }
|
||||||
}
|
}
|
||||||
@@ -77,31 +94,44 @@ class DurationTest {
|
|||||||
assertNotEquals(d1, d3, "$value $unit in $unit2")
|
assertNotEquals(d1, d3, "$value $unit in $unit2")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
run { // invariant Duration.nanoseconds(d.toLongNanoseconds()) == d when duration does not overflow nanoseconds
|
||||||
|
val d1 = Duration.nanoseconds(MAX_NANOS + 1)
|
||||||
|
val d2 = Duration.nanoseconds(d1.toLongNanoseconds())
|
||||||
|
assertEquals(d1.toLongNanoseconds(), d2.toLongNanoseconds())
|
||||||
|
assertEquals(d1, d2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun comparison() {
|
fun comparison() {
|
||||||
run {
|
fun assertGreater(d1: Duration, d2: Duration, message: String) {
|
||||||
val d1 = Duration.nanoseconds(Long.MAX_VALUE / 2 + 1)
|
assertTrue(d1 > d2, message)
|
||||||
val d2 = Duration.nanoseconds(Long.MAX_VALUE / 2)
|
assertFalse(d1 <= d2, message)
|
||||||
assertTrue(d1 >= d2, "same sign, different ranges")
|
assertTrue(
|
||||||
// TODO: Address this contradiction
|
d1.toLongNanoseconds() > d2.toLongNanoseconds() ||
|
||||||
// assertTrue(d1.toLongNanoseconds() >= d2.toLongNanoseconds())
|
d1.toLongNanoseconds() == d2.toLongNanoseconds() && d1.toLongMilliseconds() > d2.toLongMilliseconds(),
|
||||||
|
message
|
||||||
val d3 = Duration.nanoseconds(Long.MAX_VALUE / 2 - 1)
|
)
|
||||||
assertTrue(d2 > d3, "same sign, same range nanos")
|
|
||||||
|
|
||||||
val d4 = Duration.nanoseconds(Long.MAX_VALUE)
|
|
||||||
assertTrue(d4 > d1, "same sign, same range millis")
|
|
||||||
|
|
||||||
assertTrue(d2 > -d1, "different signs, different ranges")
|
|
||||||
assertTrue(d1 > -d4, "different signs, same ranges")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val d4 = Duration.nanoseconds(Long.MAX_VALUE)
|
||||||
|
val d3 = Duration.nanoseconds(MAX_NANOS + 1)
|
||||||
|
val d2 = Duration.nanoseconds(MAX_NANOS)
|
||||||
|
val d1 = Duration.nanoseconds(MAX_NANOS - 1)
|
||||||
|
|
||||||
|
assertGreater(d4, d2, "same sign, different ranges")
|
||||||
|
assertGreater(d3, d2, "same sign, different ranges 2")
|
||||||
|
assertGreater(d2, d1, "same sign, same range nanos")
|
||||||
|
assertGreater(d4, d3, "same sign, same range millis")
|
||||||
|
assertGreater(d2, -d3, "different signs, different ranges")
|
||||||
|
assertGreater(d3, -d4, "different signs, same ranges")
|
||||||
|
assertGreater(d1, -d2, "different signs, same ranges 2")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun conversionFromNumber() {
|
fun constructionFactoryFunctions() {
|
||||||
val n1 = Random.nextInt(Int.MAX_VALUE)
|
val n1 = Random.nextInt(Int.MAX_VALUE)
|
||||||
val n2 = Random.nextLong(Long.MAX_VALUE)
|
val n2 = Random.nextLong(Long.MAX_VALUE)
|
||||||
val n3 = Random.nextDouble()
|
val n3 = Random.nextDouble()
|
||||||
@@ -309,22 +339,23 @@ class DurationTest {
|
|||||||
assertEquals(Duration.milliseconds(850), Duration.seconds(1) - Duration.milliseconds(150))
|
assertEquals(Duration.milliseconds(850), Duration.seconds(1) - Duration.milliseconds(150))
|
||||||
assertEquals(Duration.milliseconds(1150), Duration.seconds(1) - Duration.milliseconds(-150))
|
assertEquals(Duration.milliseconds(1150), Duration.seconds(1) - Duration.milliseconds(-150))
|
||||||
assertEquals(Duration.milliseconds(1), Duration.microseconds(Long.MAX_VALUE) - Duration.microseconds(Long.MAX_VALUE - 1_000))
|
assertEquals(Duration.milliseconds(1), Duration.microseconds(Long.MAX_VALUE) - Duration.microseconds(Long.MAX_VALUE - 1_000))
|
||||||
|
assertEquals(Duration.milliseconds(-1), Duration.microseconds(Long.MAX_VALUE - 1_000) - Duration.microseconds(Long.MAX_VALUE))
|
||||||
|
|
||||||
run {
|
run {
|
||||||
val offset = 2_000_000L
|
val offset = 2L * NANOS_IN_MILLIS
|
||||||
val value = Long.MAX_VALUE / 2 + offset
|
val value = MAX_NANOS + offset
|
||||||
val base = Duration.nanoseconds(value)
|
val base = Duration.nanoseconds(value)
|
||||||
val baseNs = base.toLongMilliseconds() * 1_000_000
|
val baseNs = base.toLongMilliseconds() * NANOS_IN_MILLIS
|
||||||
assertEquals(baseNs, base.toLongNanoseconds()) // base stored as millis
|
assertEquals(baseNs, base.toLongNanoseconds()) // base stored as millis
|
||||||
|
|
||||||
val smallDeltas = listOf(1L, 2L, 1000L, offset / 2 - 1) + List(10) { Random.nextLong(offset / 2) }
|
val smallDeltas = listOf(1L, 2L, 1000L, NANOS_IN_MILLIS - 1L) + List(10) { Random.nextLong(NANOS_IN_MILLIS.toLong()) }
|
||||||
for (smallDeltaNs in smallDeltas) {
|
for (smallDeltaNs in smallDeltas) {
|
||||||
assertEquals(base, base - Duration.nanoseconds(smallDeltaNs), "delta: $smallDeltaNs")
|
assertEquals(base, base - Duration.nanoseconds(smallDeltaNs), "delta: $smallDeltaNs")
|
||||||
}
|
}
|
||||||
|
|
||||||
val deltas = listOf(offset + 1L, offset + 1500L) +
|
val deltas = listOf(offset + 1L, offset + 1500L) +
|
||||||
List(10) { Random.nextLong(offset + 1500, offset + 10000) } +
|
List(10) { Random.nextLong(offset + 1500, offset + 10000) } +
|
||||||
List(100) { Random.nextLong(offset + 1500, Long.MAX_VALUE / 2) }
|
List(100) { Random.nextLong(offset + 1500, MAX_NANOS) }
|
||||||
for (deltaNs in deltas) {
|
for (deltaNs in deltas) {
|
||||||
val delta = Duration.nanoseconds(deltaNs)
|
val delta = Duration.nanoseconds(deltaNs)
|
||||||
assertEquals(deltaNs, delta.toLongNanoseconds())
|
assertEquals(deltaNs, delta.toLongNanoseconds())
|
||||||
@@ -356,6 +387,21 @@ class DurationTest {
|
|||||||
assertEquals(Duration.ZERO, 0 * Duration.hours(1))
|
assertEquals(Duration.ZERO, 0 * Duration.hours(1))
|
||||||
assertEquals(Duration.ZERO, Duration.seconds(1) * 0.0)
|
assertEquals(Duration.ZERO, Duration.seconds(1) * 0.0)
|
||||||
|
|
||||||
|
run { // promoting nanos range to millis range after multiplication
|
||||||
|
val value = MAX_NANOS
|
||||||
|
assertEquals(value, (Duration.nanoseconds(value) * 1_000_000).toLongMilliseconds())
|
||||||
|
assertEquals(value / 1000, (Duration.nanoseconds(value) * 1_000).toLongMilliseconds())
|
||||||
|
assertEquals(Duration.INFINITE, Duration.nanoseconds(Long.MAX_VALUE / 1000 + 1) * 1_000_000_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
run {
|
||||||
|
val value = MAX_NANOS / Int.MAX_VALUE
|
||||||
|
assertTrue((Duration.nanoseconds(value) * Int.MIN_VALUE).inWholeNanoseconds < -MAX_NANOS)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(Duration.INFINITE, Duration.days(Int.MAX_VALUE) * Int.MAX_VALUE)
|
||||||
|
assertEquals(-Duration.INFINITE, Duration.days(Int.MAX_VALUE) * Int.MIN_VALUE)
|
||||||
|
|
||||||
assertEquals(Duration.INFINITE, Duration.INFINITE * Double.POSITIVE_INFINITY)
|
assertEquals(Duration.INFINITE, Duration.INFINITE * Double.POSITIVE_INFINITY)
|
||||||
assertEquals(Duration.INFINITE, Duration.INFINITE * Double.MIN_VALUE)
|
assertEquals(Duration.INFINITE, Duration.INFINITE * Double.MIN_VALUE)
|
||||||
assertEquals(-Duration.INFINITE, Duration.INFINITE * Double.NEGATIVE_INFINITY)
|
assertEquals(-Duration.INFINITE, Duration.INFINITE * Double.NEGATIVE_INFINITY)
|
||||||
@@ -370,6 +416,13 @@ class DurationTest {
|
|||||||
assertEquals(Duration.hours(12), Duration.days(1) / 2)
|
assertEquals(Duration.hours(12), Duration.days(1) / 2)
|
||||||
assertEquals(Duration.minutes(60), Duration.days(1) / 24.0)
|
assertEquals(Duration.minutes(60), Duration.days(1) / 24.0)
|
||||||
assertEquals(Duration.seconds(20), Duration.minutes(2) / 6)
|
assertEquals(Duration.seconds(20), Duration.minutes(2) / 6)
|
||||||
|
assertEquals(Duration.days(365), Duration.days(365 * 299) / 299)
|
||||||
|
assertEquals(Duration.days(365), Duration.days(365 * 299.5) / 299.5)
|
||||||
|
|
||||||
|
run {
|
||||||
|
val value = MAX_NANOS
|
||||||
|
assertEquals(value, (Duration.milliseconds(value) / 1_000_000).toLongNanoseconds())
|
||||||
|
}
|
||||||
|
|
||||||
assertEquals(Duration.INFINITE, Duration.seconds(1) / 0)
|
assertEquals(Duration.INFINITE, Duration.seconds(1) / 0)
|
||||||
assertEquals(-Duration.INFINITE, -Duration.seconds(1) / 0.0)
|
assertEquals(-Duration.INFINITE, -Duration.seconds(1) / 0.0)
|
||||||
@@ -393,6 +446,7 @@ class DurationTest {
|
|||||||
assertEquals(299.0, Duration.days(365 * 299) / Duration.days(365))
|
assertEquals(299.0, Duration.days(365 * 299) / Duration.days(365))
|
||||||
|
|
||||||
assertTrue((Duration.INFINITE / Duration.INFINITE).isNaN())
|
assertTrue((Duration.INFINITE / Duration.INFINITE).isNaN())
|
||||||
|
assertTrue((Duration.ZERO / Duration.ZERO).isNaN())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user