Reputation: 349
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
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()
.
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
.
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
.
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
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
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 isthis.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