BigDecimal, double and Rounding
This subject
came about thanks to my involvement with the Intelligent Pricing project which
introduced me to the client side pricing functionality found in:
com.barcap.fox.client.pricing
and it’s sub
packages. This pricing code makes heavy use of classes from:
com.barcap.fox.client.dataobjects
and it’s sub
packages.
Browsing through
the ClearQuest cases raised against the Intelligent Pricing project, I noticed
that almost all the client side cases were to do with rounding issues.
I smelled a rat
but had no remit to follow it up.
However, I can
say that floating point arithmetic is being undertaken in the client side
pricing code and may be impacted by the well known problem that not all
floating point numbers can be represents as a binary fraction.
Take this
example: Start with a relatively large floating point number (a price, for
instance) and add a few, relatively small, floating point numbers (spreads,
say) to it.
After adding
each spread we round the resulting price to two decimal places.
Are we expecting
any problems?
The answer (in
Java) depends on whether or not you are using double or BigDecimal to hold your
prices and spreads.
Take the simple
case where we add 0.0001 to 1.2345 10 times. In the real world, this equates to
a sequence like so:
Original
Value Rounded Value
1.2345
1.2346
1.2347
1.2348
1.2349
1.2350
1.2351
1.2352
1.2353
1.2354 1.23
1.23
1.23
1.23
1.23
1.24
1.24
1.24
1.24
1.24
Ok, using a
simple “half round up” algorithm, do we get this in a Java Program?
public static void main(String[] argv) {
int places= 2;
double value= 1.2345;
for (int i= 0; i < 10; i++) {
System.err.println(
"+0.000" +i +" rounded= " +round(value, places));
value += 0.0001;
}
}
static double round(double value, int places) {
double power= 1;
while (places-- > 0)
power *= 10.0;
return Math.round(value*power)/power;
}
Nope. This
produces a rounded sequence like so.
1.23
1.23
1.23
1.23
1.23
1.23
1.24
1.24
1.24
1.24
The item in bold
is wrong!
Now, I said this
problem had a lot to do with java.math.BigDecimal, so lets’ replace the basic
Math round() method with this one:
static double round(double value, int places) {
BigDecimal decimal= new BigDecimal(value);
decimal= decimal.setScale(places, BigDecimal.ROUND_HALF_UP);
return decimal.doubleValue();
}
Did that fix it?
Well, we get
this rounded sequence:
1.23
1.23
1.23
1.23
1.23
1.23
1.24
1.24
1.24
1.24
The item in bold
is still wrong!
This may be the
origin of a rather frightening belief I’ve heard bandied about here that
java.math.BigDecimal is “broken”.
In reality, the
aspect of this that is truly broken is the use of double to store the price.
Consider this
from the java.math.BigDecimal javadoc.
public
BigDecimal(double val)
Translates a
double into a BigDecimal. The scale of the BigDecimal is the smallest value
such that (10scale * val) is an integer.
Note: the
results of this constructor can be somewhat unpredictable. One might assume
that new BigDecimal(.1) is exactly equal to .1, but it is actually equal to
.1000000000000000055511151231257827021181583404541015625. This is so because .1
cannot be represented exactly as a double (or, for that matter, as a binary
fraction of any finite length). Thus, the long value that is being passed in to
the constructor is not exactly equal to .1, appearances notwithstanding.
The (String)
constructor, on the other hand, is perfectly predictable: new
BigDecimal(".1") is exactly equal to .1, as one would expect.
Therefore, it is generally recommended that the (String) constructor be used in
preference to this one.
I don’t think
anyone could argue with that.
So, let’s try
again with the price held as a string:
public static void main(String[] argv) {
int places= 2;
int integerPart= 1;
int decimalPart= 2345;
for (int i= 0; i < 10; i++) {
String value= integerPart +"." +decimalPart;
System.err.println(
"+0.000" +i +" rounded= " +round(value, places));
decimalPart += 1;
}
}
static double round(String value, int places) {
BigDecimal decimal= new BigDecimal(value);
decimal= decimal.setScale(places, BigDecimal.ROUND_HALF_UP);
return decimal.doubleValue();
}
We now get this
rounded sequence:
1.23
1.23
1.23
1.23
1.23
1.24
1.24
1.24
1.24
1.24
Our bold item is
now correct!
However,
carrying around the integer and decimal part of a price is a bit cumbersome, so
let’s delegate that responsibility to BigDecimal and try again:
public static void main(String[] argv) {
int places= 2;
BigDecimal value= new BigDecimal("1.2345");
BigDecimal spread= new BigDecimal("0.0001");
for (int i= 0; i < 10; i++) {
System.err.println(
"+0.000" +i +" rounded= " +round(value, places));
value= value.add(spread);
}
}
static double round(BigDecimal value, int places) {
return value.setScale(places,
BigDecimal.ROUND_HALF_UP).doubleValue();
}
What do we get
now?
1.23
1.23
1.23
1.23
1.23
1.24
1.24
1.24
1.24
1.24
Bingo.
Here’s a
composition of all the methods to see who gets what:
import
java.math.BigDecimal;
public class
Rounding {
public static void main(String[] argv) {
int places= 2;
double value= 1.2345;
int integerPart= 1;
int decimalPart= 2345;
BigDecimal bdv= new BigDecimal("1.2345");
BigDecimal spread= new BigDecimal("0.0001");
for (int i= 0; i < 10; i++) {
String szValue= integerPart +"." +decimalPart;
double math= math(value, places);
double decimal= decimal(value, places);
double string= string(szValue, places);
double round= round(bdv, places);
boolean match=
(math == decimal) && (decimal == string) &&
(decimal == round);
System.err.println(
"places= " +places +", szValue= " +szValue
+", value= " +value
+ " => " + math +", " +decimal +", " +string
+", " +round
+ (match ? "" : " whoops!"));
value += 0.0001;
decimalPart += 1;
bdv= bdv.add(spread);
}
}
static double math(double value, int places) {
double power= 1;
while (places-- > 0)
power *= 10.0;
return Math.round(value*power)/power;
}
static double decimal(double value, int places) {
BigDecimal decimal= new BigDecimal(value);
decimal= decimal.setScale(places, BigDecimal.ROUND_HALF_UP);
return decimal.doubleValue();
}
static double string(String value, int places) {
BigDecimal decimal= new BigDecimal(value);
decimal= decimal.setScale(places, BigDecimal.ROUND_HALF_UP);
return decimal.doubleValue();
}
static double round(BigDecimal value, int places) {
return value.setScale(places,
BigDecimal.ROUND_HALF_UP).doubleValue();
}
}
Which produces
the following:
places= 2,
szValue= 1.2345, value= 1.2345 => 1.23, 1.23, 1.23, 1.23
places= 2,
szValue= 1.2346, value= 1.2346 => 1.23, 1.23, 1.23, 1.23
places= 2,
szValue= 1.2347, value= 1.2347 => 1.23, 1.23, 1.23, 1.23
places= 2,
szValue= 1.2348, value= 1.2348 => 1.23, 1.23, 1.23, 1.23
places= 2,
szValue= 1.2349, value= 1.2348999999999999 => 1.23, 1.23, 1.23, 1.23
places= 2,
szValue= 1.2350, value= 1.2349999999999999 => 1.23, 1.23, 1.24, 1.24
whoops!
places= 2,
szValue= 1.2351, value= 1.2350999999999999 => 1.24, 1.24, 1.24, 1.24
places= 2,
szValue= 1.2352, value= 1.2351999999999999 => 1.24, 1.24, 1.24, 1.24
places= 2,
szValue= 1.2353, value= 1.2352999999999998 => 1.24, 1.24, 1.24, 1.24
places= 2,
szValue= 1.2354, value= 1.2353999999999998 => 1.24, 1.24, 1.24, 1.24
Which clearly
demonstrates that the problem is with floating point arithmetic (adding double
to double) not with java.math.BigDecimal.