3

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?

Viorel
  • 337
  • 2
  • 12
  • 7
    **Do not use any constructor of BigDecimal that uses float/double unless you can not avoid it!** Always use the BigDecimal constructor that takes a String, because otherwise you will still have the problem of having to deal with the limited precision of double/float that you are actually trying to avoid by using BigDecimal. Change your variable declarations to `BigDecimal first = new BigDecimal("21099000.0");` And `BigDecimal second = new BigDecimal("13196000.0");` and you will see the calculations will be what you expect them to be. – OH GOD SPIDERS Jun 02 '20 at 09:42
  • 1
    @OHGODSPIDERS That caveat and the prior duplicates apply to FP numbers with fractions,. The problem here is simply a scale problem: nothing to do with how the BDs are initialized. VTR. – user207421 Jun 02 '20 at 10:02
  • @OHGODSPIDERS That does solve the issue, indeed, which means it is a 4th solution to the issue. But I still do not understand the different behaviours. – Viorel Jun 02 '20 at 10:08
  • 1
    @Viorel double and float can not represent every number with 100% precision. As soon as you use a double/float in java code you will have to deal with this problem and no other class including BigDecimal can ever fix this missing precision. So the only solution to avoid this problem is to not use float/double data types at all, which is exactly why BigDecimal has a constructor that takes a String and recommends using that constructor. – OH GOD SPIDERS Jun 02 '20 at 10:13
  • @OHGODSPIDERS Thank you for your answers! So in the original case, the result of the division is 0 because the factors of those 2 numbers are not actually -7 because of the lack of precision? I did notice the factor of the result in one of the cases was -3 for some reason. But then why would it still work for scenario #2, where I simply removed a 0, while keeping the decimal? – Viorel Jun 02 '20 at 10:31

2 Answers2

1

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
Arvind Kumar Avinash
  • 71,965
  • 6
  • 74
  • 110
0

Paraphrasing the BigDecimal specification:

A BigDecimal consists of an arbitrary precision integer unscaled value and a 32-bit integer scale.

The value of the number represented by the BigDecimal is therefore (unscaledValue × 10-scale).

Also, regarding the divide method:

Returns a BigDecimal whose value is (this / divisor), and whose scale is this.scale()

You can verify the unscaledValue and scale of a number with unscaledValue and scale methods:

var first = BigDecimal.valueOf(21099000.0);
// ==> 2.1099E+7

first.unscaledValue()
// ==> 21099

first.scale()
// ==> -3

It turns out, the unscaledValue is 21099 and scale -3. This number is mathematically equal to 21099*10^3 = 21099000 as you expect, BUT since it has a scale of -3 that means that first.divide(second, RoundingMode) will also have a scale of -3.

In other words, the result of divide() must be rounded to a multiple of 1000.

The true value of the division is approximately 1.599. According to the rounding mode RoundingMode.HALF_UP it must be rounded down, to 0.

To get different behavior you must either pass a custom scale value to divide, or change the scale of first. For example you can change the scale:

first = first.setScale(2)

Or you can create the numbers in a way that guarantees a set scale:

first = new BigDecimal("21099000"); // sets scale to 0

    or

first = new BigDecimal(21099000); // sets scale to 0
Community
  • 1
  • 1
Joni
  • 108,737
  • 14
  • 143
  • 193