Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add choose methods for both Java and Scala BigDecimal. #670

Merged
merged 4 commits into from
Oct 24, 2020

Conversation

non
Copy link
Contributor

@non non commented Jul 1, 2020

In addition to implicit Choose instances for scala.math.BigDecimal and
java.math.BigDecimal we also include explicit constructor methods, since users
may wish to be explicit about the scale they want. We may want to put those
methods directly on Gen, currently the ergonomics of using this are a bit bad:

Gen.Choose.chooseBigDecimalScale(100).choose(0, 1)

The BigDecimal generation is not yet tested. That will also be added in a
follow up. This also optimizes the BigInt generator a bit and generalizes it to
java.math.BigInteger to support that as well.

Addresses #631, #637, and #664

In addition to implicit Choose instances for scala.math.BigDecimal and
java.math.BigDecimal we also include explicit constructor methods, since users
may wish to be explicit about the scale they want. We may want to put those
methods directly on Gen, currently the ergonomics of using this are a bit bad:

    Gen.Choose.chooseBigDecimalScale(100).choose(0, 1)

The BigDecimal generation is not yet tested. That will also be added in a
follow up. This also optimizes the BigInt generator a bit and generalizes it to
java.math.BigInteger to support that as well.

Addresses typelevel#631, typelevel#637, and typelevel#664
@non non requested a review from ashawley July 1, 2020 14:05
@non non mentioned this pull request Jul 10, 2020
case n if n > 0 => throw new IllegalBoundsError(low, high)
case 0 => Gen.const(low)
case _ => /* n < 0 */
val s = (low.scale max high.scale) max minScale
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's less explicit, but couldn't the user just specify the minimum desired scale on either of the parameters for high and low to the Gen.choose method? For example,

forAll(Gen.choose(BigDecimal(-2).pow(29).setScale(10),
                  BigDecimal( 2).pow(29))) { (d: BigDecimal) =>
  d.scale >= 10
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also came up in the discussion in #637, but I didn't conclude that there's a justification for the minScale parameter to the Choose factory.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could accomplish this by just changing this line in your pr:

val s = (low.scale max high.scale) max (low.scale min high.scale)

Copy link
Contributor Author

@non non Jul 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what that last line accomplishes, the LHS (low.scale max high.scale) will always be chosen over the RHS (low.scale min high.scale).

My experience here is that many people using BigDecimal don't necessarily think about scale. For example, code like Gen.choose(BigDecimal(0), BigDecimal(1)) will only produce two possible values, but this issue won't necessarily be detected by authors, leading to unexpectedly weak tests.

Using the implicit scale in BigDecimal has tripped people up, especially given how tricky it is to reason about. For example, authors might think that Gen.choose(BigDecimal(0.0000), BigDecimal(1.0000)) will give them a scale of 4, but actually it will be 1 due to a different treatment of String and Double constructors:

scala> val x = BigDecimal(0.0000)
val x: scala.math.BigDecimal = 0.0

scala> x.scale
val res0: Int = 1

scala> val y = BigDecimal("0.0000")
val y: scala.math.BigDecimal = 0.0000

scala> y.scale
val res1: Int = 4

I'm open to simplifying the API a bit but I strongly feel like we should have a relatively rigorous minScale, even if it's hardcoded. It's possible to truncate a generated BigDecimal for less precision but the reverse is not possible, so I feel like more precision (to a point) is a better default.

@ashawley ashawley added this to the 1.15.0 milestone Sep 22, 2020
@larsrh
Copy link
Contributor

larsrh commented Oct 13, 2020

Should we go ahead with this for 1.15.0?

@non
Copy link
Contributor Author

non commented Oct 13, 2020

@larsrh I'm happy to including this as-is. I know @ashawley had concerns about it, so I didn't want to merge it without hearing more from him.

@ashawley
Copy link
Contributor

I will try to take a look at this again. I had written some code that experimented with this. Hopefully, it can help me remember where I left off.

@non
Copy link
Contributor Author

non commented Oct 19, 2020

@ashawley Ping?

If I made the methods taking minScale (i.e. chooseBigDecimalScale and chooseJavaBigDecimalScale) private for now, would that make you feel better? Then we wouldn't be committed to keeping them around for binary compatibility -- we could get a release out while giving you more time to consider the issue.

@non
Copy link
Contributor Author

non commented Oct 24, 2020

I'm going to go ahead and make the change I suggested -- it seems like a reasonable step to do regardless.

@larsrh
Copy link
Contributor

larsrh commented Oct 24, 2020

Thanks, Erik!

@larsrh larsrh merged commit 594c1ea into typelevel:master Oct 24, 2020
@ashawley ashawley mentioned this pull request Apr 8, 2021
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants