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
@@ -44,9 +44,14 @@ class DurationConversionTest {
@Test
fun javaToKotlinRounding() {
val jtDuration = JTDuration.ofDays(105).plusNanos(1)
val duration = jtDuration.toKotlinDuration()
assertEquals(Duration.days(105), duration)
val jtDuration1 = JTDuration.ofDays(365 * 150)
val jtDuration2 = jtDuration1.plusNanos(1)
assertNotEquals(jtDuration1, jtDuration2)
val duration1 = jtDuration1.toKotlinDuration()
val duration2 = jtDuration1.toKotlinDuration()
assertEquals(duration1, duration2)
assertEquals(Duration.days(365 * 150), duration2)
}
@Test
@@ -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.
*/
@@ -49,3 +49,34 @@ internal actual fun convertDurationUnit(value: Double, sourceUnit: DurationUnit,
}
}
@SinceKotlin("1.5")
@ExperimentalTime
internal actual fun convertDurationUnitOverflow(value: Long, sourceUnit: DurationUnit, targetUnit: DurationUnit): Long {
val sourceCompareTarget = sourceUnit.scale.compareTo(targetUnit.scale)
return when {
sourceCompareTarget > 0 -> value * (sourceUnit.scale / targetUnit.scale).toLong()
sourceCompareTarget < 0 -> value / (targetUnit.scale / sourceUnit.scale).toLong()
else -> value
}
}
@SinceKotlin("1.5")
@ExperimentalTime
internal actual fun convertDurationUnit(value: Long, sourceUnit: DurationUnit, targetUnit: DurationUnit): Long {
val sourceCompareTarget = sourceUnit.scale.compareTo(targetUnit.scale)
return when {
sourceCompareTarget > 0 -> {
val scale = (sourceUnit.scale / targetUnit.scale).toLong()
val result = value * scale
when {
result / scale == value -> result
value > 0 -> Long.MAX_VALUE
else -> Long.MIN_VALUE
}
}
sourceCompareTarget < 0 -> value / (targetUnit.scale / sourceUnit.scale).toLong()
else -> value
}
}
@@ -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.
*/
@@ -21,4 +21,16 @@ internal actual fun convertDurationUnit(value: Double, sourceUnit: DurationUnit,
val otherInThis = sourceUnit.convert(1, targetUnit)
return value / otherInThis
}
}
@SinceKotlin("1.5")
@ExperimentalTime
internal actual fun convertDurationUnitOverflow(value: Long, sourceUnit: DurationUnit, targetUnit: DurationUnit): Long {
return targetUnit.convert(value, sourceUnit)
}
@SinceKotlin("1.5")
@ExperimentalTime
internal actual fun convertDurationUnit(value: Long, sourceUnit: DurationUnit, targetUnit: DurationUnit): Long {
return targetUnit.convert(value, sourceUnit)
}
+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
+134 -33
View File
@@ -4,6 +4,7 @@
*/
@file:OptIn(ExperimentalTime::class)
@file:Suppress("INVISIBLE_MEMBER")
package test.time
import test.numbers.assertAlmostEquals
@@ -12,26 +13,40 @@ import kotlin.test.*
import kotlin.time.*
import kotlin.random.*
@SharedImmutable
private val expectStorageUnit = DurationUnit.NANOSECONDS
@SharedImmutable
private val units = DurationUnit.values()
class DurationTest {
// construction white-box testing
@Test
fun construction() {
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
repeat(100) {
val value = Random.nextInt(1_000_000)
val unit = units.random()
val expected = convertDurationUnit(value.toDouble(), unit, expectStorageUnit)
assertEquals(expected, value.toDuration(unit).value)
assertEquals(expected, value.toLong().toDuration(unit).value)
assertEquals(expected, value.toDouble().toDuration(unit).value)
// nanosecond precision
val testValues = listOf(0L, 1L, Long.MAX_VALUE / 2) + List(100) { Random.nextLong(0, Long.MAX_VALUE / 2) }
for (value in testValues) {
assertEquals(value, value.toDuration(DurationUnit.NANOSECONDS).toLongNanoseconds())
assertEquals(-value, -value.toDuration(DurationUnit.NANOSECONDS).toLongNanoseconds())
}
// expressible as long nanoseconds but stored as milliseconds
for (delta in testValues) {
val value = 1L.shl(62) + delta
val expected = value - (value % 1_000_000)
assertEquals(expected, value.toDuration(DurationUnit.NANOSECONDS).toLongNanoseconds())
assertEquals(-expected, -value.toDuration(DurationUnit.NANOSECONDS).toLongNanoseconds())
}
// any int value of small units can always be represented in nanoseconds
for (unit in units.filter { it <= DurationUnit.SECONDS }) {
val scale = convertDurationUnitOverflow(1L, unit, DurationUnit.NANOSECONDS)
repeat(100) {
val value = Random.nextInt()
assertEquals(value * scale, value.toDuration(unit).toLongNanoseconds())
}
}
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(Duration.INFINITE, (Long.MAX_VALUE / 2).toDuration(DurationUnit.MILLISECONDS))
assertEquals(-Duration.INFINITE, (Long.MIN_VALUE / 2).toDuration(DurationUnit.MILLISECONDS))
assertFailsWith<IllegalArgumentException> { Double.NaN.toDuration(DurationUnit.SECONDS) }
}
@@ -64,6 +79,26 @@ class DurationTest {
}
}
@Test
fun comparison() {
run {
val d1 = Duration.nanoseconds(Long.MAX_VALUE / 2 + 1)
val d2 = Duration.nanoseconds(Long.MAX_VALUE / 2)
assertTrue(d1 >= d2, "same sign, different ranges")
// TODO: Address this contradiction
// assertTrue(d1.toLongNanoseconds() >= d2.toLongNanoseconds())
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")
}
}
@Test
fun conversionFromNumber() {
@@ -127,6 +162,15 @@ class DurationTest {
assertAlmostEquals(Duration.convert(value.toDouble(), unit, unit2), value.toDuration(unit).toDouble(unit2))
}
for (unit in units) {
assertEquals(Long.MAX_VALUE, Duration.INFINITE.toLong(unit))
assertEquals(Int.MAX_VALUE, Duration.INFINITE.toInt(unit))
assertEquals(Double.POSITIVE_INFINITY, Duration.INFINITE.toDouble(unit))
assertEquals(Long.MIN_VALUE, (-Duration.INFINITE).toLong(unit))
assertEquals(Int.MIN_VALUE, (-Duration.INFINITE).toInt(unit))
assertEquals(Double.NEGATIVE_INFINITY, (-Duration.INFINITE).toDouble(unit))
}
}
@Test
@@ -248,6 +292,14 @@ class DurationTest {
assertEquals(Duration.days(0.5), Duration.hours(6) + Duration.minutes(360))
assertEquals(Duration.seconds(0.5), Duration.milliseconds(200) + Duration.microseconds(300_000))
for (value in listOf(Duration.ZERO, Duration.nanoseconds(1), Duration.days(500 * 365))) {
for (inf in listOf(Duration.INFINITE, -Duration.INFINITE)) {
for (result in listOf(inf + inf, inf + value, inf + (-value), value + inf, (-value) + inf)) {
assertEquals(inf, result)
}
}
}
assertFailsWith<IllegalArgumentException> { Duration.INFINITE + (-Duration.INFINITE) }
}
@@ -255,6 +307,38 @@ class DurationTest {
fun subtraction() {
assertEquals(Duration.hours(10), Duration.days(0.5) - Duration.minutes(120))
assertEquals(Duration.milliseconds(850), 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))
run {
val offset = 2_000_000L
val value = Long.MAX_VALUE / 2 + offset
val base = Duration.nanoseconds(value)
val baseNs = base.toLongMilliseconds() * 1_000_000
assertEquals(baseNs, base.toLongNanoseconds()) // base stored as millis
val smallDeltas = listOf(1L, 2L, 1000L, offset / 2 - 1) + List(10) { Random.nextLong(offset / 2) }
for (smallDeltaNs in smallDeltas) {
assertEquals(base, base - Duration.nanoseconds(smallDeltaNs), "delta: $smallDeltaNs")
}
val deltas = listOf(offset + 1L, offset + 1500L) +
List(10) { Random.nextLong(offset + 1500, offset + 10000) } +
List(100) { Random.nextLong(offset + 1500, Long.MAX_VALUE / 2) }
for (deltaNs in deltas) {
val delta = Duration.nanoseconds(deltaNs)
assertEquals(deltaNs, delta.toLongNanoseconds())
assertEquals(baseNs - deltaNs, (base - delta).toLongNanoseconds(), "base: $baseNs, delta: $deltaNs")
}
}
for (value in listOf(Duration.ZERO, Duration.nanoseconds(1), Duration.days(500 * 365))) {
for (inf in listOf(Duration.INFINITE, -Duration.INFINITE)) {
for (result in listOf(inf - (-inf), inf - value, inf - (-value), value - (-inf), (-value) - (-inf))) {
assertEquals(inf, result)
}
}
}
assertFailsWith<IllegalArgumentException> { Duration.INFINITE - Duration.INFINITE }
}
@@ -272,8 +356,13 @@ class DurationTest {
assertEquals(Duration.ZERO, 0 * Duration.hours(1))
assertEquals(Duration.ZERO, Duration.seconds(1) * 0.0)
assertEquals(Duration.INFINITE, Duration.INFINITE * Double.POSITIVE_INFINITY)
assertEquals(Duration.INFINITE, Duration.INFINITE * Double.MIN_VALUE)
assertEquals(-Duration.INFINITE, Duration.INFINITE * Double.NEGATIVE_INFINITY)
assertEquals(-Duration.INFINITE, Duration.INFINITE * -1)
assertFailsWith<IllegalArgumentException> { Duration.INFINITE * 0 }
assertFailsWith<IllegalArgumentException> { 0 * Duration.INFINITE }
assertFailsWith<IllegalArgumentException> { 0.0 * Duration.INFINITE }
}
@Test
@@ -282,10 +371,18 @@ class DurationTest {
assertEquals(Duration.minutes(60), Duration.days(1) / 24.0)
assertEquals(Duration.seconds(20), Duration.minutes(2) / 6)
assertEquals(Duration.INFINITE, Duration.seconds(1) / 0.0)
assertEquals(Duration.INFINITE, Duration.seconds(1) / 0)
assertEquals(-Duration.INFINITE, -Duration.seconds(1) / 0.0)
assertEquals(Duration.INFINITE, -Duration.seconds(1) / (-0.0))
assertEquals(Duration.INFINITE, Duration.INFINITE / Int.MAX_VALUE)
assertEquals(Duration.INFINITE, -Duration.INFINITE / Int.MIN_VALUE)
assertEquals(-Duration.INFINITE, Duration.INFINITE / -1)
assertEquals(Duration.INFINITE, Duration.INFINITE / Double.MAX_VALUE)
assertFailsWith<IllegalArgumentException> { Duration.INFINITE / Double.POSITIVE_INFINITY }
assertFailsWith<IllegalArgumentException> { Duration.ZERO / 0 }
assertFailsWith<IllegalArgumentException> { Duration.ZERO / 0.0 }
}
@Test
@@ -293,6 +390,7 @@ class DurationTest {
assertEquals(24.0, Duration.days(1) / Duration.hours(1))
assertEquals(0.1, Duration.minutes(9) / Duration.hours(1.5))
assertEquals(50.0, Duration.microseconds(1) / Duration.nanoseconds(20))
assertEquals(299.0, Duration.days(365 * 299) / Duration.days(365))
assertTrue((Duration.INFINITE / Duration.INFINITE).isNaN())
}
@@ -354,14 +452,15 @@ class DurationTest {
d -= Duration.milliseconds(1678)
test(DurationUnit.MICROSECONDS, "921us", "920.5us", "920.52us", "920.516us")
d -= Duration.microseconds(920)
// sub-nanosecond precision errors
test(DurationUnit.NANOSECONDS, "516ns", "516.3ns", "516.34ns", "516.344ns", "516.3438ns")
// no sub-nanosecond precision
test(DurationUnit.NANOSECONDS, "516ns", "516.0ns", "516.00ns", "516.000ns", "516.0000ns")
d = (d - Duration.nanoseconds(516)) / 17
test(DurationUnit.NANOSECONDS, "0ns", "0.0ns", "0.02ns", "0.020ns", "0.0202ns")
test(DurationUnit.NANOSECONDS, "0ns", "0.0ns", "0.00ns", "0.000ns", "0.0000ns")
d = Duration.nanoseconds(Double.MAX_VALUE)
test(DurationUnit.DAYS, "2.08e+294d")
test(DurationUnit.NANOSECONDS, "1.80e+308ns")
// infinite
// d = Duration.nanoseconds(Double.MAX_VALUE)
// test(DurationUnit.DAYS, "2.08e+294d")
// test(DurationUnit.NANOSECONDS, "1.80e+308ns")
assertEquals("0.500000000000s", Duration.seconds(0.5).toString(DurationUnit.SECONDS, 100))
assertEquals("99999000000000.000000000000ns", Duration.seconds(99_999).toString(DurationUnit.NANOSECONDS, 15))
@@ -417,27 +516,29 @@ class DurationTest {
test(Duration.microseconds(1.005), "1.01us", "1.00us")
test(Duration.nanoseconds(950.5), "951ns", "950ns")
test(Duration.nanoseconds(85.23), "85.2ns")
test(Duration.nanoseconds(8.235), "8.24ns", "8.23ns")
test(Duration.nanoseconds(1.3), "1.30ns")
test(Duration.nanoseconds(85.23), "85.0ns")
test(Duration.nanoseconds(8.235), "8.00ns")
test(Duration.nanoseconds(1.3), "1.00ns")
test(Duration.nanoseconds(0.75), "0.75ns")
test(Duration.nanoseconds(0.7512), "0.7512ns")
test(Duration.nanoseconds(0.023), "0.023ns")
test(Duration.nanoseconds(0.0034), "0.0034ns")
test(Duration.nanoseconds(0.0000035), "0.0000035ns")
// equal to zero
// test(Duration.nanoseconds(0.75), "0.75ns")
// test(Duration.nanoseconds(0.7512), "0.7512ns")
// test(Duration.nanoseconds(0.023), "0.023ns")
// test(Duration.nanoseconds(0.0034), "0.0034ns")
// test(Duration.nanoseconds(0.0000035), "0.0000035ns")
test(Duration.ZERO, "0s")
test(Duration.days(365) * 10000, "3650000d")
test(Duration.days(300) * 100000, "3.00e+7d")
test(Duration.days(365) * 100000, "3.65e+7d")
val universeAge = Duration.days(365.25) * 13.799e9
val planckTime = Duration.seconds(5.4e-44)
// all infinite
// val universeAge = Duration.days(365.25) * 13.799e9
// val planckTime = Duration.seconds(5.4e-44)
test(universeAge, "5.04e+12d")
test(planckTime, "5.40e-44s")
test(Duration.nanoseconds(Double.MAX_VALUE), "2.08e+294d")
// test(universeAge, "5.04e+12d")
// test(planckTime, "5.40e-44s")
// test(Duration.nanoseconds(Double.MAX_VALUE), "2.08e+294d")
test(Duration.INFINITE, "Infinity")
}
@@ -1,5 +1,5 @@
/*
* Copyright 2010-2020 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.
*/
@@ -47,4 +47,12 @@ public actual enum class DurationUnit {
/** Converts the given time duration [value] expressed in the specified [sourceUnit] into the specified [targetUnit]. */
@SinceKotlin("1.3")
@ExperimentalTime
internal actual fun convertDurationUnit(value: Double, sourceUnit: DurationUnit, targetUnit: DurationUnit): Double = TODO("Wasm stdlib: convertDurationUnit")
internal actual fun convertDurationUnit(value: Double, sourceUnit: DurationUnit, targetUnit: DurationUnit): Double = TODO("Wasm stdlib: convertDurationUnit Double")
@SinceKotlin("1.5")
@ExperimentalTime
internal actual fun convertDurationUnitOverflow(value: Long, sourceUnit: DurationUnit, targetUnit: DurationUnit): Long = TODO("Wasm stdlib: convertDurationUnitOverflow Long")
@SinceKotlin("1.5")
@ExperimentalTime
internal actual fun convertDurationUnit(value: Long, sourceUnit: DurationUnit, targetUnit: DurationUnit): Long = TODO("Wasm stdlib: convertDurationUnit Long")