Orkun
Orkun

Reputation: 7228

BigDecimal : HALF_UP rounding with setScale

We have the following code:

BigDecimal net = price
.divide(taxCumulative, RoundingMode.HALF_UP)
.setScale(2, BigDecimal.ROUND_UP); 

We are unit testing this and getting different results depending on if we use @Transactional on the test classes or not.

I just want to know if we should expect the application of the HALF_UP to be considered along with the setScale or before it.

In example:

Say that :

price = 4.00

taxCumulative = 1.20

Would you expect the calculation to be like :

a) 4.00 / 1.20 = 3.33333333... --> HALF_UP --> 3 --> setScale --> 3

or

b) 4.00 / 1.20 = 3.33333333... --> HALF_UP with setScale 2 --> 3.33

Like I said, we have unit tests against this code that behaves differently when we have the @Transactional or not. So we could not conclude. In real world, the result is b. But a also makes sense.

Any thoughts?

UPDATE:

Following @Mark's suggestions, I ve created a test outside the spring context. (but i guess there is no way to share it online like codepen).

package com.company;

import java.math.BigDecimal;
import java.math.RoundingMode;

public class Main {

    public static void main(String[] args) {
        BigDecimal price = new BigDecimal("4.00");
        BigDecimal taxCumulative = new BigDecimal("1.20");
        BigDecimal net = price.divide(taxCumulative, RoundingMode.HALF_UP).setScale(2, BigDecimal.ROUND_UP);
        System.out.println("With String constructor, the net =  " + net.toString());
        // prints 3.33
        price = new BigDecimal(4.00);
        taxCumulative = new BigDecimal(1.20);
        net = price.divide(taxCumulative, RoundingMode.HALF_UP).setScale(2, BigDecimal.ROUND_UP);
        System.out.println("With doubles, the net =  " + net.toString());
        // prints 3.00
    }
}

So, as pointed out by @gtgaxiola the String constructor makes the difference.

And as for the question for the use of setScale for the divide operation. I have done some more tests :

price = new BigDecimal("4.000000");
taxCumulative = new BigDecimal("1.2000000");
net = price.divide(taxCumulative, RoundingMode.HALF_UP).setScale(2, BigDecimal.ROUND_UP);
// 3.34

price = new BigDecimal("4.000000");
taxCumulative = new BigDecimal("1.2000000");
net = price.divide(taxCumulative, RoundingMode.HALF_UP);
// 3.333333

price = new BigDecimal("4");
taxCumulative = new BigDecimal("1.2");
net = price.divide(taxCumulative, RoundingMode.HALF_UP);
// 3

So the result highly depends on the precision of the string input and the setScale is applied after the divide yields its result.

Upvotes: 3

Views: 2708

Answers (1)

gtgaxiola
gtgaxiola

Reputation: 9331

It would seem it gets affected depending on how you construct your BigDecimal

Check the Constructor Notes on public BigDecimal(double val)

1.- The results of this constructor can be somewhat unpredictable. One might assume that writing new BigDecimal(0.1) in Java creates a BigDecimal which is exactly equal to 0.1 (an unscaled value of 1, with a scale of 1), but it is actually equal to 0.1000000000000000055511151231257827021181583404541015625. This is because 0.1 cannot be represented exactly as a double (or, for that matter, as a binary fraction of any finite length). Thus, the value that is being passed in to the constructor is not exactly equal to 0.1, appearances notwithstanding.

2.-The String constructor, on the other hand, is perfectly predictable: writing new BigDecimal("0.1") creates a BigDecimal which is exactly equal to 0.1, as one would expect. Therefore, it is generally recommended that the String constructor be used in preference to this one.

3.-When a double must be used as a source for a BigDecimal, note that this constructor provides an exact conversion; it does not give the same result as converting the double to a String using the Double.toString(double) method and then using the BigDecimal(String) constructor. To get that result, use the static valueOf(double) method.

Upvotes: 2

Related Questions