diff options
author | Josh Suereth <Joshua.Suereth@gmail.com> | 2012-09-12 07:54:08 -0700 |
---|---|---|
committer | Josh Suereth <Joshua.Suereth@gmail.com> | 2012-09-12 07:54:08 -0700 |
commit | ea242102060286cd3921ce552142435960352712 (patch) | |
tree | 48aad6e426c28de8d83978b0ea7c2079db6d1868 | |
parent | 7e1f10d228c1f0c47db4f2136dda4b5690f43445 (diff) | |
parent | 37cced5769e218e805cdcb1f87846f0f2548c8bb (diff) | |
download | scala-ea242102060286cd3921ce552142435960352712.tar.gz scala-ea242102060286cd3921ce552142435960352712.tar.bz2 scala-ea242102060286cd3921ce552142435960352712.zip |
Merge pull request #1261 from rkuhn/fix-duration-issues-RK
several fixes to scala.concurrent.util.Duration
-rw-r--r-- | src/library/scala/concurrent/impl/Promise.scala | 19 | ||||
-rw-r--r-- | src/library/scala/concurrent/util/Duration.scala | 610 | ||||
-rw-r--r-- | test/files/jvm/duration-java.check | 46 | ||||
-rw-r--r-- | test/files/jvm/duration-java/Test.java | 12 | ||||
-rw-r--r-- | test/files/jvm/duration-tck.scala | 186 | ||||
-rw-r--r-- | test/files/neg/deadline-inf-illegal.check | 15 | ||||
-rw-r--r-- | test/files/neg/deadline-inf-illegal.scala | 8 |
7 files changed, 764 insertions, 132 deletions
diff --git a/src/library/scala/concurrent/impl/Promise.scala b/src/library/scala/concurrent/impl/Promise.scala index f7ab85dc0c..9228872f2b 100644 --- a/src/library/scala/concurrent/impl/Promise.scala +++ b/src/library/scala/concurrent/impl/Promise.scala @@ -12,7 +12,7 @@ package scala.concurrent.impl import java.util.concurrent.TimeUnit.NANOSECONDS import scala.concurrent.{ ExecutionContext, CanAwait, OnCompleteRunnable, TimeoutException, ExecutionException } -import scala.concurrent.util.{ Duration, Deadline } +import scala.concurrent.util.{ Duration, Deadline, FiniteDuration } import scala.annotation.tailrec import scala.util.control.NonFatal import scala.util.{ Try, Success, Failure } @@ -64,7 +64,7 @@ private[concurrent] object Promise { protected final def tryAwait(atMost: Duration): Boolean = { @tailrec - def awaitUnsafe(deadline: Deadline, nextWait: Duration): Boolean = { + def awaitUnsafe(deadline: Deadline, nextWait: FiniteDuration): Boolean = { if (!isCompleted && nextWait > Duration.Zero) { val ms = nextWait.toMillis val ns = (nextWait.toNanos % 1000000l).toInt // as per object.wait spec @@ -84,14 +84,13 @@ private[concurrent] object Promise { } } - if (atMost eq Duration.Undefined) - throw new IllegalArgumentException("cannot wait for Undefined period") - else if (atMost <= Duration.Zero) - isCompleted - else if (atMost == Duration.Inf) - awaitUnbounded() - else - awaitUnsafe(atMost.fromNow, atMost) + import Duration.Undefined + atMost match { + case u if u eq Undefined => throw new IllegalArgumentException("cannot wait for Undefined period") + case Duration.Inf => awaitUnbounded + case Duration.MinusInf => isCompleted + case f: FiniteDuration => if (f > Duration.Zero) awaitUnsafe(f.fromNow, f) else isCompleted + } } @throws(classOf[TimeoutException]) diff --git a/src/library/scala/concurrent/util/Duration.scala b/src/library/scala/concurrent/util/Duration.scala index bf0f3006f1..1a4bc1323d 100644 --- a/src/library/scala/concurrent/util/Duration.scala +++ b/src/library/scala/concurrent/util/Duration.scala @@ -10,36 +10,121 @@ package scala.concurrent.util import java.util.concurrent.TimeUnit import TimeUnit._ -import java.lang.{ Double => JDouble } +import java.lang.{ Double => JDouble, Long => JLong } import language.implicitConversions -case class Deadline private (time: Duration) { - def +(other: Duration): Deadline = copy(time = time + other) - def -(other: Duration): Deadline = copy(time = time - other) - def -(other: Deadline): Duration = time - other.time - def timeLeft: Duration = this - Deadline.now +/** + * 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: FiniteDuration) extends Ordered[Deadline] { + /** + * Return a deadline advanced (i.e. moved into the future) by the given duration. + */ + def +(other: FiniteDuration): Deadline = copy(time = time + other) + /** + * Return a deadline moved backwards (i.e. towards the past) by the given duration. + */ + def -(other: FiniteDuration): 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): FiniteDuration = 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: FiniteDuration = 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 + * + * Infinite inputs (and NaN) are converted into [[Duration.Inf]], [[Duration.MinusInf]] and [[Duration.Undefined]], respectively. + * + * @throws IllegalArgumentException if the length was finite but the resulting duration cannot be expressed as a [[FiniteDuration]] + */ + 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)) + + // Double stores 52 bits mantissa, but there is an implied '1' in front, making the limit 2^53 + final val maxPreciseDouble = 9007199254740992d /** * 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. + * + * @throws NumberFormatException if format is not parseable */ def apply(s: String): Duration = { val s1: String = s filterNot (_.isWhitespace) @@ -48,21 +133,24 @@ object Duration { case "MinusInf" | "-Inf" => MinusInf case _ => val unitName = s1.reverse takeWhile (_.isLetter) reverse; - def length = JDouble.parseDouble(s1 dropRight unitName.length) timeUnit get unitName match { - case Some(unit) => Duration(length, unit) - case _ => sys.error("format error " + s) + case Some(unit) => + val valueStr = s1 dropRight unitName.length + val valueD = JDouble.parseDouble(valueStr) + if (valueD >= -maxPreciseDouble && valueD <= maxPreciseDouble) Duration(valueD, unit) + else Duration(JLong.parseLong(valueStr), unit) + case _ => throw new NumberFormatException("format error " + s) } } } // "ms milli millisecond" -> List("ms", "milli", "millis", "millisecond", "milliseconds") - private def words(s: String) = (s.trim split "\\s+").toList - private def expandLabels(labels: String): List[String] = { + private[this] def words(s: String) = (s.trim split "\\s+").toList + private[this] def expandLabels(labels: String): List[String] = { val hd :: rest = words(labels) hd :: rest.flatMap(s => List(s, s + "s")) } - private val timeUnitLabels = List( + private[this] val timeUnitLabels = List( DAYS -> "d day", HOURS -> "h hour", MINUTES -> "min minute", @@ -80,124 +168,218 @@ 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 = - fromNanos((nanos + 0.5).toLong) + /** + * 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. + * + * @throws IllegalArgumentException if the length was finite but the resulting duration cannot be expressed as a [[FiniteDuration]] + */ + 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") + else + fromNanos((nanos + 0.5).toLong) + } + + private[this] final val µs_per_ns = 1000L + private[this] final val ms_per_ns = µs_per_ns * 1000 + private[this] final val s_per_ns = ms_per_ns * 1000 + private[this] final val min_per_ns = s_per_ns * 60 + private[this] final val h_per_ns = min_per_ns * 60 + private[this] final val d_per_ns = h_per_ns * 24 + /** + * Construct a finite duration from the given number of nanoseconds. The + * result will have the coarsest possible time unit which can exactly express + * this duration. + * + * @throws IllegalArgumentException for `Long.MinValue` since that would lead to inconsistent behavior afterwards (cannot be negated) + */ 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 + } + def -(other: Duration): Duration = other match { + case x if x eq Undefined => Undefined + case x: Infinite if x eq 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") - def *(factor: Double): Duration = this - def /(factor: Double): Duration = this + def *(factor: Double): Duration = + if (factor == 0d || factor.isNaN) Undefined + else if (factor < 0d) -this + else this + def /(factor: Double): Duration = + if (factor.isNaN || factor.isInfinite) Undefined + else if ((factor compare 0d) < 0) -this + else this 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") + private[this] def fail(what: String) = throw new IllegalArgumentException(s"$what not allowed on infinite Durations") + 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 // Undefined != Undefined + case x if x eq this => 0 // `case Inf` will include null checks in the byte code + 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 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 + * + * Infinite inputs (and NaN) are converted into [[Duration.Inf]], [[Duration.MinusInf]] and [[Duration.Undefined]], respectively. + * + * @throws IllegalArgumentException if the length was finite but the resulting duration cannot be expressed as a [[FiniteDuration]] + */ + 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 NumberFormatException 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 * @@ -207,57 +389,212 @@ 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 @throws IllegalArgumentException when invoked on a non-finite duration + * + * @define ovf @throws IllegalArgumentException in case of a finite overflow: the range of a finite duration is +-(2^63-1)ns, and no conversion to infinite durations takes place. */ -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. When involving non-finite summands the semantics match those + * of Double. + * + * $ovf + */ def +(other: Duration): Duration + /** + * Return the difference of that duration and this. When involving non-finite summands the semantics match those + * of Double. + * + * $ovf + */ def -(other: Duration): Duration + /** + * Return this duration multiplied by the scalar factor. When involving non-finite factors the semantics match those + * of Double. + * + * $ovf + */ def *(factor: Double): Duration + /** + * Return this duration divided by the scalar factor. When involving non-finite factors the semantics match those + * of Double. + * + * $ovf + */ 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 - def fromNow: Deadline = Deadline.now + this // Java API + + /** + * Return this duration divided by the scalar factor. When involving non-finite factors the semantics match those + * of Double. + * + * $ovf + */ 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. When involving non-finite summands the semantics match those + * of Double. + * + * $ovf + */ def minus(other: Duration) = this - other + /** + * Return this duration multiplied by the scalar factor. When involving non-finite factors the semantics match those + * of Double. + * + * $ovf + */ 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. When involving non-finite summands the semantics match those + * of Double. + * + * $ovf + */ def plus(other: Duration) = this + other } @@ -268,11 +605,43 @@ object FiniteDuration { def apply(length: Long, unit: TimeUnit) = new FiniteDuration(length, unit) def apply(length: Long, unit: String) = new FiniteDuration(length, Duration.timeUnit(unit)) + + // limit on abs. value of durations in their units + private final val max_ns = Long.MaxValue + private final val max_µs = max_ns / 1000 + private final val max_ms = max_µs / 1000 + private final val max_s = max_ms / 1000 + private final val max_min= max_s / 60 + private final val max_h = max_min / 60 + private final val max_d = max_h / 24 } -class FiniteDuration(val length: Long, val unit: TimeUnit) extends Duration { +/** + * 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. + */ +final class FiniteDuration(val length: Long, val unit: TimeUnit) extends Duration { + import FiniteDuration._ import Duration._ + private[this] def bounded(max: Long) = -max <= length && length <= max + + require(unit match { + /* + * enforce the 2^63-1 ns limit, must be pos/neg symmetrical because of unary_- + */ + case NANOSECONDS ⇒ bounded(max_ns) + case MICROSECONDS ⇒ bounded(max_µs) + case MILLISECONDS ⇒ bounded(max_ms) + case SECONDS ⇒ bounded(max_s) + case MINUTES ⇒ bounded(max_min) + case HOURS ⇒ bounded(max_h) + case DAYS ⇒ bounded(max_d) + case _ ⇒ + val v = DAYS.convert(length, unit) + -max_d <= v && v <= max_d + }, "Duration is limited to +-(2^63-1)ns (ca. 292 years)") + def toNanos = unit.toNanos(length) def toMicros = unit.toMicros(length) def toMillis = unit.toMillis(length) @@ -282,27 +651,68 @@ class FiniteDuration(val length: Long, val unit: TimeUnit) extends Duration { def toDays = unit.toDays(length) def toUnit(u: TimeUnit) = toNanos.toDouble / NANOSECONDS.convert(1, u) - private def unitString = timeUnitName(unit) + ( if (length == 1) "" else "s" ) + /** + * Construct a [[Deadline]] from this duration by adding it to the current instant `Deadline.now`. + */ + def fromNow: Deadline = Deadline.now + this + + private[this] def unitString = timeUnitName(unit) + ( if (length == 1) "" else "s" ) override def toString = "" + length + " " + unitString def compare(other: Duration) = other match { case x: FiniteDuration => toNanos compare x.toNanos case _ => -(other compare this) } + + // see https://www.securecoding.cert.org/confluence/display/java/NUM00-J.+Detect+or+prevent+integer+overflow + private[this] 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[this] 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(toNanos + x.toNanos) + case x: FiniteDuration => add(x.length, x.unit) case _ => other } def -(other: Duration) = other match { - case x: FiniteDuration => fromNanos(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[this] 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) = 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 + override def mul(factor: Double) = this * factor + def min(other: FiniteDuration) = if (this < other) this else other + def max(other: FiniteDuration) = if (this > other) this else other def unary_- = Duration(-length, unit) @@ -383,5 +793,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-java.check b/test/files/jvm/duration-java.check index 7ae257dcc0..49d06fbe93 100644 --- a/test/files/jvm/duration-java.check +++ b/test/files/jvm/duration-java.check @@ -201,7 +201,7 @@ 6.0E7 seconds => 1000000 minutes 1.0E8 seconds => 100000000 seconds 1.0E9 seconds => 1000000000 seconds - 1.0E12 seconds => 9223372036854775807 nanoseconds + 1.0E12 seconds => class java.lang.IllegalArgumentException 0.0 minutes => 0 days 1.0 minutes => 1 minute 7.0 minutes => 7 minutes @@ -251,8 +251,8 @@ 3.0E7 minutes => 500000 hours 6.0E7 minutes => 1000000 hours 1.0E8 minutes => 100000000 minutes - 1.0E9 minutes => 9223372036854775807 nanoseconds - 1.0E12 minutes => 9223372036854775807 nanoseconds + 1.0E9 minutes => class java.lang.IllegalArgumentException + 1.0E12 minutes => class java.lang.IllegalArgumentException 0.0 hours => 0 days 1.0 hours => 1 hour 7.0 hours => 7 hours @@ -295,15 +295,15 @@ 60000.0 hours => 2500 days 100000.0 hours => 100000 hours 1000000.0 hours => 1000000 hours - 7000000.0 hours => 9223372036854775807 nanoseconds - 1.0E7 hours => 9223372036854775807 nanoseconds - 1.2E7 hours => 9223372036854775807 nanoseconds - 2.4E7 hours => 9223372036854775807 nanoseconds - 3.0E7 hours => 9223372036854775807 nanoseconds - 6.0E7 hours => 9223372036854775807 nanoseconds - 1.0E8 hours => 9223372036854775807 nanoseconds - 1.0E9 hours => 9223372036854775807 nanoseconds - 1.0E12 hours => 9223372036854775807 nanoseconds + 7000000.0 hours => class java.lang.IllegalArgumentException + 1.0E7 hours => class java.lang.IllegalArgumentException + 1.2E7 hours => class java.lang.IllegalArgumentException + 2.4E7 hours => class java.lang.IllegalArgumentException + 3.0E7 hours => class java.lang.IllegalArgumentException + 6.0E7 hours => class java.lang.IllegalArgumentException + 1.0E8 hours => class java.lang.IllegalArgumentException + 1.0E9 hours => class java.lang.IllegalArgumentException + 1.0E12 hours => class java.lang.IllegalArgumentException 0.0 days => 0 days 1.0 days => 1 day 7.0 days => 7 days @@ -345,16 +345,18 @@ 30000.0 days => 30000 days 60000.0 days => 60000 days 100000.0 days => 100000 days - 1000000.0 days => 9223372036854775807 nanoseconds - 7000000.0 days => 9223372036854775807 nanoseconds - 1.0E7 days => 9223372036854775807 nanoseconds - 1.2E7 days => 9223372036854775807 nanoseconds - 2.4E7 days => 9223372036854775807 nanoseconds - 3.0E7 days => 9223372036854775807 nanoseconds - 6.0E7 days => 9223372036854775807 nanoseconds - 1.0E8 days => 9223372036854775807 nanoseconds - 1.0E9 days => 9223372036854775807 nanoseconds - 1.0E12 days => 9223372036854775807 nanoseconds + 1000000.0 days => class java.lang.IllegalArgumentException + 7000000.0 days => class java.lang.IllegalArgumentException + 1.0E7 days => class java.lang.IllegalArgumentException + 1.2E7 days => class java.lang.IllegalArgumentException + 2.4E7 days => class java.lang.IllegalArgumentException + 3.0E7 days => class java.lang.IllegalArgumentException + 6.0E7 days => class java.lang.IllegalArgumentException + 1.0E8 days => class java.lang.IllegalArgumentException + 1.0E9 days => class java.lang.IllegalArgumentException + 1.0E12 days => class java.lang.IllegalArgumentException +10000000000000001 nanoseconds => 10000000000000001 nanoseconds +10000000000000002 nanoseconds => 10000000000000002 nanoseconds Inf => Duration.Inf -Inf => Duration.MinusInf +Inf => Duration.Inf diff --git a/test/files/jvm/duration-java/Test.java b/test/files/jvm/duration-java/Test.java index 02feb522b8..1c53ccb266 100644 --- a/test/files/jvm/duration-java/Test.java +++ b/test/files/jvm/duration-java/Test.java @@ -26,10 +26,18 @@ public class Test { for (TimeUnit t : TimeUnit.values()) { for (Double n: makeNumbers()) { String s = "" + n + " " + t.toString().toLowerCase(); - Duration d = Duration.create(n, t); - p(String.format("%25s => %s", s, d)); + String result; + try { + Duration d = Duration.create(n, t); + result = d.toString(); + } catch(Exception e) { + result = e.getClass().toString(); + } + p(String.format("%25s => %s", s, result)); } } + for (String s: new String[] {"10000000000000001 nanoseconds", "10000000000000002 nanoseconds"}) + p(String.format("%25s => %s", s, Duration.create(s))); for (String s: Arrays.asList("Inf", "-Inf", "+Inf", "PlusInf", "MinusInf")) { Duration d = Duration.create(s); p(String.format("%25s => %s", s, d)); diff --git a/test/files/jvm/duration-tck.scala b/test/files/jvm/duration-tck.scala new file mode 100644 index 0000000000..0947e84004 --- /dev/null +++ b/test/files/jvm/duration-tck.scala @@ -0,0 +1,186 @@ +/** + * Copyright (C) 2012 Typesafe Inc. <http://www.typesafe.com> + */ + +import scala.concurrent.util._ +import duration._ +import scala.reflect._ +import java.util.concurrent.TimeUnit._ +import scala.tools.partest.TestUtil.intercept + +object Test extends App { + + implicit class Assert(val left: Any) extends AnyVal { + 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") + } + } + + 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 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 (_ * -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 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) + undef / 1 mustBe undef + undef / nan mustBe undef + undef * 1 mustBe undef + undef * nan mustBe undef + inputs foreach (x => x / zero mustBe x.toUnit(SECONDS) / 0d) + inputs foreach (x => x / 0d mustBe Duration.fromNanos(x.toUnit(NANOSECONDS) / 0d)) + inputs foreach (x => x / -0d mustBe Duration.fromNanos(x.toUnit(NANOSECONDS) / -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 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 + + + // 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 + assert(dead.timeLeft > 1.second) + assert(dead2.timeLeft > 1.second) + Thread.sleep(1.second.toMillis) + assert(dead.timeLeft < 1.second) + assert(dead2.timeLeft < 1.second) + + + // check statically retaining finite-ness + val finiteDuration: FiniteDuration = 1.second plus 3.seconds minus 1.millisecond min 1.second max 1.second + + +} diff --git a/test/files/neg/deadline-inf-illegal.check b/test/files/neg/deadline-inf-illegal.check new file mode 100644 index 0000000000..2b9b25e48e --- /dev/null +++ b/test/files/neg/deadline-inf-illegal.check @@ -0,0 +1,15 @@ +deadline-inf-illegal.scala:5: error: value fromNow is not a member of scala.concurrent.util.Duration + d.fromNow + ^ +deadline-inf-illegal.scala:6: error: type mismatch; + found : scala.concurrent.util.Duration + required: scala.concurrent.util.FiniteDuration + Deadline.now + d + ^ +deadline-inf-illegal.scala:7: error: overloaded method value - with alternatives: + (other: scala.concurrent.util.Deadline)scala.concurrent.util.FiniteDuration <and> + (other: scala.concurrent.util.FiniteDuration)scala.concurrent.util.Deadline + cannot be applied to (scala.concurrent.util.Duration) + Deadline.now - d + ^ +three errors found diff --git a/test/files/neg/deadline-inf-illegal.scala b/test/files/neg/deadline-inf-illegal.scala new file mode 100644 index 0000000000..161089bfee --- /dev/null +++ b/test/files/neg/deadline-inf-illegal.scala @@ -0,0 +1,8 @@ +import concurrent.util.{ Deadline, Duration } + +class T { + val d: Duration = Duration.Zero + d.fromNow + Deadline.now + d + Deadline.now - d +} |