diff --git a/README.md b/README.md index 87543d8..acd7d53 100644 --- a/README.md +++ b/README.md @@ -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)); +``` + + diff --git a/src/main/scala/scala/compat/java8/DurationConverters.scala b/src/main/scala/scala/compat/java8/DurationConverters.scala new file mode 100644 index 0000000..55a3720 --- /dev/null +++ b/src/main/scala/scala/compat/java8/DurationConverters.scala @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2012-2017 Typesafe Inc. + */ +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) + } + +} diff --git a/src/test/java/scala/compat/java8/DurationConvertersJavaTest.java b/src/test/java/scala/compat/java8/DurationConvertersJavaTest.java new file mode 100644 index 0000000..a032ae9 --- /dev/null +++ b/src/test/java/scala/compat/java8/DurationConvertersJavaTest.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2012-2018 Lightbend Inc. + */ +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)); + } + +} \ No newline at end of file diff --git a/src/test/scala/scala/compat/java8/DurationConvertersTest.scala b/src/test/scala/scala/compat/java8/DurationConvertersTest.scala new file mode 100644 index 0000000..9ce09c1 --- /dev/null +++ b/src/test/scala/scala/compat/java8/DurationConvertersTest.scala @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2009-2017 Lightbend Inc. + */ +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) + } + } + + +}