summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRoland <rk@rkuhn.info>2012-09-07 16:04:52 +0200
committerRoland <rk@rkuhn.info>2012-09-07 16:04:52 +0200
commit7a833f94296849c0e2a6cbacb7e513599c831f38 (patch)
treeca34eeab89215ce3b44db39583bd012baaa7a7ac
parent9da135880d0b78b440f4ce62dd8b7dc156571d71 (diff)
downloadscala-7a833f94296849c0e2a6cbacb7e513599c831f38.tar.gz
scala-7a833f94296849c0e2a6cbacb7e513599c831f38.tar.bz2
scala-7a833f94296849c0e2a6cbacb7e513599c831f38.zip
second round of Duration cleanup
- make Duration behave consistent with Double's non-finite semantics - add ScalaDoc - add complete test suite - change overflow protection impl after review comments - clean up code
-rw-r--r--src/library/scala/concurrent/util/Duration.scala522
-rw-r--r--test/files/jvm/duration-tck.scala236
2 files changed, 582 insertions, 176 deletions
diff --git a/src/library/scala/concurrent/util/Duration.scala b/src/library/scala/concurrent/util/Duration.scala
index c5b7e8328d..c0eccec4db 100644
--- a/src/library/scala/concurrent/util/Duration.scala
+++ b/src/library/scala/concurrent/util/Duration.scala
@@ -13,32 +13,108 @@ import TimeUnit._
import java.lang.{ Double => JDouble, Long => JLong }
import language.implicitConversions
+/**
+ * This class stores a deadline, as obtained via `Deadline.now` or the
+ * duration DSL:
+ *
+ * {{{
+ * import scala.concurrent.util.duration._
+ * 3.seconds.fromNow
+ * }}}
+ *
+ * Its main purpose is to manage repeated attempts to achieve something (like
+ * awaiting a condition) by offering the methods `hasTimeLeft` and `timeLeft`. All
+ * durations are measured according to `System.nanoTime` aka wall-time; this
+ * does not take into account changes to the system clock (such as leap
+ * seconds).
+ */
case class Deadline private (time: Duration) extends Ordered[Deadline] {
+ /**
+ * Return a deadline advanced (i.e. moved into the future) by the given duration.
+ */
def +(other: Duration): Deadline = copy(time = time + other)
+ /**
+ * Return a deadline moved backwards (i.e. towards the past) by the given duration.
+ */
def -(other: Duration): Deadline = copy(time = time - other)
+ /**
+ * Calculate time difference between this and the other deadline, where the result is directed (i.e. may be negative).
+ */
def -(other: Deadline): Duration = time - other.time
+ /**
+ * Calculate time difference between this duration and now; the result is negative if the deadline has passed.
+ *
+ * '''''Note that on some systems this operation is costly because it entails a system call.'''''
+ * Check `System.nanoTime` for your platform.
+ */
def timeLeft: Duration = this - Deadline.now
+ /**
+ * Determine whether the deadline still lies in the future at the point where this method is called.
+ *
+ * '''''Note that on some systems this operation is costly because it entails a system call.'''''
+ * Check `System.nanoTime` for your platform.
+ */
def hasTimeLeft(): Boolean = !isOverdue()
+ /**
+ * Determine whether the deadline lies in the past at the point where this method is called.
+ *
+ * '''''Note that on some systems this operation is costly because it entails a system call.'''''
+ * Check `System.nanoTime` for your platform.
+ */
def isOverdue(): Boolean = (time.toNanos - System.nanoTime()) < 0
+ /**
+ * The natural ordering for deadline is determined by the natural order of the underlying (finite) duration.
+ */
def compare(other: Deadline) = time compare other.time
}
object Deadline {
+ /**
+ * Construct a deadline due exactly at the point where this method is called. Useful for then
+ * advancing it to obtain a future deadline, or for sampling the current time exactly once and
+ * then comparing it to multiple deadlines (using subtraction).
+ */
def now: Deadline = Deadline(Duration(System.nanoTime, NANOSECONDS))
+ /**
+ * The natural ordering for deadline is determined by the natural order of the underlying (finite) duration.
+ */
implicit object DeadlineIsOrdered extends Ordering[Deadline] {
def compare(a: Deadline, b: Deadline) = a compare b
}
}
-// TODO: "Inf", "PlusInf", "MinusInf", where did these names come from?
-// TODO: Duration.create(n, DAYS) == Duration(Long.MaxValue, NANOSECONDS) forall (n: Double) >= 106752d
object Duration {
+ /**
+ * This implicit conversion allows the use of a Deadline in place of a Duration, which will
+ * insert the time left until the deadline in its place.
+ */
implicit def timeLeft(implicit d: Deadline): Duration = d.timeLeft
- def apply(length: Double, unit: TimeUnit): FiniteDuration = fromNanos(unit.toNanos(1) * length)
- def apply(length: Long, unit: TimeUnit): FiniteDuration = new FiniteDuration(length, unit)
- def apply(length: Long, unit: String): FiniteDuration = new FiniteDuration(length, Duration.timeUnit(unit))
+ /**
+ * Construct a Duration from the given length and unit. Observe that nanosecond precision may be lost if
+ *
+ * - the unit is NANOSECONDS
+ * - and the length has an absolute value greater than 2^53
+ *
+ * Will throw an exception if the length was finite but the resulting duration cannot be expressed
+ * as a [[FiniteDuration]]. Infinite inputs (and NaN) are converted into [[Duration.Inf]],
+ * [[Duration.MinusInf]] and [[Duration.Undefined]], respectively.
+ */
+ def apply(length: Double, unit: TimeUnit): Duration = fromNanos(unit.toNanos(1) * length)
+ /**
+ * Construct a finite duration from the given length and time unit. The unit given is retained
+ * throughout calculations as long as possible, so that it can be retrieved later.
+ */
+ def apply(length: Long, unit: TimeUnit): FiniteDuration = new FiniteDuration(length, unit)
+ /**
+ * Construct a finite duration from the given length and time unit, where the latter is
+ * looked up in a list of string representation. Valid choices are:
+ *
+ * `d, day, h, hour, min, minute, s, sec, second, ms, milli, millisecond, µs, micro, microsecond, ns, nano, nanosecond`
+ * and their pluralized forms (for every but the first mentioned form of each unit, i.e. no "ds", but "days").
+ */
+ def apply(length: Long, unit: String): FiniteDuration = new FiniteDuration(length, Duration.timeUnit(unit))
/**
* Parse String into Duration. Format is `"<length><unit>"`, where
@@ -88,131 +164,217 @@ object Duration {
protected[util] val timeUnit: Map[String, TimeUnit] =
timeUnitLabels flatMap { case (unit, names) => expandLabels(names) map (_ -> unit) } toMap
+ /**
+ * Extract length and time unit out of a string, where the format must match the description for [[Duration$.apply(String):Duration apply(String)]].
+ * The extractor will not match for malformed strings or non-finite durations.
+ */
def unapply(s: String): Option[(Long, TimeUnit)] =
( try Some(apply(s)) catch { case _: RuntimeException => None } ) flatMap unapply
+ /**
+ * Extract length and time unit out of a duration, if it is finite.
+ */
def unapply(d: Duration): Option[(Long, TimeUnit)] =
if (d.isFinite) Some((d.length, d.unit)) else None
- def fromNanos(nanos: Double): FiniteDuration = {
- if (nanos > Long.MaxValue || nanos < Long.MinValue)
+ /**
+ * Construct a possibly infinite or undefined Duration from the given number of nanoseconds.
+ *
+ * - `Double.PositiveInfinity` is mapped to [[Duration.Inf]]
+ * - `Double.NegativeInfinity` is mapped to [[Duration.MinusInf]]
+ * - `Double.NaN` is mapped to [[Duration.Undefined]]
+ * - `-0d` is mapped to [[Duration.Zero]] (exactly like `0d`)
+ *
+ * The semantics of the resulting Duration objects matches the semantics of their Double
+ * counterparts with respect to arithmetic operations.
+ */
+ def fromNanos(nanos: Double): Duration = {
+ if (nanos.isInfinite)
+ if (nanos > 0) Inf else MinusInf
+ else if (nanos.isNaN)
+ Undefined
+ else if (nanos > Long.MaxValue || nanos < Long.MinValue)
throw new IllegalArgumentException("trying to construct too large duration with " + nanos + "ns")
- fromNanos((nanos + 0.5).toLong)
+ else
+ fromNanos((nanos + 0.5).toLong)
}
+ private val µs_per_ns = 1000L
+ private val ms_per_ns = µs_per_ns * 1000
+ private val s_per_ns = ms_per_ns * 1000
+ private val min_per_ns = s_per_ns * 60
+ private val h_per_ns = min_per_ns * 60
+ private val d_per_ns = h_per_ns * 24
+
+ /**
+ * Construct a finite duration from the given number of nanoseconds. This will throw an
+ * exception for `Long.MinValue` since that would lead to inconsistent behavior afterwards
+ * (cannot be negated). The result will have the coarsest possible time unit which can
+ * exactly express this duration.
+ */
def fromNanos(nanos: Long): FiniteDuration = {
- if (nanos % 86400000000000L == 0) {
- Duration(nanos / 86400000000000L, DAYS)
- } else if (nanos % 3600000000000L == 0) {
- Duration(nanos / 3600000000000L, HOURS)
- } else if (nanos % 60000000000L == 0) {
- Duration(nanos / 60000000000L, MINUTES)
- } else if (nanos % 1000000000L == 0) {
- Duration(nanos / 1000000000L, SECONDS)
- } else if (nanos % 1000000L == 0) {
- Duration(nanos / 1000000L, MILLISECONDS)
- } else if (nanos % 1000L == 0) {
- Duration(nanos / 1000L, MICROSECONDS)
- } else {
- Duration(nanos, NANOSECONDS)
- }
+ if (nanos % d_per_ns == 0) Duration(nanos / d_per_ns, DAYS)
+ else if (nanos % h_per_ns == 0) Duration(nanos / h_per_ns, HOURS)
+ else if (nanos % min_per_ns == 0) Duration(nanos / min_per_ns, MINUTES)
+ else if (nanos % s_per_ns == 0) Duration(nanos / s_per_ns, SECONDS)
+ else if (nanos % ms_per_ns == 0) Duration(nanos / ms_per_ns, MILLISECONDS)
+ else if (nanos % µs_per_ns == 0) Duration(nanos / µs_per_ns, MICROSECONDS)
+ else Duration(nanos, NANOSECONDS)
}
/**
- * Parse TimeUnit from string representation.
+ * Preconstructed value of `0.days`.
*/
+ // unit as coarse as possible to keep (_ + Zero) sane unit-wise
+ val Zero: FiniteDuration = new FiniteDuration(0, DAYS)
- val Zero: FiniteDuration = new FiniteDuration(0, NANOSECONDS)
-
- object Undefined extends Infinite {
- private def fail(what: String) = throw new IllegalArgumentException(s"cannot $what Undefined duration")
-
+ /**
+ * The Undefined value corresponds closely to Double.NaN:
+ *
+ * - it is the result of otherwise invalid operations
+ * - it does not equal itself (according to `equals()`)
+ * - it compares greater than any other Duration apart from itself (for which `compare` returns 0)
+ *
+ * The particular comparison semantics mirror those of Double.NaN.
+ *
+ * '''''Use `eq` when checking an input of a method against this value.'''''
+ */
+ val Undefined: Infinite = new Infinite {
override def toString = "Duration.Undefined"
- override def equals(other: Any) = this eq other.asInstanceOf[AnyRef]
- override def +(other: Duration): Duration = fail("add")
- override def -(other: Duration): Duration = fail("subtract")
- override def *(factor: Double): Duration = fail("multiply")
- override def /(factor: Double): Duration = fail("divide")
- override def /(other: Duration): Double = fail("divide")
- def compare(other: Duration) = fail("compare")
- def unary_- : Duration = fail("negate")
+ override def equals(other: Any) = false
+ override def +(other: Duration): Duration = this
+ override def -(other: Duration): Duration = this
+ override def *(factor: Double): Duration = this
+ override def /(factor: Double): Duration = this
+ override def /(other: Duration): Double = Double.NaN
+ def compare(other: Duration) = if (other eq this) 0 else 1
+ def unary_- : Duration = this
+ def toUnit(unit: TimeUnit): Double = Double.NaN
}
sealed abstract class Infinite extends Duration {
def +(other: Duration): Duration = other match {
- case x: Infinite if x ne this => throw new IllegalArgumentException("illegal addition of infinities")
+ case x if x eq Undefined => Undefined
+ case x: Infinite if x ne this => Undefined
case _ => this
}
- // Is this really intended to throw if the argument is "this" but otherwise return this?
- def -(other: Duration): Duration =
- if (other ne this) this
- else throw new IllegalArgumentException("illegal subtraction of infinities")
-
- private def mult(f: Double) =
- if (f == 0d) fail("multiply by zero")
- else if (f < 0d) -this
+ def -(other: Duration): Duration = other match {
+ case x if x eq Undefined => Undefined
+ case x: Infinite if x eq this => Undefined
+ case _ => this
+ }
+
+ def *(factor: Double): Duration =
+ if (factor == 0d || factor.isNaN) Undefined
+ else if (factor < 0d) -this
+ else this
+ def /(factor: Double): Duration =
+ if (factor == 0d || factor.isNaN || factor.isInfinite) Undefined
+ else if (factor < 0d) -this
else this
- def *(factor: Double): Duration = mult(factor)
- def /(factor: Double): Duration = mult(factor)
def /(other: Duration): Double = other match {
- case _: Infinite => throw new IllegalArgumentException("illegal division of infinities")
- // maybe questionable but pragmatic: Inf / 0 => Inf
+ case _: Infinite => Double.NaN
case x => Double.PositiveInfinity * (if ((this > Zero) ^ (other >= Zero)) -1 else 1)
}
final def isFinite() = false
private def fail(what: String) = throw new IllegalArgumentException(s"$what not allowed on infinite Durations")
- def length: Long = fail("length")
- def toNanos: Long = fail("toNanos")
- def toMicros: Long = fail("toMicros")
- def toMillis: Long = fail("toMillis")
- def toSeconds: Long = fail("toSeconds")
- def toMinutes: Long = fail("toMinutes")
- def toHours: Long = fail("toHours")
- def toDays: Long = fail("toDays")
-
- def unit: TimeUnit = fail("unit")
- def toUnit(unit: TimeUnit): Double = fail("toUnit")
+ final def length: Long = fail("length")
+ final def unit: TimeUnit = fail("unit")
+ final def toNanos: Long = fail("toNanos")
+ final def toMicros: Long = fail("toMicros")
+ final def toMillis: Long = fail("toMillis")
+ final def toSeconds: Long = fail("toSeconds")
+ final def toMinutes: Long = fail("toMinutes")
+ final def toHours: Long = fail("toHours")
+ final def toDays: Long = fail("toDays")
}
/**
- * Infinite duration: greater than any other and not equal to any other,
- * including itself.
+ * Infinite duration: greater than any other (apart from Undefined) and not equal to any other
+ * but itself. This value closely corresponds to Double.PositiveInfinity,
+ * matching its semantics in arithmetic operations.
*/
val Inf: Infinite = new Infinite {
override def toString = "Duration.Inf"
- def compare(other: Duration) = if (other eq this) 0 else 1
+ def compare(other: Duration) = other match {
+ case x if x eq Undefined => -1
+ case x if x eq this => 0
+ case _ => 1
+ }
def unary_- : Duration = MinusInf
+ def toUnit(unit: TimeUnit): Double = Double.PositiveInfinity
}
/**
- * Infinite negative duration: lesser than any other and not equal to any other,
- * including itself.
+ * Infinite duration: less than any other and not equal to any other
+ * but itself. This value closely corresponds to Double.NegativeInfinity,
+ * matching its semantics in arithmetic operations.
*/
val MinusInf: Infinite = new Infinite {
override def toString = "Duration.MinusInf"
- def compare(other: Duration) = if (other eq this) 0 else -1
+ def compare(other: Duration) = other match {
+ case x if x eq this => 0
+ case _ => -1
+ }
def unary_- : Duration = Inf
+ def toUnit(unit: TimeUnit): Double = Double.NegativeInfinity
}
// Java Factories
- def create(length: Long, unit: TimeUnit): FiniteDuration = apply(length, unit)
- def create(length: Double, unit: TimeUnit): FiniteDuration = apply(length, unit)
- def create(length: Long, unit: String): FiniteDuration = apply(length, unit)
- def create(s: String): Duration = apply(s)
+ /**
+ * Construct a finite duration from the given length and time unit. The unit given is retained
+ * throughout calculations as long as possible, so that it can be retrieved later.
+ */
+ def create(length: Long, unit: TimeUnit): FiniteDuration = apply(length, unit)
+ /**
+ * Construct a Duration from the given length and unit. Observe that nanosecond precision may be lost if
+ *
+ * - the unit is NANOSECONDS
+ * - and the length has an absolute value greater than 2^53
+ *
+ * Will throw an exception if the length was finite but the resulting duration cannot be expressed
+ * as a [[FiniteDuration]]. Infinite inputs (and NaN) are converted into [[Duration.Inf]],
+ * [[Duration.MinusInf]] and [[Duration.Undefined]], respectively.
+ */
+ def create(length: Double, unit: TimeUnit): Duration = apply(length, unit)
+ /**
+ * Construct a finite duration from the given length and time unit, where the latter is
+ * looked up in a list of string representation. Valid choices are:
+ *
+ * `d, day, h, hour, min, minute, s, sec, second, ms, milli, millisecond, µs, micro, microsecond, ns, nano, nanosecond`
+ * and their pluralized forms (for every but the first mentioned form of each unit, i.e. no "ds", but "days").
+ */
+ def create(length: Long, unit: String): FiniteDuration = apply(length, unit)
+ /**
+ * Parse String into Duration. Format is `"<length><unit>"`, where
+ * whitespace is allowed before, between and after the parts. Infinities are
+ * designated by `"Inf"`, `"PlusInf"`, `"+Inf"` and `"-Inf"` or `"MinusInf"`.
+ * Throws exception if format is not parseable.
+ */
+ def create(s: String): Duration = apply(s)
+
+ /**
+ * The natural ordering of durations matches the natural ordering for Double, including non-finite values.
+ */
implicit object DurationIsOrdered extends Ordering[Duration] {
def compare(a: Duration, b: Duration) = a compare b
}
}
/**
- * Utility for working with java.util.concurrent.TimeUnit durations.
+ * <h2>Utility for working with java.util.concurrent.TimeUnit durations.</h2>
+ *
+ * '''''This class is not meant as a general purpose representation of time, it is
+ * optimized for the needs of `scala.concurrent`.'''''
+ *
+ * <h2>Basic Usage</h2>
*
* <p/>
* Examples:
- * <pre>
+ * {{{
* import scala.concurrent.util.Duration
* import java.util.concurrent.TimeUnit
*
@@ -222,57 +384,196 @@ object Duration {
* duration.toNanos
* duration < 1.second
* duration <= Duration.Inf
- * </pre>
+ * }}}
+ *
+ * '''''Invoking inexpressible conversions (like calling `toSeconds` on an infinite duration) will throw an IllegalArgumentException.'''''
*
* <p/>
* Implicits are also provided for Int, Long and Double. Example usage:
- * <pre>
+ * {{{
* import scala.concurrent.util.Duration._
*
* val duration = 100 millis
- * </pre>
+ * }}}
+ *
+ * '''''The DSL provided by the implicit conversions always allows construction of finite durations, even for infinite Double inputs; use Duration.Inf instead.'''''
*
* Extractors, parsing and arithmetic are also included:
- * <pre>
+ * {{{
* val d = Duration("1.2 µs")
* val Duration(length, unit) = 5 millis
* val d2 = d * 2.5
* val d3 = d2 + 1.millisecond
- * </pre>
+ * }}}
+ *
+ * <h2>Handling of Time Units</h2>
+ *
+ * Calculations performed on finite durations always retain the more precise unit of either operand, no matter
+ * whether a coarser unit would be able to exactly express the same duration. This means that Duration can be
+ * used as a lossless container for a (length, unit) pair if it is constructed using the corresponding methods
+ * and no arithmetic is performed on it; adding/subtracting durations should in that case be done with care.
+ *
+ * <h2>Correspondence to Double Semantics</h2>
+ *
+ * The semantics of arithmetic operations on Duration are two-fold:
+ *
+ * - exact addition/subtraction with nanosecond resolution for finite durations, independent of the summands' magnitude
+ * - isomorphic to `java.lang.Double` when it comes to infinite or undefined values
+ *
+ * The conversion between Duration and Double is done using [[Duration.toUnit]] (with unit NANOSECONDS)
+ * and [[Duration$.fromNanos(Double):Duration Duration.fromNanos(Double)]].
+ *
+ * <h2>Ordering</h2>
+ *
+ * The default ordering is consistent with the ordering of Double numbers, which means that Undefined is
+ * considered greater than all other durations, including [[Duration.Inf]].
+ *
+ * @define exc Invoking this method on a non-finite duration will result in an IllegalArgumentException.
*/
-abstract class Duration extends Serializable with Ordered[Duration] {
+sealed abstract class Duration extends Serializable with Ordered[Duration] {
+ /**
+ * Obtain the length of this Duration measured in the unit obtained by the `unit` method. $exc
+ */
def length: Long
+ /**
+ * Obtain the time unit in which the length of this duration is measured. $exc
+ */
def unit: TimeUnit
+ /**
+ * Return the length of this duration measured in whole nanoseconds, rounding towards zero. $exc
+ */
def toNanos: Long
+ /**
+ * Return the length of this duration measured in whole microseconds, rounding towards zero. $exc
+ */
def toMicros: Long
+ /**
+ * Return the length of this duration measured in whole milliseconds, rounding towards zero. $exc
+ */
def toMillis: Long
+ /**
+ * Return the length of this duration measured in whole seconds, rounding towards zero. $exc
+ */
def toSeconds: Long
+ /**
+ * Return the length of this duration measured in whole minutes, rounding towards zero. $exc
+ */
def toMinutes: Long
+ /**
+ * Return the length of this duration measured in whole hours, rounding towards zero. $exc
+ */
def toHours: Long
+ /**
+ * Return the length of this duration measured in whole days, rounding towards zero. $exc
+ */
def toDays: Long
+ /**
+ * Return the number of nanoseconds as floating point number, scaled down to the given unit.
+ * The result may not precisely represent this duration due to the Double datatype's inherent
+ * limitations (mantissa size effectively 53 bits). Non-finite durations are represented as
+ * - [[Duration.Undefined]] is mapped to Double.NaN
+ * - [[Duration.Inf]] is mapped to Double.PositiveInfinity
+ * - [[Duration.MinusInf]] is mapped to Double.NegativeInfinity
+ */
def toUnit(unit: TimeUnit): Double
+ /**
+ * Return the sum of that duration and this. In case of an overflow, an IllegalArgumentException
+ * is thrown: the range of a finite duration is +-(2^63-1)ns, and no conversion to infinite
+ * durations takes place. When involving non-finite summands the semantics match those
+ * of Double.
+ */
def +(other: Duration): Duration
+ /**
+ * Return the difference of that duration and this. In case of an overflow, an IllegalArgumentException
+ * is thrown: the range of a finite duration is +-(2^63-1)ns, and no conversion to infinite
+ * durations takes place. When involving non-finite summands the semantics match those
+ * of Double.
+ */
def -(other: Duration): Duration
+ /**
+ * Return this duration multiplied by the scalar factor. In case of an overflow, an IllegalArgumentException
+ * is thrown: the range of a finite duration is +-(2^63-1)ns, and no conversion to infinite
+ * durations takes place. When involving non-finite factors the semantics match those
+ * of Double.
+ */
def *(factor: Double): Duration
+ /**
+ * Return this duration divided by the scalar factor. In case of an overflow, an IllegalArgumentException
+ * is thrown: the range of a finite duration is +-(2^63-1)ns, and no conversion to infinite
+ * durations takes place. When involving non-finite factors the semantics match those
+ * of Double.
+ */
def /(factor: Double): Duration
+ /**
+ * Return the quotient of this and that duration as floating-point number. The semantics are
+ * determined by Double as if calculating the quotient of the nanosecond lengths of both factors.
+ */
def /(other: Duration): Double
+ /**
+ * Negate this duration. The only two values which are mapped to themselves are [[Duration.Zero]] and [[Duration.Undefined]].
+ */
def unary_- : Duration
+ /**
+ * This method returns whether this duration is finite, which is not the same as
+ * `!isInfinite` for Double because this method also returns `false` for [[Duration.Undefined]].
+ */
def isFinite(): Boolean
+ /**
+ * Return the smaller of this and that duration as determined by the natural ordering.
+ */
def min(other: Duration): Duration = if (this < other) this else other
+ /**
+ * Return the larger of this and that duration as determined by the natural ordering.
+ */
def max(other: Duration): Duration = if (this > other) this else other
+ /**
+ * Construct a [[Deadline]] from this duration by adding it to the current instant `Duration.now`.
+ */
def fromNow: Deadline = Deadline.now + this
// Java API
+
+ /**
+ * Return this duration divided by the scalar factor. In case of an overflow, an IllegalArgumentException
+ * is thrown: the range of a finite duration is +-(2^63-1)ns, and no conversion to infinite
+ * durations takes place. When involving non-finite factors the semantics match those
+ * of Double.
+ */
def div(factor: Double) = this / factor
+ /**
+ * Return the quotient of this and that duration as floating-point number. The semantics are
+ * determined by Double as if calculating the quotient of the nanosecond lengths of both factors.
+ */
def div(other: Duration) = this / other
def gt(other: Duration) = this > other
def gteq(other: Duration) = this >= other
def lt(other: Duration) = this < other
def lteq(other: Duration) = this <= other
+ /**
+ * Return the difference of that duration and this. In case of an overflow, an IllegalArgumentException
+ * is thrown: the range of a finite duration is +-(2^63-1)ns, and no conversion to infinite
+ * durations takes place. When involving non-finite summands the semantics match those
+ * of Double.
+ */
def minus(other: Duration) = this - other
+ /**
+ * Return this duration multiplied by the scalar factor. In case of an overflow, an IllegalArgumentException
+ * is thrown: the range of a finite duration is +-(2^63-1)ns, and no conversion to infinite
+ * durations takes place. When involving non-finite factors the semantics match those
+ * of Double.
+ */
def mul(factor: Double) = this * factor
+ /**
+ * Negate this duration. The only two values which are mapped to themselves are [[Duration.Zero]] and [[Duration.Undefined]].
+ */
def neg() = -this
+ /**
+ * Return the sum of that duration and this. In case of an overflow, an IllegalArgumentException
+ * is thrown: the range of a finite duration is +-(2^63-1)ns, and no conversion to infinite
+ * durations takes place. When involving non-finite summands the semantics match those
+ * of Double.
+ */
def plus(other: Duration) = this + other
}
@@ -285,6 +586,10 @@ object FiniteDuration {
def apply(length: Long, unit: String) = new FiniteDuration(length, Duration.timeUnit(unit))
}
+/**
+ * This class represents a finite duration. Its addition and subtraction operators are overloaded to retain
+ * this guarantee statically. The range of this class is limited to +-(2^63-1)ns, which is roughly 292 years.
+ */
class FiniteDuration(val length: Long, val unit: TimeUnit) extends Duration {
import Duration._
@@ -293,7 +598,7 @@ class FiniteDuration(val length: Long, val unit: TimeUnit) extends Duration {
* sorted so that the first cases should be most-used ones, because enum
* is checked one after the other.
*/
- case NANOSECONDS ⇒ true
+ case NANOSECONDS ⇒ length != Long.MinValue
case MICROSECONDS ⇒ length <= 9223372036854775L && length >= -9223372036854775L
case MILLISECONDS ⇒ length <= 9223372036854L && length >= -9223372036854L
case SECONDS ⇒ length <= 9223372036L && length >= -9223372036L
@@ -301,9 +606,9 @@ class FiniteDuration(val length: Long, val unit: TimeUnit) extends Duration {
case HOURS ⇒ length <= 2562047L && length >= -2562047L
case DAYS ⇒ length <= 106751L && length >= -106751L
case _ ⇒
- val v = unit.convert(length, DAYS)
+ val v = DAYS.convert(length, unit)
v <= 106751L && v >= -106751L
- }, "Duration is limited to 2^63ns (ca. 292 years)")
+ }, "Duration is limited to +-(2^63-1)ns (ca. 292 years)")
def toNanos = unit.toNanos(length)
def toMicros = unit.toMicros(length)
@@ -322,30 +627,49 @@ class FiniteDuration(val length: Long, val unit: TimeUnit) extends Duration {
case _ => -(other compare this)
}
- private def add(a: Long, b: Long): Long = {
- val c = a + b
- // check if the signs of the top bit of both summands differ from the sum
- if (((a ^ c) & (b ^ c)) < 0) throw new IllegalArgumentException("integer overflow")
- else c
+ // see https://www.securecoding.cert.org/confluence/display/java/NUM00-J.+Detect+or+prevent+integer+overflow
+ private def safeAdd(a: Long, b: Long): Long = {
+ if ((b > 0) && (a > Long.MaxValue - b) ||
+ (b < 0) && (a < Long.MinValue - b)) throw new IllegalArgumentException("integer overflow")
+ a + b
+ }
+ private def add(otherLength: Long, otherUnit: TimeUnit): FiniteDuration = {
+ val commonUnit = if (otherUnit.convert(1, unit) == 0) unit else otherUnit
+ val totalLength = safeAdd(commonUnit.convert(length, unit), commonUnit.convert(otherLength, otherUnit))
+ new FiniteDuration(totalLength, commonUnit)
}
+
def +(other: Duration) = other match {
- case x: FiniteDuration => fromNanos(add(toNanos, x.toNanos))
+ case x: FiniteDuration => add(x.length, x.unit)
case _ => other
}
def -(other: Duration) = other match {
- case x: FiniteDuration => fromNanos(add(toNanos, -x.toNanos))
+ case x: FiniteDuration => add(-x.length, x.unit)
case _ => other
}
- def *(factor: Double) = fromNanos(toNanos.toDouble * factor)
-
- def /(factor: Double) = fromNanos(toNanos.toDouble / factor)
-
- def /(other: Duration) = if (other.isFinite) toNanos.toDouble / other.toNanos else 0
+ def *(factor: Double) =
+ if (!factor.isInfinite) fromNanos(toNanos * factor)
+ else if (factor.isNaN) Undefined
+ else if ((factor > 0) ^ (this < Zero)) Inf
+ else MinusInf
+
+ def /(factor: Double) =
+ if (!factor.isInfinite) fromNanos(toNanos / factor)
+ else if (factor.isNaN) Undefined
+ else Zero
+
+ // if this is made a constant, then scalac will elide the conditional and always return +0.0, SI-6331
+ private def minusZero = -0d
+ def /(other: Duration): Double =
+ if (other.isFinite) toNanos.toDouble / other.toNanos
+ else if (other eq Undefined) Double.NaN
+ else if ((length < 0) ^ (other > Zero)) 0d
+ else minusZero
// overridden methods taking FiniteDurations, so that you can calculate while statically staying finite
- def +(other: FiniteDuration) = fromNanos(add(toNanos, other.toNanos))
- def -(other: FiniteDuration) = fromNanos(add(toNanos, -other.toNanos))
+ def +(other: FiniteDuration) = add(other.length, other.unit)
+ def -(other: FiniteDuration) = add(-other.length, other.unit)
def plus(other: FiniteDuration) = this + other
def minus(other: FiniteDuration) = this - other
override def div(factor: Double) = this / factor
@@ -432,5 +756,9 @@ final class DurationLong(val n: Long) extends AnyVal with DurationConversions {
}
final class DurationDouble(val d: Double) extends AnyVal with DurationConversions {
- override protected def durationIn(unit: TimeUnit): FiniteDuration = Duration(d, unit)
+ override protected def durationIn(unit: TimeUnit): FiniteDuration =
+ Duration(d, unit) match {
+ case f: FiniteDuration => f
+ case _ => throw new IllegalArgumentException("Duration DSL not applicable to " + d)
+ }
}
diff --git a/test/files/jvm/duration-tck.scala b/test/files/jvm/duration-tck.scala
index 679712aa59..aa1ac50078 100644
--- a/test/files/jvm/duration-tck.scala
+++ b/test/files/jvm/duration-tck.scala
@@ -10,7 +10,13 @@ import java.util.concurrent.TimeUnit._
object Test extends App {
implicit class Assert(val left: Any) extends AnyVal {
- def =!=(right: Any) = assert(left == right, s"$left was not equal to $right")
+ import Duration.Undefined
+ def mustBe(right: Any) = right match {
+ case r: Double if r.isNaN => assert(left.asInstanceOf[Double].isNaN, s"$left was not NaN")
+ case r: Double if r == 0 && r.compareTo(0) == -1 => assert(left == 0 && left.asInstanceOf[Double].compareTo(0) == -1, s"$left was not -0.0")
+ case Undefined => assert(left.asInstanceOf[AnyRef] eq Undefined, s"$left was not Undefined")
+ case _ => assert(left == right, s"$left was not equal to $right")
+ }
}
def intercept[T <: Exception : ClassTag](code: => Unit) =
@@ -19,89 +25,161 @@ object Test extends App {
case ex: Exception => if (classTag[T].runtimeClass isAssignableFrom ex.getClass) () else throw ex
}
- { // test field ops
- val zero = 0 seconds
- val one = 1 second
- val two = one + one
- val three = 3 * one
- (0 * one) =!= (zero)
- (2 * one) =!= (two)
- (three - two) =!= (one)
- (three / 3) =!= (one)
- (two / one) =!= (2)
- (one + zero) =!= (one)
- (one / 1000000) =!= (1.micro)
- }
+ val zero = 0 seconds
+ val one = 1 second
+ val two = one + one
+ val three = 3 * one
+ val inf = Duration.Inf
+ val minf = Duration.MinusInf
+ val undef = Duration.Undefined
+ val inputs = List(zero, one, inf, minf, undef)
+ val nan = Double.NaN
- { // test infinities
- val one = 1.second
- val inf = Duration.Inf
- val minf = Duration.MinusInf
- (-inf) =!= (minf)
- intercept[IllegalArgumentException] { minf + inf }
- intercept[IllegalArgumentException] { inf - inf }
- intercept[IllegalArgumentException] { inf + minf }
- intercept[IllegalArgumentException] { minf - minf }
- (inf + inf) =!= (inf)
- (inf - minf) =!= (inf)
- (minf - inf) =!= (minf)
- (minf + minf) =!= (minf)
- assert(inf == inf)
- assert(minf == minf)
- inf.compareTo(inf) =!= (0)
- inf.compareTo(one) =!= (1)
- minf.compareTo(minf) =!= (0)
- minf.compareTo(one) =!= (-1)
- assert(inf != minf)
- assert(minf != inf)
- assert(one != inf)
- assert(minf != one)
- inf =!= (minf * -1d)
- inf =!= (minf / -1d)
- }
+ // test field ops
+ one.isFinite mustBe true
+ 0 * one mustBe zero
+ 2 * one mustBe two
+ three - two mustBe one
+ three / 3 mustBe one
+ two / one mustBe 2
+ one + zero mustBe one
+ one / 1000000 mustBe 1.micro
+
+
+ // test infinities
+
+ inf.isFinite mustBe false
+ minf.isFinite mustBe false
+
+ inf mustBe inf
+ minf mustBe minf
+ -inf mustBe minf
+ -minf mustBe inf
+
+ minf + inf mustBe undef
+ inf - inf mustBe undef
+ inf + minf mustBe undef
+ minf - minf mustBe undef
+
+ inf + inf mustBe inf
+ inf - minf mustBe inf
+ minf - inf mustBe minf
+ minf + minf mustBe minf
+
+ inf.compareTo(inf) mustBe 0
+ inf.compareTo(one) mustBe 1
+ inf.compareTo(minf) mustBe 1
+ minf.compareTo(minf) mustBe 0
+ minf.compareTo(one) mustBe -1
+ minf.compareTo(inf) mustBe -1
+
+ assert(inf != minf)
+ assert(minf != inf)
+ assert(one != inf)
+ assert(minf != one)
+
+ inf mustBe (minf * -1d)
+ inf mustBe (minf / -1d)
+
+ one / inf mustBe 0d
+ -one / inf mustBe -0d
+ one / minf mustBe -0d
+ -one / minf mustBe 0d
+
+ inputs filterNot (_.isFinite) foreach (x => x / zero mustBe x.toUnit(DAYS))
+ inputs filterNot (_.isFinite) foreach (_ * 0d mustBe undef)
+ inputs filterNot (_.isFinite) foreach (x => x * Double.PositiveInfinity mustBe x)
+ inputs filterNot (_.isFinite) foreach (x => x * Double.NegativeInfinity mustBe -x)
+
+ inf.toUnit(SECONDS) mustBe Double.PositiveInfinity
+ minf.toUnit(MINUTES) mustBe Double.NegativeInfinity
+ Duration.fromNanos(Double.PositiveInfinity) mustBe inf
+ Duration.fromNanos(Double.NegativeInfinity) mustBe minf
+
- { // test overflow protection
- for (unit ← Seq(DAYS, HOURS, MINUTES, SECONDS, MILLISECONDS, MICROSECONDS, NANOSECONDS)) {
- val x = unit.convert(Long.MaxValue, NANOSECONDS)
- val dur = Duration(x, unit)
- val mdur = Duration(-x, unit)
- -mdur =!= (dur)
- intercept[IllegalArgumentException] { Duration(x + 10000000d, unit) }
- intercept[IllegalArgumentException] { Duration(-x - 10000000d, unit) }
- if (unit != NANOSECONDS) {
- intercept[IllegalArgumentException] { Duration(x + 1, unit) }
- intercept[IllegalArgumentException] { Duration(-x - 1, unit) }
- }
- intercept[IllegalArgumentException] { dur + 1.day }
- intercept[IllegalArgumentException] { mdur - 1.day }
- intercept[IllegalArgumentException] { dur * 1.1 }
- intercept[IllegalArgumentException] { mdur * 1.1 }
- intercept[IllegalArgumentException] { dur * 2.1 }
- intercept[IllegalArgumentException] { mdur * 2.1 }
- intercept[IllegalArgumentException] { dur / 0.9 }
- intercept[IllegalArgumentException] { mdur / 0.9 }
- intercept[IllegalArgumentException] { dur / 0.4 }
- intercept[IllegalArgumentException] { mdur / 0.4 }
- Duration(x + unit.toString.toLowerCase)
- Duration("-" + x + unit.toString.toLowerCase)
- intercept[IllegalArgumentException] { Duration("%.0f".format(x + 10000000d) + unit.toString.toLowerCase) }
- intercept[IllegalArgumentException] { Duration("-%.0f".format(x + 10000000d) + unit.toString.toLowerCase) }
+ // test undefined & NaN
+
+ undef.isFinite mustBe false
+ -undef mustBe undef
+ assert(undef != undef)
+ assert(undef eq undef)
+
+ inputs foreach (_ + undef mustBe undef)
+ inputs foreach (_ - undef mustBe undef)
+ inputs foreach (_ / undef mustBe nan)
+ inputs foreach (_ / nan mustBe undef)
+ inputs foreach (_ * nan mustBe undef)
+ inputs foreach (undef + _ mustBe undef)
+ inputs foreach (undef - _ mustBe undef)
+ inputs foreach (undef / _ mustBe nan)
+ inputs filter (_.isFinite) foreach (x => x / zero mustBe x.toUnit(SECONDS) / 0d)
+
+ inputs filterNot (_ eq undef) foreach (_ compareTo undef mustBe -1)
+ inputs filterNot (_ eq undef) foreach (undef compareTo _ mustBe 1)
+ undef compare undef mustBe 0
+
+ undef.toUnit(DAYS) mustBe nan
+ Duration.fromNanos(nan) mustBe undef
+
+
+ // test overflow protection
+ for (unit ← Seq(DAYS, HOURS, MINUTES, SECONDS, MILLISECONDS, MICROSECONDS, NANOSECONDS)) {
+ val x = unit.convert(Long.MaxValue, NANOSECONDS)
+ val dur = Duration(x, unit)
+ val mdur = Duration(-x, unit)
+ -mdur mustBe (dur)
+ intercept[IllegalArgumentException] { Duration(x + 10000000d, unit) }
+ intercept[IllegalArgumentException] { Duration(-x - 10000000d, unit) }
+ if (unit != NANOSECONDS) {
+ intercept[IllegalArgumentException] { Duration(x + 1, unit) }
+ intercept[IllegalArgumentException] { Duration(-x - 1, unit) }
}
+ intercept[IllegalArgumentException] { dur + 1.day }
+ intercept[IllegalArgumentException] { mdur - 1.day }
+ intercept[IllegalArgumentException] { dur * 1.1 }
+ intercept[IllegalArgumentException] { mdur * 1.1 }
+ intercept[IllegalArgumentException] { dur * 2.1 }
+ intercept[IllegalArgumentException] { mdur * 2.1 }
+ intercept[IllegalArgumentException] { dur / 0.9 }
+ intercept[IllegalArgumentException] { mdur / 0.9 }
+ intercept[IllegalArgumentException] { dur / 0.4 }
+ intercept[IllegalArgumentException] { mdur / 0.4 }
+ Duration(x + unit.toString.toLowerCase)
+ Duration("-" + x + unit.toString.toLowerCase)
+ intercept[IllegalArgumentException] { Duration("%.0f".format(x + 10000000d) + unit.toString.toLowerCase) }
+ intercept[IllegalArgumentException] { Duration("-%.0f".format(x + 10000000d) + unit.toString.toLowerCase) }
}
+ intercept[IllegalArgumentException] { Duration.fromNanos(1e20) }
+ intercept[IllegalArgumentException] { Duration.fromNanos(-1e20) }
+
- { // test Deadline
- val dead = 2.seconds.fromNow
- val dead2 = 2 seconds fromNow
- // view bounds vs. very local type inference vs. operator precedence: sigh
- assert(dead.timeLeft > (1 second: Duration))
- assert(dead2.timeLeft > (1 second: Duration))
- Thread.sleep(1.second.toMillis)
- assert(dead.timeLeft < (1 second: Duration))
- assert(dead2.timeLeft < (1 second: Duration))
- }
+ // test precision
+ 1.second + 1.millisecond mustBe 1001.milliseconds
+ 100000.days + 1.nanosecond mustBe 8640000000000000001L.nanoseconds
+ 1.5.seconds.toSeconds mustBe 1
+ (-1.5).seconds.toSeconds mustBe -1
+
- { // check statically retaining finite-ness
- val d: FiniteDuration = 1.second * 2 / 1.4 mul 1.1 div 2.1 plus 3.seconds minus 1.millisecond min 1.second max 1.second
- }
+ // test unit stability
+ 1000.millis.unit mustBe MILLISECONDS
+ (1000.millis + 0.days).unit mustBe MILLISECONDS
+ 1.second.unit mustBe SECONDS
+ (1.second + 1.millisecond).unit mustBe MILLISECONDS
+
+
+ // test Deadline
+ val dead = 2.seconds.fromNow
+ val dead2 = 2 seconds fromNow
+ // view bounds vs. very local type inference vs. operator precedence: sigh
+ assert(dead.timeLeft > (1 second: Duration))
+ assert(dead2.timeLeft > (1 second: Duration))
+ Thread.sleep(1.second.toMillis)
+ assert(dead.timeLeft < (1 second: Duration))
+ assert(dead2.timeLeft < (1 second: Duration))
+
+
+ // check statically retaining finite-ness
+ val finiteDuration: FiniteDuration = 1.second plus 3.seconds minus 1.millisecond min 1.second max 1.second
+
}