blob: b3b591146af57adb01e8a64ee0a9669ee6217032 [file]
/*
* Copyright 2010-2025 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.
*/
@file:Suppress("INVISIBLE_MEMBER")
package test.time
import test.TestPlatform
import test.current
import test.numbers.assertAlmostEquals
import kotlin.math.nextDown
import kotlin.math.pow
import kotlin.test.*
import kotlin.time.*
import kotlin.random.*
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.microseconds
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.nanoseconds
import kotlin.time.Duration.Companion.seconds
private val units = DurationUnit.entries
class DurationTest {
@Test
fun constructionFromNumber() {
// nanosecond precision
val testValues = listOf(0L, 1L, MAX_NANOS) + List(100) { Random.nextLong(0, MAX_NANOS) }
for (value in testValues) {
assertEquals(value, value.toDuration(DurationUnit.NANOSECONDS).inWholeNanoseconds)
assertEquals(-value, -value.toDuration(DurationUnit.NANOSECONDS).inWholeNanoseconds)
}
// expressible as long nanoseconds but stored as milliseconds
for (delta in testValues) {
val value = (MAX_NANOS + 1) + delta
val expected = value - (value % NANOS_IN_MILLIS)
assertEquals(expected, value.toDuration(DurationUnit.NANOSECONDS).inWholeNanoseconds)
assertEquals(-expected, -value.toDuration(DurationUnit.NANOSECONDS).inWholeNanoseconds)
}
// 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).inWholeNanoseconds)
}
}
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 + 1.nanoseconds)
assertEquals(d2, d2 + 1.nanoseconds)
}
assertEquals(Long.MAX_VALUE / 1000, Long.MAX_VALUE.toDuration(DurationUnit.MICROSECONDS).inWholeMilliseconds)
assertEquals(Long.MAX_VALUE / 1000 * 1000, Long.MAX_VALUE.toDuration(DurationUnit.MICROSECONDS).toLong(DurationUnit.MICROSECONDS))
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).inWholeNanoseconds
val lessThanMaxNs = lessThanMaxDouble.toDuration(DurationUnit.NANOSECONDS).inWholeNanoseconds
assertTrue(maxNs > lessThanMaxNs, "$maxNs should be > $lessThanMaxNs")
}
assertFailsWith<IllegalArgumentException> { Double.NaN.toDuration(DurationUnit.SECONDS) }
}
@Test
fun longBoundaryValuesDurationConversion() {
val testCases = listOf(
Long.MIN_VALUE to Duration.NEG_INFINITE,
Long.MAX_VALUE to Duration.INFINITE
)
for ((value, expectedInfinite) in testCases) {
assertEquals(expectedInfinite, value.days)
assertEquals(expectedInfinite, value.hours)
assertEquals(expectedInfinite, value.minutes)
assertEquals(expectedInfinite, value.seconds)
assertEquals(expectedInfinite, value.milliseconds)
}
val usDuration = 106751991.days + 4.hours + 54.seconds + 775.milliseconds
assertEquals(-usDuration, Long.MIN_VALUE.microseconds)
assertEquals(usDuration, Long.MAX_VALUE.microseconds)
val nsDuration = 106751.days + 23.hours + 47.minutes + 16.seconds + 854.milliseconds
assertEquals(-nsDuration, Long.MIN_VALUE.nanoseconds)
assertEquals(nsDuration, Long.MAX_VALUE.nanoseconds)
}
@Test
fun equality() {
val data = listOf<Pair<Double, DurationUnit>>(
Pair(2.0, DurationUnit.DAYS),
Pair(2.0, DurationUnit.HOURS),
Pair(0.25, DurationUnit.MINUTES),
Pair(1.0, DurationUnit.SECONDS),
Pair(50.0, DurationUnit.MILLISECONDS),
Pair(0.3, DurationUnit.MICROSECONDS),
Pair(20_000_000_000.0, DurationUnit.NANOSECONDS),
Pair(1.0, DurationUnit.NANOSECONDS)
)
for ((value, unit) in data) {
repeat(10) {
val d1 = value.toDuration(unit)
val unit2 = units.random()
@OptIn(ExperimentalTime::class)
val value2 = Duration.convert(value, unit, unit2)
val d2 = value2.toDuration(unit2)
assertEquals(d1, d2, "$value $unit in $unit2")
assertEquals(d1.hashCode(), d2.hashCode())
val d3 = (value2 * 2).toDuration(unit2)
assertNotEquals(d1, d3, "$value $unit in $unit2")
}
}
run { // invariant Duration.nanoseconds(d.inWholeNanoseconds) == d when whole nanoseconds fits into Long range
val d1 = (MAX_NANOS + 1).nanoseconds
val d2 = d1.inWholeNanoseconds.nanoseconds
assertEquals(d1.inWholeNanoseconds, d2.inWholeNanoseconds)
assertEquals(d1, d2)
}
}
@Test
fun comparison() {
fun assertGreater(d1: Duration, d2: Duration, message: String) {
assertTrue(d1 > d2, message)
assertFalse(d1 <= d2, message)
assertTrue(
d1.inWholeNanoseconds > d2.inWholeNanoseconds ||
d1.inWholeNanoseconds == d2.inWholeNanoseconds && d1.inWholeMilliseconds > d2.inWholeMilliseconds,
message
)
}
val d4 = Long.MAX_VALUE.nanoseconds
val d3 = (MAX_NANOS + 1).nanoseconds
val d2 = MAX_NANOS.nanoseconds
val d1 = (MAX_NANOS - 1).nanoseconds
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 constructionFactoryFunctions() {
val n1 = Random.nextInt(Int.MAX_VALUE)
val n2 = Random.nextLong(Long.MAX_VALUE)
val n3 = Random.nextDouble()
assertEquals(n1.toDuration(DurationUnit.DAYS), n1.days)
assertEquals(n2.toDuration(DurationUnit.DAYS), n2.days)
assertEquals(n3.toDuration(DurationUnit.DAYS), n3.days)
assertEquals(n1.toDuration(DurationUnit.HOURS), n1.hours)
assertEquals(n2.toDuration(DurationUnit.HOURS), n2.hours)
assertEquals(n3.toDuration(DurationUnit.HOURS), n3.hours)
assertEquals(n1.toDuration(DurationUnit.MINUTES), n1.minutes)
assertEquals(n2.toDuration(DurationUnit.MINUTES), n2.minutes)
assertEquals(n3.toDuration(DurationUnit.MINUTES), n3.minutes)
assertEquals(n1.toDuration(DurationUnit.SECONDS), n1.seconds)
assertEquals(n2.toDuration(DurationUnit.SECONDS), n2.seconds)
assertEquals(n3.toDuration(DurationUnit.SECONDS), n3.seconds)
assertEquals(n1.toDuration(DurationUnit.MILLISECONDS), n1.milliseconds)
assertEquals(n2.toDuration(DurationUnit.MILLISECONDS), n2.milliseconds)
assertEquals(n3.toDuration(DurationUnit.MILLISECONDS), n3.milliseconds)
assertEquals(n1.toDuration(DurationUnit.MICROSECONDS), n1.microseconds)
assertEquals(n2.toDuration(DurationUnit.MICROSECONDS), n2.microseconds)
assertEquals(n3.toDuration(DurationUnit.MICROSECONDS), n3.microseconds)
assertEquals(n1.toDuration(DurationUnit.NANOSECONDS), n1.nanoseconds)
assertEquals(n2.toDuration(DurationUnit.NANOSECONDS), n2.nanoseconds)
assertEquals(n3.toDuration(DurationUnit.NANOSECONDS), n3.nanoseconds)
}
@Test
fun conversionToNumber() {
assertEquals(24, 1.days.inWholeHours)
assertEquals(0.5, 12.hours.toDouble(DurationUnit.DAYS))
assertEquals(0, 12.hours.inWholeDays)
assertEquals(15, 0.25.hours.inWholeMinutes)
assertEquals(600, 10.minutes.inWholeSeconds)
assertEquals(500, 0.5.seconds.inWholeMilliseconds)
assertEquals(50_000, 0.05.seconds.inWholeMicroseconds)
assertEquals(50_000, 0.05.milliseconds.inWholeNanoseconds)
assertEquals(365 * 86400 * 1_000_000_000L, 365.days.inWholeNanoseconds)
assertEquals(0, Duration.ZERO.inWholeNanoseconds)
assertEquals(0, Duration.ZERO.inWholeMicroseconds)
assertEquals(0, Duration.ZERO.inWholeMilliseconds)
assertEquals(10500, 10.5.seconds.inWholeMilliseconds)
assertEquals(11, 11.5.milliseconds.inWholeMilliseconds)
assertEquals(-11, (-11.5).milliseconds.inWholeMilliseconds)
assertEquals(252_000_000, 252.milliseconds.inWholeNanoseconds)
assertEquals(Long.MAX_VALUE, (365.days * 293).inWholeNanoseconds) // clamping overflowed value
repeat(100) {
val value = Random.nextLong(1000)
val unit = units.random()
val unit2 = units.random()
@OptIn(ExperimentalTime::class)
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
fun componentsOfProperSum() {
repeat(100) {
val isNsRange = Random.nextBoolean()
val d = if (isNsRange)
Random.nextLong(365L * 146)
else
Random.nextLong(365L * 150, 365L * 146_000_000)
val h = Random.nextInt(24)
val m = Random.nextInt(60)
val s = Random.nextInt(60)
val ns = Random.nextInt(1e9.toInt())
val expectedNs = if (isNsRange) ns else ns - (ns % NANOS_IN_MILLIS)
(d.days + h.hours + m.minutes + s.seconds + ns.nanoseconds).run {
toComponents { seconds, nanoseconds ->
assertEquals(d * 86400 + h * 3600 + m * 60 + s, seconds)
assertEquals(expectedNs, nanoseconds)
}
toComponents { minutes, seconds, nanoseconds ->
assertEquals(d * 1440 + h * 60 + m, minutes)
assertEquals(s, seconds)
assertEquals(expectedNs, nanoseconds)
}
toComponents { hours, minutes, seconds, nanoseconds ->
assertEquals(d * 24 + h, hours)
assertEquals(m, minutes)
assertEquals(s, seconds)
assertEquals(expectedNs, nanoseconds)
}
toComponents { days, hours, minutes, seconds, nanoseconds ->
assertEquals(d, days)
assertEquals(h, hours)
assertEquals(m, minutes)
assertEquals(s, seconds)
assertEquals(expectedNs, nanoseconds)
}
}
}
}
@Test
fun componentsOfCarriedSum() {
(36.hours + 90.minutes + 90.seconds + 1500.milliseconds).run {
toComponents { days, hours, minutes, seconds, nanoseconds ->
assertEquals(1, days)
assertEquals(13, hours)
assertEquals(31, minutes)
assertEquals(31, seconds)
assertEquals(500_000_000, nanoseconds)
}
}
}
@Test
fun componentsOfInfinity() {
for (d in listOf(Duration.INFINITE, -Duration.INFINITE)) {
val expected = if (d.isPositive()) Long.MAX_VALUE else Long.MIN_VALUE
d.toComponents { seconds, nanoseconds ->
assertEquals(expected, seconds)
assertEquals(0, nanoseconds)
}
d.toComponents { minutes: Long, seconds: Int, nanoseconds: Int ->
assertEquals(expected, minutes)
assertEquals(0, seconds)
assertEquals(0, nanoseconds)
}
d.toComponents { hours, minutes, seconds, nanoseconds ->
assertEquals(expected, hours)
assertEquals(0, minutes)
assertEquals(0, seconds)
assertEquals(0, nanoseconds)
}
d.toComponents { days, hours, minutes, seconds, nanoseconds ->
assertEquals(expected, days)
assertEquals(0, hours)
assertEquals(0, minutes)
assertEquals(0, seconds)
assertEquals(0, nanoseconds)
}
}
}
@Test
fun infinite() {
assertTrue(Duration.INFINITE.isInfinite())
assertTrue((-Duration.INFINITE).isInfinite())
assertTrue(Double.POSITIVE_INFINITY.nanoseconds.isInfinite())
// seconds converted to nanoseconds overflow to infinite
assertTrue(Double.MAX_VALUE.seconds.isInfinite())
assertTrue((-Double.MAX_VALUE).seconds.isInfinite())
}
@Test
fun negation() {
repeat(100) {
val value = Random.nextLong()
val unit = units.random()
assertEquals((-value).toDuration(unit), -value.toDuration(unit))
}
}
@Test
fun signAndAbsoluteValue() {
val negative = -1.seconds
val positive = 1.seconds
val zero = Duration.ZERO
assertTrue(negative.isNegative())
assertFalse(zero.isNegative())
assertFalse(positive.isNegative())
assertFalse(negative.isPositive())
assertFalse(zero.isPositive())
assertTrue(positive.isPositive())
assertEquals(positive, negative.absoluteValue)
assertEquals(positive, positive.absoluteValue)
assertEquals(zero, zero.absoluteValue)
}
@Test
fun negativeZero() {
fun equivalentToZero(value: Duration) {
assertEquals(Duration.ZERO, value)
assertEquals(Duration.ZERO, value.absoluteValue)
assertEquals(value, value.absoluteValue)
assertEquals(value, value.absoluteValue)
assertFalse(value.isNegative())
assertFalse(value.isPositive())
assertEquals(Duration.ZERO.toString(), value.toString())
assertEquals(Duration.ZERO.toIsoString(), value.toIsoString())
assertEquals(Duration.ZERO.toDouble(DurationUnit.SECONDS), value.toDouble(DurationUnit.SECONDS))
assertEquals(0, Duration.ZERO.compareTo(value))
assertEquals(0, Duration.ZERO.toDouble(DurationUnit.NANOSECONDS).compareTo(value.toDouble(DurationUnit.NANOSECONDS)))
}
equivalentToZero((-0.0).seconds)
equivalentToZero((-0.0).toDuration(DurationUnit.DAYS))
equivalentToZero(-Duration.ZERO)
equivalentToZero((-1).seconds / Double.POSITIVE_INFINITY)
equivalentToZero(0.seconds / -1)
equivalentToZero((-1).seconds * 0.0)
equivalentToZero(0.seconds * -1)
}
@Test
fun addition() {
assertEquals(1.5.hours, 1.hours + 30.minutes)
assertEquals(0.5.days, 6.hours + 360.minutes)
assertEquals(0.5.seconds, 200.milliseconds + 300_000.microseconds)
for (value in listOf(Duration.ZERO, 1.nanoseconds, (500 * 365).days)) {
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) }
}
@Test
fun subtraction() {
assertEquals(10.hours, 0.5.days - 120.minutes)
assertEquals(850.milliseconds, 1.seconds - 150.milliseconds)
assertEquals(1150.milliseconds, 1.seconds - (-150).milliseconds)
assertEquals(1.milliseconds, Long.MAX_VALUE.microseconds - (Long.MAX_VALUE - 1_000).microseconds)
assertEquals((-1).milliseconds, (Long.MAX_VALUE - 1_000).microseconds - Long.MAX_VALUE.microseconds)
run {
val offset = 2L * NANOS_IN_MILLIS
val value = MAX_NANOS + offset
val base = value.nanoseconds
val baseNs = base.inWholeMilliseconds * NANOS_IN_MILLIS
assertEquals(baseNs, base.inWholeNanoseconds) // base stored as millis
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 - smallDeltaNs.nanoseconds, "delta: $smallDeltaNs")
}
val deltas = listOf(offset + 1L, offset + 1500L) +
List(10) { Random.nextLong(offset + 1500, offset + 10000) } +
List(100) { Random.nextLong(offset + 1500, MAX_NANOS) }
for (deltaNs in deltas) {
val delta = deltaNs.nanoseconds
assertEquals(deltaNs, delta.inWholeNanoseconds)
assertEquals(baseNs - deltaNs, (base - delta).inWholeNanoseconds, "base: $baseNs, delta: $deltaNs")
}
}
for (value in listOf(Duration.ZERO, 1.nanoseconds, (500 * 365).days)) {
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 }
}
@Test
fun multiplication() {
assertEquals(1.days, 12.hours * 2)
assertEquals(1.days, 60.minutes * 24.0)
assertEquals(1.microseconds, 20.nanoseconds * 50)
assertEquals(1.days, 2 * 12.hours)
assertEquals(12.5.hours, 12.5 * 60.minutes)
assertEquals(1.microseconds, 50 * 20.nanoseconds)
assertEquals(Duration.ZERO, 0 * 1.hours)
assertEquals(Duration.ZERO, 1.seconds * 0.0)
run { // promoting nanos range to millis range after multiplication
val value = MAX_NANOS
assertEquals(value, (value.nanoseconds * 1_000_000).inWholeMilliseconds)
assertEquals(value / 1000, (value.nanoseconds * 1_000).inWholeMilliseconds)
assertEquals(Duration.INFINITE, (Long.MAX_VALUE / 1000 + 1).nanoseconds * 1_000_000_000)
}
run {
val value = MAX_NANOS / Int.MAX_VALUE
assertTrue((value.nanoseconds * Int.MIN_VALUE).inWholeNanoseconds < -MAX_NANOS)
}
assertEquals(Duration.INFINITE, Int.MAX_VALUE.days * Int.MAX_VALUE)
assertEquals(-Duration.INFINITE, Int.MAX_VALUE.days * 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)
assertEquals(-Duration.INFINITE, Duration.INFINITE * -1)
assertFailsWith<IllegalArgumentException> { Duration.INFINITE * 0 }
assertFailsWith<IllegalArgumentException> { 0.0 * Duration.INFINITE }
}
@Test
fun divisionByNumber() {
assertEquals(12.hours, 1.days / 2)
assertEquals(60.minutes, 1.days / 24.0)
assertEquals(20.seconds, 2.minutes / 6)
assertEquals(365.days, (365 * 299).days / 299)
assertEquals(365.days, (365 * 299.5).days / 299.5)
run {
val value = MAX_NANOS
assertEquals(value, (value.milliseconds / 1_000_000).inWholeNanoseconds)
}
assertEquals(Duration.INFINITE, 1.seconds / 0)
assertEquals(-Duration.INFINITE, -1.seconds / 0.0)
assertEquals(Duration.INFINITE, -1.seconds / (-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
fun divisionByDuration() {
assertEquals(24.0, 1.days / 1.hours)
assertEquals(0.1, 9.minutes / 1.5.hours)
assertEquals(50.0, 1.microseconds / 20.nanoseconds)
assertEquals(299.0, (365 * 299).days / 365.days)
assertTrue((Duration.INFINITE / Duration.INFINITE).isNaN())
assertTrue((Duration.ZERO / Duration.ZERO).isNaN())
}
@Test
fun truncation() {
fun expect(expected: Duration, value: Duration, unit: DurationUnit) {
assertEquals(expected, value.truncateTo(unit))
assertEquals(-expected, (-value).truncateTo(unit))
}
for (unit in units) {
expect(Duration.ZERO, Duration.ZERO, unit)
expect(Duration.INFINITE, Duration.INFINITE, unit)
expect(Duration.ZERO, 1.toDuration(unit) - 1.nanoseconds, unit)
repeat(100) {
val whole = Random.nextInt(100_000).toDuration(unit)
expect(whole, whole, unit)
if (unit > DurationUnit.NANOSECONDS) {
val part = Random.nextLong(1, 1.toDuration(unit).inWholeNanoseconds).nanoseconds
expect(Duration.ZERO, part, unit)
expect(whole, whole + part, unit)
}
}
}
repeat(10) {
val d = Random.nextLong().nanoseconds
expect(d, d, DurationUnit.NANOSECONDS)
}
expect(12.microseconds, 12998.nanoseconds, DurationUnit.MICROSECONDS)
expect(1503.milliseconds, 1503_889_404.nanoseconds, DurationUnit.MILLISECONDS)
expect(340.seconds, 340_990_567_444L.nanoseconds, DurationUnit.SECONDS)
expect(3.minutes, 200.seconds, DurationUnit.MINUTES)
expect(4.hours, 250.minutes, DurationUnit.HOURS)
expect(1.days, 30.hours, DurationUnit.DAYS)
// big durations
run {
val d = (Long.MAX_VALUE / 4).milliseconds
for (unit in units) {
if (unit <= DurationUnit.MILLISECONDS) {
expect(d, d, unit)
} else {
expect(d.toLong(unit).toDuration(unit), d, unit)
}
}
}
}
@Test
fun parseAndFormatIsoString() {
fun test(duration: Duration, vararg isoStrings: String) {
assertEquals(isoStrings.first(), duration.toIsoString())
for (isoString in isoStrings) {
assertEquals(duration, Duration.parseIsoString(isoString), isoString)
assertEquals(duration, Duration.parse(isoString), isoString)
assertEquals(duration, Duration.parseIsoStringOrNull(isoString), isoString)
assertEquals(duration, Duration.parseOrNull(isoString), isoString)
}
}
// zero
test(Duration.ZERO, "PT0S", "P0D", "PT0H", "PT0M", "P0DT0H", "PT0H0M", "PT0H0S", "PT000000000000000000000000H")
// single unit
test(1.days, "PT24H", "P1D", "PT1440M", "PT86400S")
test(1.hours, "PT1H")
test(1.minutes, "PT1M")
test(1.seconds, "PT1S", "PT000000000000000000000001S")
test(1.milliseconds, "PT0.001S")
test(1.microseconds, "PT0.000001S")
test(1.nanoseconds, "PT0.000000001S", "PT0.0000000009S")
test(0.9.nanoseconds, "PT0.000000001S")
// rounded to zero
test(0.1.nanoseconds, "PT0S")
test(Duration.ZERO, "PT0S", "PT0.0000000004S")
// several units combined
test(1.days + 1.minutes, "PT24H1M")
test(1.days + 1.seconds, "PT24H0M1S")
test(1.days + 1.milliseconds, "PT24H0M0.001S")
test(1.hours + 30.minutes, "PT1H30M")
test(1.hours + 500.milliseconds, "PT1H0M0.500S")
test(2.minutes + 500.milliseconds, "PT2M0.500S")
test(1.hours + 500.milliseconds, "PT1H0M0.500S", "PT1H0.5S")
test(90_500.milliseconds, "PT1M30.500S")
test(20.minutes + 34.seconds + 100000395.nanoseconds, "PT20M34.100000395S", "PT1234.1000003945S")
// with sign
test(-1.days + 15.minutes, "-PT23H45M", "PT-23H-45M", "+PT-24H+15M")
test(-1.days - 15.minutes, "-PT24H15M", "PT-24H-15M", "-PT25H-45M", "-PT25H-44M-60.0S")
test(Duration.ZERO, "PT0S", "P1DT-24H", "+PT-1H+60M", "-PT1M-60S", "PT-000000000000000000000000H")
test((-1.1).seconds, "-PT1.100S", "PT-1.1S")
test((-0.5).seconds, "-PT0.500S", "PT-0.5S", "PT-1M+59.5S")
// nanoseconds rounding
for (i in 0..4) {
test(Duration.ZERO, "PT0S", "PT0.000000000${i}S")
test(1.nanoseconds, "PT0.000000001S", "PT0.000000000${i + 5}S")
}
// close to infinite
test(
53375995583.days + 15.hours + 36.minutes + 27.seconds + 902.milliseconds + 999999.nanoseconds,
"PT1281023894007H36M27.902S", "P53375995583DT15H36M27.9029999994S", "PT4611686018427387.902S"
)
test(
(-53375995583).days - 15.hours - 36.minutes - 27.seconds - 902.milliseconds - 999999.nanoseconds,
"-PT1281023894007H36M27.902S", "P-53375995583DT-15H-36M-27.9029999994S", "PT-4611686018427387.902S"
)
// infinite
test(
Duration.INFINITE, "PT9999999999999H", "PT+10000000000000H", "-PT-9999999999999H", "-PT-1234567890123456789012S",
"PT+000000000000000001234567890123456789012H", "P+9999999999999DT+9999999999999H",
"PT9999999999999H1M99999999999999999999999999.1S", "P12345678901234567890D",
"P999999999999DT1S", "P999999999999DT-1S", "P1DT12345678901234567890S", "P-1DT12345678901234567890S",
"PT106751991100H9223372036854000S", "PT106751991100H9223372036854000.1S",
"P53375995583DT15H36M27.9029999995S", "PT4611686018427387.903S",
"PT1H9223372036854775.0S", "PT76861433640456M12345S", "PT4611686018427387908S", "PT4611686018427387901S"
)
test(
-Duration.INFINITE, "-PT9999999999999H", "P-12345678901234567890D", "-PT10000000000000H",
"PT-1234567890123456789012S", "PT-000000000000000001234567890123456789012H", "P-9999999999999DT-9999999999999H",
"PT-9999999999999H1M-99999999999999999999999999.1S",
"P-999999999999DT1S", "P-999999999999DT-1S", "P1DT-12345678901234567890S", "P-1DT-12345678901234567890S",
"PT-106751991100H-9223372036854000S", "PT-106751991100H-9223372036854000.1S",
"P-53375995583DT-15H-36M-28.0S", "PT1H-9223372036854775.0S",
"P-53375995583DT-15H-36M-27.9029999995S", "PT-4611686018427387.903S"
)
}
@Test
fun parseIsoStringFailing() {
for (invalidValue in listOf(
"", " ", "P", "PT", "P1DT", "P1", "PT1", "0", "+P", "+", "-", "h", "H", "something", "PT1HT", ",", "PT,123H",
"1m", "1d", "2d 11s", "Infinity", "-Infinity",
"P+12+34D", "P12-34D", "PT1234567890-1234567890S", "PhD", "PT+H", "PT0.123", "P1DW", "PT1H+", "PT1H+W", "PT1M-", "PT1M+W",
"PT0.1234567890123456789", "PT1234567890123456789", "PT0",
" P1D", "PT1S ",
"P3W", "PT4611686018427387908999W",
"P1Y", "P1M", "P1S", "PT1D", "PT1Y",
"PT1S2S", "PT1S2H", "PT1H1D", "PT3M5D", "PT0.1S1H",
"P9999999999999DT-9999999999999H", "P-9999999999999DT+9999999999999H",
"PT1.5H", "PT0.5D", "PT.5S", "PT0.25.25S", "PT1.S",
"PT+-2H", "PT-+2H", "PT+-01234567890123456S",
"PT-9999999999999H1M99999999999999999999999999.1S",
"P9999999999999DT-999999999999999999M", "P9999999999999DT-999999999999999999.0S", "P9999999999999DT-999999999999999999S",
"PT9999999999999H-999999999999999999M", "PT9999999999999H-999999999999999999.0S", "PT9999999999999H-999999999999999999S",
"PT999999999999999M-999999999999999999S", "PT-9999999999999H999999999999999999.0S",
"PT9223372036854775H-9223372036854775.0S", "PT-9223372036854775H9223372036854775.0S",
"PT4611686018427387908999"
)) {
assertNull(Duration.parseIsoStringOrNull(invalidValue), invalidValue)
assertFailsWith<IllegalArgumentException>(invalidValue) { Duration.parseIsoString(invalidValue) }.let { e ->
assertContains(e.message!!, "'$invalidValue'")
}
}
}
@Test
fun parseAndFormatInUnits() {
if (TestPlatform.current == TestPlatform.WasmWasi) return
var d = 1.days + 15.hours + 31.minutes + 45.seconds +
678.milliseconds + 920.microseconds + 516.34.nanoseconds
fun test(unit: DurationUnit, vararg representations: String) {
assertFails { d.toString(unit, -1) }
assertEquals(representations.toList(), representations.indices.map { d.toString(unit, it) })
for ((decimals, string) in representations.withIndex()) {
val d1 = Duration.parse(string)
assertEquals(d1, Duration.parseOrNull(string))
if (!(d1 == d || (d1 - d).absoluteValue <= (0.5 * 10.0.pow(-decimals)).toDuration(unit))) {
fail("Parsed value $d1 (from $string) is too far from the real value $d")
}
}
}
test(DurationUnit.DAYS, "2d", "1.6d", "1.65d", "1.647d")
test(DurationUnit.HOURS, "40h", "39.5h", "39.53h")
test(DurationUnit.MINUTES, "2372m", "2371.8m", "2371.76m")
d -= 39.hours
test(DurationUnit.SECONDS, "1906s", "1905.7s", "1905.68s", "1905.679s")
d -= 1904.seconds
test(DurationUnit.MILLISECONDS, "1679ms", "1678.9ms", "1678.92ms", "1678.921ms")
d -= 1678.milliseconds
test(DurationUnit.MICROSECONDS, "921us", "920.5us", "920.52us", "920.516us")
d -= 920.microseconds
// no sub-nanosecond precision
test(DurationUnit.NANOSECONDS, "516ns", "516.0ns", "516.00ns", "516.000ns", "516.0000ns")
d = (d - 516.nanoseconds) / 17
test(DurationUnit.NANOSECONDS, "0ns", "0.0ns", "0.00ns", "0.000ns", "0.0000ns")
// infinite
// d = Duration.nanoseconds(Double.MAX_VALUE)
// test(DurationUnit.DAYS, "2.08e+294d")
// test(DurationUnit.NANOSECONDS, "1.80e+308ns")
assertEquals("0.500000000000s", 0.5.seconds.toString(DurationUnit.SECONDS, 100))
assertEquals("99999000000000.000000000000ns", 99_999.seconds.toString(DurationUnit.NANOSECONDS, 15))
assertContains(
listOf(
"-4611686018427388000000000.000000000000ns",
"-4611686018427387904000000.000000000000ns"
),
(-(MAX_MILLIS - 1).milliseconds).toString(DurationUnit.NANOSECONDS, 15)
)
d = Duration.INFINITE
test(DurationUnit.DAYS, "Infinity", "Infinity")
d = -Duration.INFINITE
test(DurationUnit.NANOSECONDS, "-Infinity", "-Infinity")
}
@Test
fun parseAndFormatDefault() {
testDefaultParsing(101.days, "101d", "2424h")
testDefaultParsing(45.3.days, "45d 7h 12m", "45.3d", "45d 7.2h") // 0.3d == 7.2h
testDefaultParsing(45.days, "45d")
testDefaultParsing(40.5.days, "40d 12h", "40.5d", "40d 720m")
testDefaultParsing(40.days + 20.minutes, "40d 0h 20m", "40d 20m", "40d 1200s")
testDefaultParsing(40.days + 20.seconds, "40d 0h 0m 20s", "40d 20s")
testDefaultParsing(40.days + 100.nanoseconds, "40d 0h 0m 0.000000100s", "40d 100ns")
testDefaultParsing(40.hours + 15.minutes, "1d 16h 15m", "40h 15m")
testDefaultParsing(40.hours, "1d 16h", "40h")
testDefaultParsing(12.5.hours, "12h 30m")
testDefaultParsing(12.hours + 15.seconds, "12h 0m 15s")
testDefaultParsing(12.hours + 1.nanoseconds, "12h 0m 0.000000001s")
testDefaultParsing(30.minutes, "30m")
testDefaultParsing(17.5.minutes, "17m 30s")
testDefaultParsing(16.5.minutes, "16m 30s")
testDefaultParsing(1097.1.seconds, "18m 17.1s")
testDefaultParsing(90.36.seconds, "1m 30.36s")
testDefaultParsing(50.seconds, "50s")
testDefaultParsing(1.3.seconds, "1.3s")
testDefaultParsing(1.seconds, "1s")
testDefaultParsing(0.5.seconds, "500ms")
testDefaultParsing(40.2.milliseconds, "40.2ms")
testDefaultParsing(4.225.milliseconds, "4.225ms")
testDefaultParsing(4.24501.milliseconds, "4.245010ms", "4ms 245us 10ns")
testDefaultParsing(1.milliseconds, "1ms")
testDefaultParsing(0.75.milliseconds, "750us")
testDefaultParsing(75.35.microseconds, "75.35us")
testDefaultParsing(7.25.microseconds, "7.25us")
testDefaultParsing(1.035.microseconds, "1.035us")
testDefaultParsing(1.005.microseconds, "1.005us")
testDefaultParsing(1800.nanoseconds, "1.8us", "1800ns", "0.0000000005h")
testDefaultParsing(950.5.nanoseconds, "951ns")
testDefaultParsing(85.23.nanoseconds, "85ns")
testDefaultParsing(8.235.nanoseconds, "8ns")
testDefaultParsing(1.nanoseconds, "1ns", "0.9ns", "0.001us", "0.0009us")
testDefaultParsing(1.3.nanoseconds, "1ns")
testDefaultParsing(0.75.nanoseconds, "1ns")
testDefaultParsing(0.7512.nanoseconds, "1ns")
// equal to zero
testDefaultParsing(Duration.ZERO, "0s", "0.4ns", "0000.0000ns")
testDefaultParsing(0.023.nanoseconds, "0s", "0.023ns")
testDefaultParsing(0.0034.nanoseconds, "0s", "0.0034ns")
testDefaultParsing(0.0000035.nanoseconds, "0s", "0.0000035ns")
testDefaultParsing(365.days * 10000, "3650000d")
testDefaultParsing(300.days * 100000, "30000000d")
testDefaultParsing(365.days * 100000, "36500000d")
testDefaultParsing((MAX_MILLIS - 1).milliseconds, "53375995583d 15h 36m 27.902s") // max finite value
testDefaultParsing(10.days, "10d")
testDefaultParsing(1.days + 12.hours, "1d 12h", "1d12h")
testDefaultParsing((-5).days - 23.hours - 2.minutes, "-(5d 23h 2m)", "-5d23h2m")
testDefaultParsing(9.days + 7.hours + 28.minutes + 6.seconds, "9d 7h 28m 6s", "8d 31h 28m 6s")
testDefaultParsing(
15.days + 1.hours + 45.minutes + 31.seconds + 30123.microseconds,
"15d 1h 45m 31.030123s",
"15d 98m 451s 30.123ms"
)
testDefaultParsing(
102.days + 9.hours + 12.minutes + 45.seconds + 31.milliseconds + 210.123.microseconds,
"102d 9h 12m 45.031210123s",
"100d 57h 12m 45s 28ms 3210.12345us"
)
testDefaultParsing(
8771.days + 14.hours + 52.minutes + 42.seconds + 997.milliseconds + 438.microseconds + 653.nanoseconds,
"8771d 14h 52m 42.997438653s",
"8765d 151h 452m 1233s 9873ms 123451us 987653.12345678ns"
)
testDefaultParsing(
-(1835.days + 14.hours + 27.minutes + 46.619332752.seconds),
"-(1835d 14h 27m 46.619332752s)",
"-(01257d 012395h 0087542m 000115874s 0871542ms 00951487us 000125845751.985487515ns)"
)
testDefaultParsing(
219124.days + 8.hours + 54.minutes + 38.909.seconds,
"219124d 8h 54m 38.909s",
"12345678910ms 18901234567890123us 18765432109876543ns"
)
testDefaultParsing(
10_000_000_000_000_100L.microseconds + 10_000_000_000_900_000L.nanoseconds,
"115856d 11h 33m 20s",
"10000000000000100us 10000000000900000ns"
)
testDefaultParsing(
4602453423018496273.milliseconds + Long.MAX_VALUE.microseconds + Long.MAX_VALUE.nanoseconds,
"53375995583d 15h 36m 27.902s",
"4602453423018496273ms ${Long.MAX_VALUE}us ${Long.MAX_VALUE}ns"
)
testDefaultParsing(Long.MAX_VALUE.microseconds, "106751991d 4h 0m 54.775s", "${Long.MAX_VALUE}.99999999999us")
testDefaultParsing(
1.days + 2.hours + 3.minutes + 4.seconds + 5.milliseconds + 6.microseconds + 7.nanoseconds,
"1d 2h 3m 4.005006007s",
"1d2h3m 4s 5ms 6us 7ns"
)
// close to infinite
testDefaultParsing(
53375995583.days,
"${MAX_MILLIS / MILLIS_IN_DAY}d"
)
testDefaultParsing(
53375995583.days + 15.hours,
"53375995583d 15h", "${MAX_MILLIS / MILLIS_IN_HOUR}h"
)
testDefaultParsing(
53375995583.days + 15.hours + 36.minutes,
"53375995583d 15h 36m", "${MAX_MILLIS / MILLIS_IN_MINUTE}m"
)
testDefaultParsing(
53375995583.days + 15.hours + 36.minutes + 27.seconds,
"53375995583d 15h 36m 27s", "${MAX_MILLIS / MILLIS_IN_SECOND}s"
)
testDefaultParsing(
53375995583.days + 15.hours + 36.minutes + 27.seconds + 902.milliseconds,
"53375995583d 15h 36m 27.902s", "${MAX_MILLIS - 1}ms"
)
// 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")
testDefaultParsing(
Duration.INFINITE, "Infinity", "53375995583d 20h", "+Infinity", "123456789012345d 123456789012345h",
"4602453423018496274ms ${Long.MAX_VALUE}us ${Long.MAX_VALUE}ns"
)
testDefaultParsing(90_000_000_000_000.minutes, "Infinity", "90000000000000m")
testDefaultParsing((-90_000_000_000_000).minutes, "-Infinity", "-90000000000000m")
testDefaultParsing(
Duration.INFINITE, "Infinity", "${MAX_MILLIS / MILLIS_IN_DAY + 1}d", "${MAX_MILLIS / MILLIS_IN_HOUR + 1}h",
"${MAX_MILLIS / MILLIS_IN_MINUTE + 1}m", "${MAX_MILLIS / MILLIS_IN_SECOND + 1}s", "${MAX_MILLIS}ms"
)
testDefaultParsing(-Duration.INFINITE, "-Infinity", "-(53375995583d 20h)")
}
@Test
fun nanosecondsRounding() {
for (i in 0..4) {
testDefaultParsing(Duration.ZERO, "0s", "0.000000000${i}s", "0.000000${i}ms", "0.000${i}us", "0.${i}ns")
testDefaultParsing(1.nanoseconds, "1ns", "0.000000000${i + 5}s", "0.000000${i + 5}ms", "0.000${i + 5}us", "0.${i + 5}ns")
testDefaultParsing(1.seconds - 1.nanoseconds, "999.999999ms", "0.999999999${i}s")
testDefaultParsing(1.seconds, "1s", "0.999999999${i + 5}s")
testDefaultParsing(1.milliseconds - 1.nanoseconds, "999.999us", "0.999999${i}ms")
testDefaultParsing(1.milliseconds, "1ms", "0.999999${i + 5}ms")
testDefaultParsing(1.microseconds - 1.nanoseconds, "999ns", "0.999${i}us")
testDefaultParsing(1.microseconds, "1us", "0.999${i + 5}us")
}
}
@Test
fun parseDefaultFailing() {
for (invalidValue in listOf(
"", " ", "P", "PT", "P1DT", "P1", "PT1", "0", "+P", "+", "-", "h", "H", "something", "P,D",
"1234567890123456789012ns", "Inf", "-Infinity value",
"1s ", " 1s",
"1d 1m 1h", "1s 2s",
"-12m 15s", "-12m -15s", "-()", "-(12m 30s",
"+12m 15s", "+12m +15s", "+()", "+(12m 30s",
"()", "(12m 30s)", "-(12m) 30s", "(12m) 30s", "12m (30s)",
"12.5m 11.5s", ".2s", "0.1553.39m",
"12.h", "12.55", "123", "123uw", "876nm", "531n",
"P+12+34D", "P12-34D", "PT1234567890-1234567890S",
" P1D", "PT1S ",
"P1Y", "P1M", "P1S", "PT1D", "PT1Y",
"PT1S2S", "PT1S2H",
"P9999999999999DT-9999999999999H",
"PT1.5H", "PT0.5D", "PT.5S", "PT0.25.25S",
)) {
assertNull(Duration.parseOrNull(invalidValue), invalidValue)
assertFailsWith<IllegalArgumentException>(invalidValue) { Duration.parse(invalidValue) }.let { e ->
assertContains(e.message!!, "'$invalidValue'")
}
}
}
@Test
fun parseComponentOrdering() {
val validDefaultFormat = listOf(
"1d 2h 3m 4s 5ms 6us 7ns",
"1d 2h 3m 4s",
"2h 3m 4s",
"3m 4s 5ms",
"4s 5ms 6us 7ns",
"1d 3m",
"1d 4s",
"2h 5ms",
)
for (valid in validDefaultFormat) {
assertNotNull(Duration.parseOrNull(valid), "Should parse valid ordering: $valid")
Duration.parse(valid) // should not throw
}
val invalidDefaultFormat = listOf(
"1m 2h",
"1s 2m",
"1us 2ms",
"1ms 2s",
"1ns 2us",
"1h 2d",
"3m 2h 1d",
"1d 3m 2h",
"2h 4s 3m",
"1d 2h 3m 4s 3m",
"5ms 4s 3m 2h 1d",
"1ns 1us 1ms 1s 1m 1h 1d",
)
for (invalid in invalidDefaultFormat) {
assertNull(Duration.parseOrNull(invalid), "Should not parse invalid ordering: $invalid")
assertFailsWith<IllegalArgumentException>("Should throw for invalid ordering: $invalid") {
Duration.parse(invalid)
}
}
val validIsoFormat = listOf(
"P1DT2H3M4S",
"PT2H3M4S",
"PT3M4S",
"P1DT4S",
"PT2H4S",
)
for (valid in validIsoFormat) {
assertNotNull(Duration.parseIsoStringOrNull(valid), "Should parse valid ISO ordering: $valid")
Duration.parseIsoString(valid) // should not throw
}
val invalidIsoFormat = listOf(
"PT1M2H",
"PT1S2M",
"PT2H1S3M",
"P1DT3M2H",
"PT1S2M3H",
"PT2H3M2H",
)
for (invalid in invalidIsoFormat) {
assertNull(Duration.parseIsoStringOrNull(invalid), "Should not parse invalid ISO ordering: $invalid")
assertFailsWith<IllegalArgumentException>("Should throw for invalid ISO ordering: $invalid") {
Duration.parseIsoString(invalid)
}
}
}
}