java rounding errors

eon

2[H]4U
Joined
Oct 11, 2003
Messages
2,218
I needed to round floating point numbers half up to the nearest 1/100 decimal place:
123.045 = 123.05
Plus I need to eliminate any floating point representation errors. I'm currently doing this:

double val = x*y;
val= new BigDecimal(val, MathContext.DECIMAL64).setScale(2, BigDecimal.ROUND_HALF_UP)

This works for all my test cases but my confidence in this has dropped after realizing Mathcontexts DECIMAL128 and UNLIMITED inexplicably didnt evaluate one test case correctly.
Is there a more fail proof method to do this? thanks
 
I'm definatly no expert on the subject but I have a couple questions on that code that might help you out....or maybe not.

Assuming the primitive type double won't work for what you need, which is why you're using BigDecimal I'm guessing, why do you do the math and then assign it to a double, and then convert it to a BigDecimal? Wouldn't the BigDecimal created with the double value then contain the same loss in precision that the double had? I wouldn't think it would recover the precision if its already lost.

Also, I don't know if that code compiles for you but Eclipse doesn't like it. The only way I could see it working for you is if the BigDecimal returned by setScale was auto-unboxed into a double type, because "val" is still a double.

I don't have time to mess around with it now even though I'd like to, and I've never used the BigDecimal class, just looked it up on javadocs. This is the first thing that I thought of though:

Code:
BigDecimal val = new BigDecimal(x, Mathcontext.DECIMAL64).setScale(2, BigDecimal.ROUND_HALF_UP);

BigDecimal yourY = new BigDecimal(y, Mathcontext.DECIMAL64).setScale(2, BigDecimal.ROUND_HALF_UP);

val = val.multiply(yourY);


Oh, and from the javadocs...Preferred Scale of Result for Multiplication = multiplier.scale() + multiplicand.scale(), so the scale of val should be whatever the scale of x is + whatever the scale of y is.

EDIT: NM about the scale thing. The BigDecimal returned by .multiply() has a modified scale to fit what I said above.
 
Last edited:
it works so I assume assigning it to a double first does not strip away any precision needed for the BigDecimal constructor to correct the floating point representation
I did it like that because its a class variable and didnt want to mess anything up from changing it (most of the code isnt mine)
yes forgot to mention, this isnt exactly Java, its groovy on grails. When I was doing tests in Java I also had compilation problems of converting BigDecimal to double.
 
Ah okay. I kinda figured you wouldn't be posting something with compile time errors after saying that it passes most of your test cases. Thought I'd give it a try anyways just in case.
 
it looks like your suggestion of using the multiply function works. However this would mean going through a ton of code instead of just changing a few lines in the base class how I currently have.

here is what I dont understand of what I'm currently doing:

BigDecimal t = new BigDecimal(1.505, MathContext.DECIMAL128).setScale(2, BigDecimal.ROUND_HALF_UP);
System.out.println(t);

It works fine with Decimal32 and Decimal64 evaluating to 1.51 while Decimal128 and unlimited evaluates it to 1.50. Decimal32 breaks when dealing with a number greater than 7 digits. Decimal64 so far works for all my tests, (I should never need more than 16 digits) but its unsettling that Decimal128 is not working when it looks like it should.
 
It works fine with Decimal32 and Decimal64 evaluating to 1.51 while Decimal128 and unlimited evaluates it to 1.50. Decimal32 breaks when dealing with a number greater than 7 digits. Decimal64 so far works for all my tests, (I should never need more than 16 digits) but its unsettling that Decimal128 is not working when it looks like it should.

It is working as intended.

new BigDecimal(1.505, MathContext.DECIMAL128);

Evaluates to 1.504999999999999893418589635984972

I think perhaps what you intended to do was this

Code:
BigDecimal t = new BigDecimal("1.505", MathContext.DECIMAL128).setScale(2, BigDecimal.ROUND_HALF_UP);

Notice the quotes around 1.505. By passing in a String you are telling BigDecimal I want the exact value represented by the String.

When you give it the double 1.505 you are telling it to represent the double closest to 1.505. This occurs because 1.505 can not be exactly represented in double precision floating point.


This is all covered in the BigDecimal javadocs. http://java.sun.com/javase/6/docs/api/java/math/BigDecimal.html#BigDecimal(double)
 
ah ok I see now
well Strings arent really an option because I'm already dealing with doubles. My task is basically to provide a method that you pass in a double and returns a double (or BigDecimal, I'm able to convert it to double without problems) rounded correctly to the 1/100ths place

but you gave me an idea... first round it to the 1/1000 place, then round again to 1/100 place:

BigDecimal t = new BigDecimal(1.505, MathContext.DECIMAL128).setScale(3, BigDecimal.ROUND_HALF_UP); //t=1.505
t = t.setScale(2, BigDecimal.ROUND_HALF_UP); //t=1.51

shouldnt that take care of any floating point representation quirks?
 
shouldnt that take care of any floating point representation quirks?

No guarantees. That kind of approach will work, until you find a test case that doesn't. (Similar to what you just encountered).

There is no real easy way to get around this if you want to take a double as an input.
 
ya i just realized one...
1.0449....
should be 1.04 but this method would do:
1.045 -> 1.05
 
I think the appropriate way to do this would be

Code:
double val = x*y;
val= new BigDecimal(val).setScale(2, BigDecimal.ROUND_HALF_UP)

And document for the caller that the exact double binary floating point representation is used when performing the math.
 
Another option is to convert the double to a String with Double.toString() before passing it in to the BigInteger. This would at least make code or inputs using a literal double (1.505 in your code for instance) work correctly.
 
ya that was just an example, there likely wont be any literals passed in
 
well now that I think about it, I'm pretty sure using Decimal64 will always round to the nearest 1/100 with the desired result as long as the value is 16 digits or less.
 
If there are no literals then you don't have a problem, you should just let BigDecimal choose the precision appropriate (by not specifying a precision) then use a DecimalFormat for display without changing the underlying data, that way if the value is used in further calculations you don't introduce rounding errors.
 
Back
Top