I have read a few questions about flaoting point numbers and their math. To me it looks like the issues only happen at greater scales (i.e. 10+ decimals). Now my issue happens at 2 decimals already and its quite big off in a few cases:
Following code:
System.out.println(
String.format("%.2f", BigDecimal.valueOf(2.8).divide(BigDecimal.valueOf(3.87), 2).doubleValue()));
System.out.println(
String.format("%.2f", BigDecimal.valueOf(2.41).divide(BigDecimal.valueOf(2.73), 2).doubleValue()));
generates this output respectively:
0.80
0.89
If I perform the calculations by hand (using google calculations), I get these results:
0.72351421188
0.88278388278
for the first calculations the results are really big (~0.08 off) while for the second one its very low (~0.01 off).
Is there some sane explanation as to why thie first result is that big? Or any way to get the right result using BigDecimal
?
Note that
System.out.println(2.8/3.87);
actually returns the right result (0.7235142118863048).
Please also note that the String.format
stuff is only used to check if the result was changed by it which is not the case. BigDecimal.valueOf(2.8).divide(BigDecimal.valueOf(3.87), 2).doubleValue()
yields exactly the same result.
The problem is that you're using the wrong BigDecimal#divide
for your rounding to 2 decimals. Here are the available arguments for the BigDecimal#divide
methods:
divide(BigDecimal divisor)
divide(BigDecimal divisor, int roundingMode)
divide(BigDecimal divisor, MathContext mc)
divide(BigDecimal divisor, RoundingMode roundingMode)
divide(BigDecimal divisor, int scale, int roundingMode)
divide(BigDecimal divisor, int scale, RoundingMode roundingMode)
Since you're using a divide
with a BigDecimal
and int
argument, it therefore uses the divide(BigDecimal divisor, int roundingMode)
, where your 2
is the rounding mode and NOT the scale. In this case, 2
is actually ROUND_CEILING
, and scale is unspecified.
Instead, you'll have to use either the divide(BigDecimal divisor, int scale, int roundingMode)
or divide(BigDecimal divisor, int scale, RoundingMode roundingMode)
. So change your calls to:
System.out.println(BigDecimal.valueOf(2.8).divide(BigDecimal.valueOf(3.87), 2,
BigDecimal.ROUND_HALF_UP));
System.out.println(BigDecimal.valueOf(2.41).divide(BigDecimal.valueOf(2.73), 2,
BigDecimal.ROUND_HALF_UP));
(Feel free to use a rounding mode other than ROUND_HALF_UP
.)
I'm not sure what scale it uses by default, but the ROUND_CEILING
you've specified with your 2
caused the issues in your calculations.
As for the mentioned comments, there are three possible ways to create the BigDecimal
with the specified values:
BigDecimal.valueOf(2.8)
new BigDecimal(2.8)
new BigDecimal("2.8")
The new BigDecimal(2.8)
still gives floating point errors, so I would advice to use either BigDecimal.valueOf(2.8)
as you already did, or the String constructor new BigDecimal("2.8")
.
This is all a bit irrelevant for your rounding issues however, since using the correct divide
method will give the correct results regardless of the BigDecimal
initialization you've used: