Viorel
Viorel

Reputation: 349

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

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?

Upvotes: 3

Views: 2087

Answers (2)

Arvind Kumar Avinash
Arvind Kumar Avinash

Reputation: 79005

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

Upvotes: 1

Joni
Joni

Reputation: 111219

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

Upvotes: 0

Related Questions