BigDecimal gives unexpected results for numbers larger than 7 or 0 decimal numbers

Tags: , ,



While trying to calculate a ratio of the volume of 2 objects, I noticed some weirdness in the calculation, here is a sample you can run for yourself:

public class TestApplication {

  public static void main(String[] args) {
    BigDecimal first = BigDecimal.valueOf(21099000.0);
    BigDecimal second = BigDecimal.valueOf(13196000.0);

    System.out.println("First: " + first);
    System.out.println("Second: " + second);
    System.out.println("Division: " + first.divide(second, RoundingMode.HALF_UP).doubleValue());
  }
}

And the result is:

First: 2.1099E+7
Second: 1.3196E+7
Division: 0.0

There are 3 ways I could make it give me the expected result

1. If I change the decimal part from 0 to 1 (or any non-0 number):

First: 21099000.1
Second: 13196000.1
Division: 1.6

2. If I divide the numbers beforehand (make them 7 digit numbers instead of 8):

First: 2109900.0
Second: 1319600.0
Division: 1.6

3. If I specify a scale doing division (first.divide(second, 0, RoundingMode.HALF_UP):

First: 2.1099E+7
Second: 1.3196E+7
Division: 2.0

I thought that BigDecimal is backed by an integer and the numbers I used are way below 2 billion. Can anyone explain what makes these 3 cases different from the original result?

Answer

As per the documentation, divide​(BigDecimal divisor, RoundingMode roundingMode) returns a BigDecimal whose value is (this / divisor), and whose scale is this.scale().

Why did you get the expected result for 21099000.1 / 13196000.1?

Check the result of the following code:

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

public class Main {
    public static void main(String[] args) {
        BigDecimal first = BigDecimal.valueOf(21099000.1);
        BigDecimal second = BigDecimal.valueOf(13196000.1);
        System.out.println("First: " + first + ", Scale: " + first.scale());
        System.out.println("Second: " + second + ", Scale: " + second.scale());

        // 21099000.0 / 13196000.0 = 1.5988936041
        System.out.println(BigDecimal.valueOf(1.5988936041).setScale(first.scale(), RoundingMode.HALF_UP));
    }
}

Output:

First: 21099000.1, Scale: 1
Second: 13196000.1, Scale: 1
1.6

As you can see, JVM has chosen the scale as 1 for first and thus the result of divide (which is 1.5988936041) is also set as 1 which is equal to 1.6 with RoundingMode.HALF_UP.

Why did you not get the expected result for 21099000.0 / 13196000.0?

Check the result of the following code:

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

public class Main {
    public static void main(String[] args) {
        BigDecimal first = BigDecimal.valueOf(21099000.0);
        BigDecimal second = BigDecimal.valueOf(13196000.0);
        System.out.println("First: " + first + ", Scale: " + first.scale());
        System.out.println("Second: " + second + ", Scale: " + second.scale());

        // 21099000.0 / 13196000.0 = 1.5988936041
        System.out.println(BigDecimal.valueOf(1.5988936041).setScale(first.scale(), RoundingMode.HALF_UP));
    }
}

Output:

First: 2.1099E+7, Scale: -3
Second: 1.3196E+7, Scale: -3
0E+3

As you can see, JVM has chosen the scale as -3 for first and thus the result of divide (which is 1.5988936041) is also set as -3 which is equal to 0 (or 0E+3) with RoundingMode.HALF_UP.

How can I change this behavior?

As mentioned in the documentation, scale of the division is set as this.scale() which means if you set the scale of first to 1, you can get the expected result.

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

public class Main {
    public static void main(String[] args) {
        BigDecimal first = BigDecimal.valueOf(21099000.0).setScale(1);
        BigDecimal second = BigDecimal.valueOf(13196000.0);
        System.out.println("First: " + first + ", Scale: " + first.scale());
        System.out.println("Second: " + second + ", Scale: " + second.scale());
        System.out.println("Division: " + first.divide(second, RoundingMode.HALF_UP).doubleValue());
    }
}

Output:

First: 21099000.0, Scale: 1
Second: 1.3196E+7, Scale: -3
Division: 1.6

What is the most common way?

The last example worked well and there is no problem using it. However, the most common way is to use divide​(BigDecimal divisor, int scale, RoundingMode roundingMode).

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

public class Main {
    public static void main(String[] args) {
        BigDecimal first = BigDecimal.valueOf(21099000.0);
        BigDecimal second = BigDecimal.valueOf(13196000.0);
        System.out.println("First: " + first + ", Scale: " + first.scale());
        System.out.println("Second: " + second + ", Scale: " + second.scale());
        System.out.println("Division: " + first.divide(second, 1, RoundingMode.HALF_UP).doubleValue());
    }
}

Output:

First: 2.1099E+7, Scale: -3
Second: 1.3196E+7, Scale: -3
Division: 1.6


Source: stackoverflow