Skip to content

Commit d810c75

Browse files
authored
Merge pull request #86 from johanandren/wip-85-duration-conversion-johanandren
Duration converters
2 parents e1fe4ec + b9813cb commit d810c75

File tree

4 files changed

+238
-0
lines changed

4 files changed

+238
-0
lines changed

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,34 @@ public class StreamConvertersExample {
253253
}
254254
}
255255
```
256+
257+
## Converters between `scala.concurrent.duration.FiniteDuration` and `java.time.Duration`
258+
259+
Interconversion between Java's standard `java.time.Duration` type
260+
and the `scala.concurrent.duration.FiniteDuration` types. The Java `Duration` does
261+
not contain a time unit, so when converting from `FiniteDuration` the time unit used
262+
to create it is lost.
263+
264+
For the opposite conversion a `Duration` can potentially express a larger time span than
265+
a `FiniteDuration`, for such cases an exception is thrown.
266+
267+
Example of conversions from the Java type ways:
268+
269+
```scala
270+
import scala.concurrent.duration._
271+
import scala.compat.java8.DurationConverters
272+
273+
val javaDuration: java.time.Duration = 5.seconds.toJava
274+
val finiteDuration: FiniteDuration = javaDuration.toScala
275+
```
276+
277+
From Java:
278+
```java
279+
import scala.compat.java8.DurationConverters;
280+
import scala.concurrent.duration.FiniteDuration;
281+
282+
DurationConverters.toScala(Duration.of(5, ChronoUnit.SECONDS));
283+
DurationConverters.toJava(FiniteDuration.create(5, TimeUnit.SECONDS));
284+
```
285+
286+
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright (C) 2012-2017 Typesafe Inc. <http://www.typesafe.com>
3+
*/
4+
package scala.compat.java8
5+
6+
import java.time.temporal.ChronoUnit
7+
import java.util.concurrent.TimeUnit
8+
import java.time.{Duration => JavaDuration}
9+
10+
import scala.concurrent.duration.{FiniteDuration, Duration => ScalaDuration}
11+
12+
13+
/**
14+
* This class contains static methods which convert between Java Durations
15+
* and the durations from the Scala concurrency package. This is useful when mediating between Scala and Java
16+
* libraries with asynchronous APIs where timeouts for example are often expressed as durations.
17+
*/
18+
object DurationConverters {
19+
20+
/**
21+
* Transform a Java duration into a Scala duration. If the nanosecond part of the Java duration is zero the returned
22+
* duration will have a time unit of seconds and if there is a nanoseconds part the Scala duration will have a time
23+
* unit of nanoseconds.
24+
*
25+
* @throws IllegalArgumentException If the given Java Duration is out of bounds of what can be expressed with the
26+
* Scala FiniteDuration.
27+
*/
28+
final def toScala(duration: java.time.Duration): scala.concurrent.duration.FiniteDuration = {
29+
val originalSeconds = duration.getSeconds
30+
val originalNanos = duration.getNano
31+
if (originalNanos == 0) {
32+
if (originalSeconds == 0) ScalaDuration.Zero
33+
else FiniteDuration(originalSeconds, TimeUnit.SECONDS)
34+
} else if (originalSeconds == 0) {
35+
FiniteDuration(originalNanos, TimeUnit.NANOSECONDS)
36+
} else {
37+
try {
38+
val secondsAsNanos = Math.multiplyExact(originalSeconds, 1000000000)
39+
val totalNanos = secondsAsNanos + originalNanos
40+
if ((totalNanos < 0 && secondsAsNanos < 0) || (totalNanos > 0 && secondsAsNanos > 0)) FiniteDuration(totalNanos, TimeUnit.NANOSECONDS)
41+
else throw new ArithmeticException()
42+
} catch {
43+
case _: ArithmeticException => throw new IllegalArgumentException(s"Java duration $duration cannot be expressed as a Scala duration")
44+
}
45+
}
46+
}
47+
48+
/**
49+
* Transform a Scala FiniteDuration into a Java duration. Note that the Scala duration keeps the time unit it was created
50+
* with while a Java duration always is a pair of seconds and nanos, so the unit it lost.
51+
*/
52+
final def toJava(duration: scala.concurrent.duration.FiniteDuration): java.time.Duration = {
53+
if (duration.length == 0) JavaDuration.ZERO
54+
else duration.unit match {
55+
case TimeUnit.NANOSECONDS => JavaDuration.ofNanos(duration.length)
56+
case TimeUnit.MICROSECONDS => JavaDuration.of(duration.length, ChronoUnit.MICROS)
57+
case TimeUnit.MILLISECONDS => JavaDuration.ofMillis(duration.length)
58+
case TimeUnit.SECONDS => JavaDuration.ofSeconds(duration.length)
59+
case TimeUnit.MINUTES => JavaDuration.ofMinutes(duration.length)
60+
case TimeUnit.HOURS => JavaDuration.ofHours(duration.length)
61+
case TimeUnit.DAYS => JavaDuration.ofDays(duration.length)
62+
}
63+
}
64+
65+
implicit final class DurationOps(val duration: java.time.Duration) extends AnyVal {
66+
/**
67+
* See [[DurationConverters.toScala]]
68+
*/
69+
def toScala: scala.concurrent.duration.FiniteDuration = DurationConverters.toScala(duration)
70+
}
71+
72+
implicit final class FiniteDurationops(val duration: scala.concurrent.duration.FiniteDuration) extends AnyVal {
73+
/**
74+
* See [[DurationConverters.toJava]]
75+
*/
76+
def toJava: java.time.Duration = DurationConverters.toJava(duration)
77+
}
78+
79+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright (C) 2012-2018 Lightbend Inc. <http://www.lightbend.com>
3+
*/
4+
package scala.compat.java8;
5+
6+
import org.junit.Test;
7+
import scala.concurrent.duration.FiniteDuration;
8+
import scala.runtime.java8.*;
9+
10+
import java.time.Duration;
11+
import java.time.temporal.ChronoUnit;
12+
import java.util.concurrent.TimeUnit;
13+
14+
public class DurationConvertersJavaTest {
15+
16+
@Test
17+
public void apiAccessibleFromJava() {
18+
DurationConverters.toScala(Duration.of(5, ChronoUnit.SECONDS));
19+
DurationConverters.toJava(FiniteDuration.create(5, TimeUnit.SECONDS));
20+
}
21+
22+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright (C) 2009-2017 Lightbend Inc. <http://www.lightbend.com>
3+
*/
4+
package scala.compat.java8
5+
6+
import java.time.{Duration => JavaDuration}
7+
8+
import org.junit.Assert._
9+
import org.junit.Test
10+
11+
import scala.util.Try
12+
13+
class DurationConvertersTest {
14+
15+
import DurationConverters._
16+
import scala.concurrent.duration._
17+
18+
@Test
19+
def scalaNanosToJavaDuration(): Unit = {
20+
Seq[(Long, (Long, Int))](
21+
(Long.MinValue + 1) -> (-9223372037L, 145224193), // because java duration nanos are offset from the "wrong" direction
22+
-1000000001L -> (-2, 999999999),
23+
-1L -> (-1, 999999999),
24+
0L -> (0, 0),
25+
1L -> (0, 1),
26+
1000000001L -> (1,1),
27+
Long.MaxValue -> (9223372036L, 854775807)
28+
).foreach { case (n, (expSecs, expNanos)) =>
29+
val result = n.nanos.toJava
30+
assertEquals(s"toJava($n nanos) -> $expSecs s)", expSecs, result.getSeconds)
31+
assertEquals(s"toJava($n nanos) -> $expNanos n)", expNanos, result.getNano)
32+
}
33+
}
34+
35+
@Test
36+
def scalaMilliSecondsToJavaDuration(): Unit = {
37+
Seq[(Long, (Long, Int))](
38+
-9223372036854L -> (-9223372037L, 146000000),
39+
-1L -> (-1L, 999000000),
40+
0L -> (0L, 0),
41+
1L -> (0L, 1000000),
42+
9223372036854L -> (9223372036L, 854000000)
43+
).foreach { case (n, (expSecs, expNanos)) =>
44+
val result = n.millis.toJava
45+
assertEquals(s"toJava($n millis) -> $expSecs s)", expSecs, result.getSeconds)
46+
assertEquals(s"toJava($n millis) -> $expNanos n)", expNanos, result.getNano)
47+
}
48+
}
49+
50+
@Test
51+
def scalaMicroSecondsToJavaDuration(): Unit = {
52+
Seq[(Long, (Long, Int))](
53+
-9223372036854775L -> (-9223372037L, 145225000),
54+
-1L -> (-1L, 999999000),
55+
0L -> (0L, 0),
56+
1L -> (0L, 1000),
57+
9223372036854775L -> (9223372036L, 854775000)
58+
).foreach { case (n, (expSecs, expNanos)) =>
59+
val result = n.micros.toJava
60+
assertEquals(s"toJava($n micros) -> $expSecs s)", expSecs, result.getSeconds)
61+
assertEquals(s"toJava($n micros) -> $expNanos n)", expNanos, result.getNano)
62+
}
63+
}
64+
65+
@Test
66+
def scalaSecondsToJavaDuration(): Unit = {
67+
Seq[(Long, (Long, Int))](
68+
-9223372036L -> (-9223372036L, 0),
69+
-1L -> (-1L, 0),
70+
0L -> (0L, 0),
71+
1L -> (1L, 0),
72+
9223372036L -> (9223372036L, 0)
73+
).foreach { case (n, (expSecs, expNanos)) =>
74+
val result = n.seconds.toJava
75+
assertEquals(expSecs, result.getSeconds)
76+
assertEquals(expNanos, result.getNano)
77+
}
78+
}
79+
80+
81+
@Test
82+
def javaSecondsToScalaDuration(): Unit = {
83+
Seq[Long](-9223372036L, -1L, 0L, 1L, 9223372036L).foreach { n =>
84+
assertEquals(n, toScala(JavaDuration.ofSeconds(n)).toSeconds)
85+
}
86+
}
87+
88+
89+
@Test
90+
def javaNanosPartToScalaDuration(): Unit = {
91+
val nanosPerSecond = 1000000000L
92+
Seq[Long](-nanosPerSecond - 1L, 0L, 1L, nanosPerSecond - 1L).foreach { n =>
93+
assertEquals(n, toScala(JavaDuration.ofNanos(n)).toNanos)
94+
}
95+
}
96+
97+
@Test
98+
def unsupportedJavaDurationThrows(): Unit = {
99+
Seq(JavaDuration.ofSeconds(-9223372037L), JavaDuration.ofSeconds(9223372037L)).foreach { d =>
100+
val res = Try { toScala(d) }
101+
assertTrue(s"Expected exception for $d but got success", res.isFailure)
102+
}
103+
}
104+
105+
106+
}

0 commit comments

Comments
 (0)