diff options
-rw-r--r-- | src/library/scala/concurrent/util/Duration.scala | 522 | ||||
-rw-r--r-- | test/files/jvm/duration-tck.scala | 236 |
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 + } |