How do I calculate correctly with Java’s BigDecimal?

Viewed 270

As I understood it, BigDecimal is there for properly dealing with numbers with fixed decimal places (i.e. money). Here is a little program I wrote:

import java.math.*;

public class Main {
    public static void main(String[] args) {
        BigDecimal a = new BigDecimal(11.22, new MathContext(2, RoundingMode.HALF_UP));
        System.out.println("a: " + a);
        BigDecimal b = a.add(new BigDecimal(0.04));
        System.out.println("b: " + b);
    }
}

I expected to see:

a: 11.22
b: 11.26

But what I got is:

a: 11
b: 11.040000000000000000832667268468867405317723751068115234375

I set a to have two decimal places, but it both does not print them, and it even forgets them and rounds to plain int. Why? b should add 0.04 and know from a to have two decimal places, too. This was at least what I expected.

How is this solved correctly using BigDecimal edit: with two double values as input and a known number of decimal places? [I.e., because an API does not give me anything else than these two doubles.] (I know there are other ways to reliably calculate with money (starting from calculating in cents with int), but that’s out of the scope of my question.)

3 Answers

Do not use the BigDecimal constructor with the double argument for exactly that reason:

  • 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.

Use the constructor with the String argument:

public static void main(String[] args) {
    BigDecimal a = new BigDecimal("11.22");
    System.out.println("a: " + a);
    BigDecimal b = a.add(new BigDecimal("0.04"));
    System.out.println("b: " + b);
}

This will generate the output as expected:

a: 11.22
b: 11.26

As @Progman also wrote, using the double constructor will create an exact decimal representation of the double value, and it is advised against in the documentation.

However the reason you are getting 11 instead of 11.22 is that you have set the precision of your MathContext to 2.
Precision is the number of digits used and not the number of decimal places. So if you changed your code to use 4 as the precision, then you would get the output

a: 11.22
b: 11.260000000000000000832667268468867405317723751068115234375

Still with the problem of the double value being used, but now with more decimal places!

The definition of the precision as the number of digits is in the MathContext class documentationdocumentation

The problem with using double values instead of strings as argument to BigDecimal is that they are mostly not exact and will result in repeating decimal expansions. In fact, only floating point numbers that are powers of 5 and or 2 in the denominator can be represented exactly as a float or double (e.g 1/20 = .05 1/4 = .25 , 1/5 = .2). That is because 5 and 2 are the only prime factors of base 10 and will return a finite (i.e. non-repeating) expansion of the fraction. Any other value will result in a repeating decimal expansion (e.g. 1/3 = .3333333333, 1/6 = .16666666), and this is what is causing your problem.

By specifying Strings instead of doubles, BigDecimal can operate on the expected desired value as opposed to the binary value to which it has limited or no control over.

Had your values been the following.

BigDecimal a = new BigDecimal(11.25);             
System.out.println("a: " + a);
BigDecimal b = a.add(new BigDecimal(.50));
System.out.println("b: " + b);

The output would have been

11.25
11.75

Because both fractional parts and the resulting sum have only 5 and 2 as their divisors.

For this reason, you should specify string representation of floating point values when initializing BigDecimal objects.

For more about internal representation of floating point numbers, check out IEEE 754

Related