typelevel / typelevel/scalacheck

arbitrary[BigDecimal] generates numbers that cause NumberFormatExceptions when serialized and deserialized

Open
#124 2 comments 3 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Scala
Stars
2k
Forks
393
Avg merge
6h 42m
Merged PRs (30d)
4

Description

I had arbitrary[BigDecimal] generate the following: "0E+2147483648" in version 1.11.3. That scale is bigger than Int.MaxValue, so when you serialize the BigDecimal as a String and then try to reconstruct it with that String you will get a NumberFormatException. I understand why this is, but it's troublesome.

scala> BigDecimal(0, Int.MinValue)
res6: scala.math.BigDecimal = 0E+2147483648

According to the Javadocs, when you use this constructor, the value of the BigDecimal is going to be unscaledValue * (10^(-1 * scale)). So Int.MinValue actually becomes the maximum scale, and thanks to two's complement, this actually results in impossible 32-bit integer scale values.

A quick fix for this case is just to choose between Int.MinValue + limit + 1, but I think there's more than meets the eye to this issue. For instance, when I ran this generator several times to see what was going on by using this print call: println(s"unscaledVal is $unscaledVal, scale is $scale, limit is $limit, mc is $mc") I found that it'll make other problematic numbers too:

unscaledVal is -28334198897217871282176, scale is -2147483640, limit is 7, mc is precision=16 roundingMode=HALF_EVEN

So what's wrong with this?

scala> val orig = BigDecimal(BigInt("-28334198897217871282176"), -2147483640, UNLIMITED)
orig: scala.math.BigDecimal = -2.8334198897217871282176E+2147483662

scala> BigDecimal(orig.toString)
java.lang.NumberFormatException
  at java.math.BigDecimal.<init>(BigDecimal.java:554)
  at java.math.BigDecimal.<init>(BigDecimal.java:383)
  at java.math.BigDecimal.<init>(BigDecimal.java:806)
  at scala.math.BigDecimal$.exact(BigDecimal.scala:125)
  at scala.math.BigDecimal$.apply(BigDecimal.scala:283)
  ... 33 elided

I realize we can blame java.math.BigDecimal for its inconsistent constructors, but, it's still problematic here. Should there be a generator provided that doesn't suffer from this gotcha? Here is one that works for me:

lazy val genBigDecimal: Gen[BigDecimal] = for {
    unscaledVal <- arbitrary[BigInt]
    scale <- Gen.chooseNum(Int.MinValue + unscaledVal.abs.toString.length, Int.MaxValue)
} yield BigDecimal(unscaledVal, scale)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the arbitrary[BigDecimal] generator and reproduce the provided Int.MinValue and large-negative-scale examples. Compare generated values' String serialization with reconstruction via BigDecimal(String); done means the generator no longer produces values that fail this round trip, while preserving its intended coverage.

Written by the indexing model from the issue text.

Assessment

Tech stack
scala
Domain
testing-qa
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.