tags:

views:

145

answers:

3

I am a bit confused by this ArithmeticException. I have tried this on Scala 2.8.0.RC6 and RC7.

scala> 7.12 to(8, 0.2)
res0: scala.collection.immutable.NumericRange[Double] = NumericRange(7.12, 7.32, 7.52, 7.72, 7.92)

scala> 7.12 to(8, 0.5)
res2: scala.collection.immutable.NumericRange[Double] = NumericRange(7.12, 7.62)

scala> 7.12 to(8, 0.3)
java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
 at java.math.BigDecimal.divide(BigDecimal.java:1525)
 at java.math.BigDecimal.divide(BigDecimal.java:1558)
 at scala.math.BigDecimal.$div(BigDecimal.scala:228)
 at scala.math.Numeric$BigDecimalAsIfIntegral$class.quot(Numeric.scala:156)
 at scala.math.Numeric$BigDecimalAsIfIntegral$.quot(Numeric.scala:163)
 at scala.math.Numeric$BigDecimalAsIfIntegral$.quot(Numeric.scala:163)
 at scala.math.Integral$IntegralOps.$div$percent(Integral.scala:23)
 at scala.collection.immutable.NumericRange.genericLength(NumericRange.scala:104)
 at scala.collection.immutable.NumericRange.<init>(NumericRange.scala:63)
 at scala.collection.immutable.NumericRange$Inclusive.<init>(NumericRange.scala:209)
 at ...
+5  A: 

I just answered this on Scala Forum:

scala> import java.math.{ MathContext => MC, RoundingMode => RM }
import java.math.{MathContext=>MC, RoundingMode=>RM}

scala> val mc1 = new MC(6, RM.HALF_DOWN)
mc1: java.math.MathContext = precision=6 roundingMode=HALF_DOWN


scala> val a1 = BigDecimal(1, mc1)
a1: scala.math.BigDecimal = 1

scala> val b1 = BigDecimal(3, mc1)
b1: scala.math.BigDecimal = 3

scala> a1 / b1
res10: scala.math.BigDecimal = 0.333333


scala> val a2 = BigDecimal(1, MC.DECIMAL128)
a2: scala.math.BigDecimal = 1

scala> val b2 = BigDecimal(3, MC.DECIMAL128)
b2: scala.math.BigDecimal = 3

scala> a2 / b2
res11: scala.math.BigDecimal = 0.3333333333333333333333333333333333

I'm not sure, however, this is the result you want:

scala> def BD128(d: Double): BigDecimal = BigDecimal(d, MC.DECIMAL128)
BD128: (d: Double)BigDecimal

scala> BD128(7.12) to(BD128(8), BD128(0.3))
res10: scala.collection.immutable.NumericRange.Inclusive[BigDecimal] = NumericRange(7.12, 7.42, 7.72)
Randall Schulz
Thanks for the WO. Do you consider this as a bug in RichDouble? If not, are the programmers supposed to know that the BigDecimal is used to implement to in RichDouble, and how to avoid this error?
olle kullberg
I can't say I think it's a problem with the numeric types themselves. Possibly you could point the finger at `NumericRange`, but to prevent this behavior there, it would have to impose a precision on any `BigDecimal` used to construct a range, so altogether, I'd say no, it's not a bug, just something its clients must know about.
Randall Schulz
I still find this strange. Do you recommend that one should:A) Always use the WO you presented (BD128(7.12) to(BD128(8), BD128(0.3))) OR B) Use the RichDouble.to method but being aware of that it might blow up, and when it does use the WO?
olle kullberg
+5  A: 

Where exactly does this BigDecimal come from? Start with Range.scala

  // Double works by using a BigDecimal under the hood for precise
  // stepping, but mapping the sequence values back to doubles with
  // .doubleValue.  This constructs the BigDecimals by way of the
  // String constructor (valueOf) instead of the Double one, which
  // is necessary to keep 0.3d at 0.3 as opposed to
  // 0.299999999999999988897769753748434595763683319091796875 or so.
  object Double {
    implicit val bigDecAsIntegral = scala.Numeric.BigDecimalAsIfIntegral
    implicit val doubleAsIntegral = scala.Numeric.DoubleAsIfIntegral
    def toBD(x: Double): BigDecimal = scala.BigDecimal valueOf x

    def apply(start: Double, end: Double, step: Double) =
      BigDecimal(toBD(start), toBD(end), toBD(step)) mapRange (_.doubleValue)

    def inclusive(start: Double, end: Double, step: Double) =
      BigDecimal.inclusive(toBD(start), toBD(end), toBD(step)) mapRange (_.doubleValue)
  }

And move to NumericRange.scala:

  // Motivated by the desire for Double ranges with BigDecimal precision,
  // we need some way to map a Range and get another Range.  This can't be
  // done in any fully general way because Ranges are not arbitrary
  // sequences but step-valued, so we have a custom method only we can call
  // which we promise to use responsibly.
  // 
  // The point of it all is that
  //
  //   0.0 to 1.0 by 0.1
  //
  // should result in
  //
  //   NumericRange[Double](0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0)
  //
  // and not 
  // 
  //   NumericRange[Double](0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9)
  //
  // or perhaps more importantly,
  //
  //   (0.1 to 0.3 by 0.1 contains 0.3) == true
  //
  private[immutable] def mapRange[A](fm: T => A)(implicit unum: Integral[A]): NumericRange[A] = {    
    val self = this

    // XXX This may be incomplete.
    new NumericRange[A](fm(start), fm(end), fm(step), isInclusive) {
      def copy(start: A, end: A, step: A): NumericRange[A] =
        if (isInclusive) NumericRange.inclusive(start, end, step)
        else NumericRange(start, end, step)

      private val underlyingRange: NumericRange[T] = self
      override def foreach[U](f: A => U) { underlyingRange foreach (x => f(fm(x))) }
      override def isEmpty = underlyingRange.isEmpty
      override def apply(idx: Int): A = fm(underlyingRange(idx))
      override def containsTyped(el: A) = underlyingRange exists (x => fm(x) == el)
    }
  }

Would the sky fall if toBD used a MathContext that allowed rounding? Which is the lesser of two evils? I'll defer that question to @extempore.

retronym
In this case a simple solution would be to use BigDecimal constructor to introduce rounding. I think the rounding should be done in the apply() and the inclusive() methods of the Double object in the Range class, since that is where we know that the expected return Range will only hold Doubles. Who should tell Odersky? :-)
olle kullberg