diff --git a/libraries/stdlib/src/kotlin/time/Duration.kt b/libraries/stdlib/src/kotlin/time/Duration.kt index 6e33639b841..cf570a56805 100644 --- a/libraries/stdlib/src/kotlin/time/Duration.kt +++ b/libraries/stdlib/src/kotlin/time/Duration.kt @@ -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 { +public value class Duration internal constructor(private val rawValue: Long) : Comparable { - 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 \ No newline at end of file diff --git a/libraries/stdlib/test/time/DurationTest.kt b/libraries/stdlib/test/time/DurationTest.kt index 55a0fab555a..cb361af04c5 100644 --- a/libraries/stdlib/test/time/DurationTest.kt +++ b/libraries/stdlib/test/time/DurationTest.kt @@ -8,6 +8,7 @@ package test.time import test.numbers.assertAlmostEquals +import kotlin.math.nextDown import kotlin.native.concurrent.SharedImmutable import kotlin.test.* import kotlin.time.* @@ -19,17 +20,17 @@ private val units = DurationUnit.values() class DurationTest { @Test - fun construction() { + fun constructionFromNumber() { // 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) { 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) + val value = (MAX_NANOS + 1) + delta + val expected = value - (value % NANOS_IN_MILLIS) 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 * 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)) + assertEquals(Duration.INFINITE, (MAX_MILLIS).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 { Double.NaN.toDuration(DurationUnit.SECONDS) } } @@ -77,31 +94,44 @@ class DurationTest { 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 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") + fun assertGreater(d1: Duration, d2: Duration, message: String) { + assertTrue(d1 > d2, message) + assertFalse(d1 <= d2, message) + assertTrue( + d1.toLongNanoseconds() > d2.toLongNanoseconds() || + d1.toLongNanoseconds() == d2.toLongNanoseconds() && d1.toLongMilliseconds() > d2.toLongMilliseconds(), + message + ) } + + 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 - fun conversionFromNumber() { + fun constructionFactoryFunctions() { val n1 = Random.nextInt(Int.MAX_VALUE) val n2 = Random.nextLong(Long.MAX_VALUE) val n3 = Random.nextDouble() @@ -309,22 +339,23 @@ class DurationTest { 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)) + assertEquals(Duration.milliseconds(-1), Duration.microseconds(Long.MAX_VALUE - 1_000) - Duration.microseconds(Long.MAX_VALUE)) run { - val offset = 2_000_000L - val value = Long.MAX_VALUE / 2 + offset + val offset = 2L * NANOS_IN_MILLIS + val value = MAX_NANOS + offset 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 - 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) { 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) } + List(100) { Random.nextLong(offset + 1500, MAX_NANOS) } for (deltaNs in deltas) { val delta = Duration.nanoseconds(deltaNs) assertEquals(deltaNs, delta.toLongNanoseconds()) @@ -356,6 +387,21 @@ class DurationTest { assertEquals(Duration.ZERO, 0 * Duration.hours(1)) 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.MIN_VALUE) assertEquals(-Duration.INFINITE, Duration.INFINITE * Double.NEGATIVE_INFINITY) @@ -370,6 +416,13 @@ class DurationTest { assertEquals(Duration.hours(12), Duration.days(1) / 2) assertEquals(Duration.minutes(60), Duration.days(1) / 24.0) 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.0) @@ -393,6 +446,7 @@ class DurationTest { assertEquals(299.0, Duration.days(365 * 299) / Duration.days(365)) assertTrue((Duration.INFINITE / Duration.INFINITE).isNaN()) + assertTrue((Duration.ZERO / Duration.ZERO).isNaN()) } @Test