Skip to content

Commit

Permalink
Add some more Duration and Instant helper utils (#139)
Browse files Browse the repository at this point in the history
* Add some more Duration helpers.

* Add Duration.toMicros() / toRoundedMicros() / toRoundedMillis().

* Add some helpers for Instant.

* Avoid using `Number` for helper functions that cast to Long.

The results are unexpected and undesirable for Float and Double.
  • Loading branch information
JonathanLennox authored Sep 6, 2024
1 parent 83984af commit 6af1020
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 8 deletions.
98 changes: 90 additions & 8 deletions src/main/kotlin/org/jitsi/utils/Duration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,110 @@
package org.jitsi.utils

import java.time.Duration
import java.time.temporal.ChronoUnit

/**
* Helpers to create instance of [Duration] more easily
* Helpers to create instances of [Duration] more easily, and Kotlin operators for it
*/

val Number.nanos: Duration
val Int.nanos: Duration
get() = Duration.ofNanos(this.toLong())

val Number.ms: Duration
val Int.micros: Duration
get() = Duration.of(this.toLong(), ChronoUnit.MICROS)

val Int.ms: Duration
get() = Duration.ofMillis(this.toLong())

val Number.secs: Duration
val Int.secs: Duration
get() = Duration.ofSeconds(this.toLong())

val Number.hours: Duration
val Int.hours: Duration
get() = Duration.ofHours(this.toLong())

val Number.mins: Duration
val Int.mins: Duration
get() = Duration.ofMinutes(this.toLong())

val Number.days: Duration
val Int.days: Duration
get() = Duration.ofDays(this.toLong())

operator fun Duration.times(x: Int): Duration = Duration.ofNanos(toNanos() * x)
val Long.nanos: Duration
get() = Duration.ofNanos(this)

val Long.micros: Duration
get() = Duration.of(this, ChronoUnit.MICROS)

val Long.ms: Duration
get() = Duration.ofMillis(this)

val Long.secs: Duration
get() = Duration.ofSeconds(this)

val Long.hours: Duration
get() = Duration.ofHours(this)

val Long.mins: Duration
get() = Duration.ofMinutes(this)

val Long.days: Duration
get() = Duration.ofDays(this)

operator fun Duration.times(x: Int): Duration = this.multipliedBy(x.toLong())
operator fun Duration.times(x: Long): Duration = this.multipliedBy(x)

operator fun Int.times(x: Duration): Duration = x.multipliedBy(this.toLong())
operator fun Long.times(x: Duration): Duration = x.multipliedBy(this)

operator fun Duration.div(other: Duration): Double = toNanos().toDouble() / other.toNanos()

/** Converts this duration to the total length in milliseconds, rounded to nearest. */
fun Duration.toRoundedMillis(): Long {
var millis = this.toMillis()
val remainder = nano % NANOS_PER_MILLI
if (remainder > 499_999) {
millis++
}
return millis
}

/**
* Converts this duration to the total length in microseconds.
*
* If this duration is too large to fit in a `long` microseconds, then an
* exception is thrown.
*
* If this duration has greater than microseconds precision, then the conversion
* will drop any excess precision information as though the amount in nanoseconds
* was subject to integer division by one thousand.
*
* @return the total length of the duration in microseconds
* @throws ArithmeticException if numeric overflow occurs
*/
fun Duration.toMicros(): Long {
var tempSeconds: Long = seconds
var tempNanos: Long = nano.toLong()
if (tempSeconds < 0) {
// change the seconds and nano value to
// handle Long.MIN_VALUE case
tempSeconds += 1
tempNanos -= NANOS_PER_SECOND
}
var micros = Math.multiplyExact(tempSeconds, MICROS_PER_SECOND)
micros = Math.addExact(micros, tempNanos / NANOS_PER_MICRO)
return micros
}

/** Converts this duration to the total length in microseconds, rounded to nearest. */
fun Duration.toRoundedMicros(): Long {
var micros = this.toMicros()
val remainder = nano % NANOS_PER_MICRO
if (remainder > 499) {
micros++
}
return micros
}

private const val NANOS_PER_MICRO = 1_000
private const val NANOS_PER_MILLI = 1_000_000
private const val MICROS_PER_SECOND = 1_000_000
private const val NANOS_PER_SECOND = 1_000_000_000
65 changes: 65 additions & 0 deletions src/main/kotlin/org/jitsi/utils/Instant.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.jitsi.utils

import java.time.Instant

/**
* Helpers to create instances of [Instant] more easily, and Kotlin operators for it
*/

/**
* Converts this instant to the number of microseconds from the epoch
* of 1970-01-01T00:00:00Z.
*
*
* If this instant represents a point on the time-line too far in the future
* or past to fit in a `long` microseconds, then an exception is thrown.
*
* If this instant has greater than microseconds precision, then the conversion
* will drop any excess precision information as though the amount in nanoseconds
* was subject to integer division by one thousand.
*
* @return the number of microseconds since the epoch of 1970-01-01T00:00:00Z
* @throws ArithmeticException if numeric overflow occurs
*/
fun Instant.toEpochMicro(): Long {
if (epochSecond < 0 && nano > 0) {
val millis = Math.multiplyExact(epochSecond + 1, 1_000_000).toLong()
val adjustment: Long = (nano / 1_000 - 1).toLong()
return Math.addExact(millis, adjustment)
} else {
val millis = Math.multiplyExact(epochSecond, 1_000_000).toLong()
return Math.addExact(millis, (nano / 1000).toLong())
}
}

/**
* Obtains an instance of {@code Instant} using microseconds from the
* epoch of 1970-01-01T00:00:00Z.
* <p>
* The seconds and nanoseconds are extracted from the specified microseconds.
*
* @param epochMicro the number of microseconds from 1970-01-01T00:00:00Z
* @return an instant, not null
* @throws DateTimeException if the instant exceeds the maximum or minimum instant
*/
fun instantOfEpochMicro(epochMicro: Long): Instant {
val secs = Math.floorDiv(epochMicro, 1_000_000)
val mos = Math.floorMod(epochMicro, 1_000_000).toLong()
return Instant.ofEpochSecond(secs, mos * 1000)
}
15 changes: 15 additions & 0 deletions src/test/kotlin/org/jitsi/utils/DurationTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,29 @@ class DurationTest : ShouldSpec() {
init {
context("the duration helpers should work") {
5.nanos shouldBe Duration.ofNanos(5)
5.micros shouldBe Duration.ofNanos(5_000)
5.ms shouldBe Duration.ofMillis(5)
5.secs shouldBe Duration.ofSeconds(5)
5.mins shouldBe Duration.ofMinutes(5)
5.hours shouldBe Duration.ofHours(5)
5.days shouldBe Duration.ofDays(5)

1.days * 2 shouldBe Duration.ofDays(2)
2 * 4.hours shouldBe Duration.ofHours(8)
10.days / 2.days shouldBe 5.0
5.hours / 2.hours shouldBe 2.5

4.ms.toMicros() shouldBe 4000
2200.nanos.toMicros() shouldBe 2
2900.nanos.toMicros() shouldBe 2

2200.nanos.toRoundedMicros() shouldBe 2
2500.nanos.toRoundedMicros() shouldBe 3
2900.nanos.toRoundedMicros() shouldBe 3

2200.micros.toRoundedMillis() shouldBe 2
2500.micros.toRoundedMillis() shouldBe 3
2900.micros.toRoundedMillis() shouldBe 3
}
}
}
31 changes: 31 additions & 0 deletions src/test/kotlin/org/jitsi/utils/InstantTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.jitsi.utils

import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.shouldBe
import java.time.Instant

class InstantTest : ShouldSpec() {
init {
context("The instant helpers should work") {
Instant.ofEpochMilli(123).toEpochMicro() shouldBe 123000L
instantOfEpochMicro(123456).toEpochMilli() shouldBe 123L
instantOfEpochMicro(123987).toEpochMilli() shouldBe 123L
}
}
}

0 comments on commit 6af1020

Please sign in to comment.