Skip to content

Duration converters #86

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,34 @@ public class StreamConvertersExample {
}
}
```

## Converters between `scala.concurrent.duration.FiniteDuration` and `java.time.Duration`

Interconversion between Java's standard `java.time.Duration` type
and the `scala.concurrent.duration.FiniteDuration` types. The Java `Duration` does
not contain a time unit, so when converting from `FiniteDuration` the time unit used
to create it is lost.

For the opposite conversion a `Duration` can potentially express a larger time span than
a `FiniteDuration`, for such cases an exception is thrown.

Example of conversions from the Java type ways:

```scala
import scala.concurrent.duration._
import scala.compat.java8.DurationConverters

val javaDuration: java.time.Duration = 5.seconds.toJava
val finiteDuration: FiniteDuration = javaDuration.toScala
```

From Java:
```java
import scala.compat.java8.DurationConverters;
import scala.concurrent.duration.FiniteDuration;

DurationConverters.toScala(Duration.of(5, ChronoUnit.SECONDS));
DurationConverters.toJava(FiniteDuration.create(5, TimeUnit.SECONDS));
```


79 changes: 79 additions & 0 deletions src/main/scala/scala/compat/java8/DurationConverters.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright (C) 2012-2017 Typesafe Inc. <http://www.typesafe.com>
*/
package scala.compat.java8

import java.time.temporal.ChronoUnit
import java.util.concurrent.TimeUnit
import java.time.{Duration => JavaDuration}

import scala.concurrent.duration.{FiniteDuration, Duration => ScalaDuration}


/**
* This class contains static methods which convert between Java Durations
* and the durations from the Scala concurrency package. This is useful when mediating between Scala and Java
* libraries with asynchronous APIs where timeouts for example are often expressed as durations.
*/
object DurationConverters {

/**
* Transform a Java duration into a Scala duration. If the nanosecond part of the Java duration is zero the returned
* duration will have a time unit of seconds and if there is a nanoseconds part the Scala duration will have a time
* unit of nanoseconds.
*
* @throws IllegalArgumentException If the given Java Duration is out of bounds of what can be expressed with the
* Scala FiniteDuration.
*/
final def toScala(duration: java.time.Duration): scala.concurrent.duration.FiniteDuration = {
val originalSeconds = duration.getSeconds
val originalNanos = duration.getNano
if (originalNanos == 0) {
if (originalSeconds == 0) ScalaDuration.Zero
else FiniteDuration(originalSeconds, TimeUnit.SECONDS)
} else if (originalSeconds == 0) {
FiniteDuration(originalNanos, TimeUnit.NANOSECONDS)
} else {
try {
val secondsAsNanos = Math.multiplyExact(originalSeconds, 1000000000)
val totalNanos = secondsAsNanos + originalNanos
if ((totalNanos < 0 && secondsAsNanos < 0) || (totalNanos > 0 && secondsAsNanos > 0)) FiniteDuration(totalNanos, TimeUnit.NANOSECONDS)
else throw new ArithmeticException()
} catch {
case _: ArithmeticException => throw new IllegalArgumentException(s"Java duration $duration cannot be expressed as a Scala duration")
}
}
}

/**
* Transform a Scala FiniteDuration into a Java duration. Note that the Scala duration keeps the time unit it was created
* with while a Java duration always is a pair of seconds and nanos, so the unit it lost.
*/
final def toJava(duration: scala.concurrent.duration.FiniteDuration): java.time.Duration = {
if (duration.length == 0) JavaDuration.ZERO
else duration.unit match {
case TimeUnit.NANOSECONDS => JavaDuration.ofNanos(duration.length)
case TimeUnit.MICROSECONDS => JavaDuration.of(duration.length, ChronoUnit.MICROS)
case TimeUnit.MILLISECONDS => JavaDuration.ofMillis(duration.length)
case TimeUnit.SECONDS => JavaDuration.ofSeconds(duration.length)
case TimeUnit.MINUTES => JavaDuration.ofMinutes(duration.length)
case TimeUnit.HOURS => JavaDuration.ofHours(duration.length)
case TimeUnit.DAYS => JavaDuration.ofDays(duration.length)
}
}

implicit final class DurationOps(val duration: java.time.Duration) extends AnyVal {
/**
* See [[DurationConverters.toScala]]
*/
def toScala: scala.concurrent.duration.FiniteDuration = DurationConverters.toScala(duration)
}

implicit final class FiniteDurationops(val duration: scala.concurrent.duration.FiniteDuration) extends AnyVal {
/**
* See [[DurationConverters.toJava]]
*/
def toJava: java.time.Duration = DurationConverters.toJava(duration)
}

}
22 changes: 22 additions & 0 deletions src/test/java/scala/compat/java8/DurationConvertersJavaTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright (C) 2012-2018 Lightbend Inc. <http://www.lightbend.com>
*/
package scala.compat.java8;

import org.junit.Test;
import scala.concurrent.duration.FiniteDuration;
import scala.runtime.java8.*;

import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;

public class DurationConvertersJavaTest {

@Test
public void apiAccessibleFromJava() {
DurationConverters.toScala(Duration.of(5, ChronoUnit.SECONDS));
DurationConverters.toJava(FiniteDuration.create(5, TimeUnit.SECONDS));
}

}
106 changes: 106 additions & 0 deletions src/test/scala/scala/compat/java8/DurationConvertersTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright (C) 2009-2017 Lightbend Inc. <http://www.lightbend.com>
*/
package scala.compat.java8

import java.time.{Duration => JavaDuration}

import org.junit.Assert._
import org.junit.Test

import scala.util.Try

class DurationConvertersTest {

import DurationConverters._
import scala.concurrent.duration._

@Test
def scalaNanosToJavaDuration(): Unit = {
Seq[(Long, (Long, Int))](
(Long.MinValue + 1) -> (-9223372037L, 145224193), // because java duration nanos are offset from the "wrong" direction
-1000000001L -> (-2, 999999999),
-1L -> (-1, 999999999),
0L -> (0, 0),
1L -> (0, 1),
1000000001L -> (1,1),
Long.MaxValue -> (9223372036L, 854775807)
).foreach { case (n, (expSecs, expNanos)) =>
val result = n.nanos.toJava
assertEquals(s"toJava($n nanos) -> $expSecs s)", expSecs, result.getSeconds)
assertEquals(s"toJava($n nanos) -> $expNanos n)", expNanos, result.getNano)
}
}

@Test
def scalaMilliSecondsToJavaDuration(): Unit = {
Seq[(Long, (Long, Int))](
-9223372036854L -> (-9223372037L, 146000000),
-1L -> (-1L, 999000000),
0L -> (0L, 0),
1L -> (0L, 1000000),
9223372036854L -> (9223372036L, 854000000)
).foreach { case (n, (expSecs, expNanos)) =>
val result = n.millis.toJava
assertEquals(s"toJava($n millis) -> $expSecs s)", expSecs, result.getSeconds)
assertEquals(s"toJava($n millis) -> $expNanos n)", expNanos, result.getNano)
}
}

@Test
def scalaMicroSecondsToJavaDuration(): Unit = {
Seq[(Long, (Long, Int))](
-9223372036854775L -> (-9223372037L, 145225000),
-1L -> (-1L, 999999000),
0L -> (0L, 0),
1L -> (0L, 1000),
9223372036854775L -> (9223372036L, 854775000)
).foreach { case (n, (expSecs, expNanos)) =>
val result = n.micros.toJava
assertEquals(s"toJava($n micros) -> $expSecs s)", expSecs, result.getSeconds)
assertEquals(s"toJava($n micros) -> $expNanos n)", expNanos, result.getNano)
}
}

@Test
def scalaSecondsToJavaDuration(): Unit = {
Seq[(Long, (Long, Int))](
-9223372036L -> (-9223372036L, 0),
-1L -> (-1L, 0),
0L -> (0L, 0),
1L -> (1L, 0),
9223372036L -> (9223372036L, 0)
).foreach { case (n, (expSecs, expNanos)) =>
val result = n.seconds.toJava
assertEquals(expSecs, result.getSeconds)
assertEquals(expNanos, result.getNano)
}
}


@Test
def javaSecondsToScalaDuration(): Unit = {
Seq[Long](-9223372036L, -1L, 0L, 1L, 9223372036L).foreach { n =>
assertEquals(n, toScala(JavaDuration.ofSeconds(n)).toSeconds)
}
}


@Test
def javaNanosPartToScalaDuration(): Unit = {
val nanosPerSecond = 1000000000L
Seq[Long](-nanosPerSecond - 1L, 0L, 1L, nanosPerSecond - 1L).foreach { n =>
assertEquals(n, toScala(JavaDuration.ofNanos(n)).toNanos)
}
}

@Test
def unsupportedJavaDurationThrows(): Unit = {
Seq(JavaDuration.ofSeconds(-9223372037L), JavaDuration.ofSeconds(9223372037L)).foreach { d =>
val res = Try { toScala(d) }
assertTrue(s"Expected exception for $d but got success", res.isFailure)
}
}


}