Can somebody explain to me why the following code:
public class Test {
public static void main(String... args) {
round(6.2088, 3);
round(6.2089, 3);
}
private static void round(Double num, int numDecimal) {
System.out.println("BigDecimal: " + new BigDecimal(num).toString());
// Use Locale.ENGLISH for '.' as decimal separator
NumberFormat nf = NumberFormat.getInstance(Locale.ENGLISH);
nf.setGroupingUsed(false);
nf.setMaximumFractionDigits(numDecimal);
nf.setRoundingMode(RoundingMode.HALF_UP);
if(Math.abs(num) - Math.abs(num.intValue()) != 0){
nf.setMinimumFractionDigits(numDecimal);
}
System.out.println("Formatted: " + nf.format(num));
}
}
gives the following output?
[me@localhost trunk]$ java Test
BigDecimal: 6.208800000000000096633812063373625278472900390625
Formatted: 6.209
BigDecimal: 6.208899999999999863575794734060764312744140625
Formatted: 6.208
In case you don't see it: "6.2089" rounded to 3 digits gives the output "6.208" while "6.2088" gives "6.209" as output. Less is more?
The results were good when using Java 5, 6 or 7 but this Java 8 gives me this strange output. Java version:
[me@localhost trunk]$ java -version
java version "1.8.0_05"
Java(TM) SE Runtime Environment (build 1.8.0_05-b13)
Java HotSpot(TM) Server VM (build 25.5-b02, mixed mode)
EDIT: this is Java 7's output:
[me@localhost trunk]$ java Test
BigDecimal: 6.208800000000000096633812063373625278472900390625
Formatted: 6.209
BigDecimal: 6.208899999999999863575794734060764312744140625
Formatted: 6.209
Java 7 version:
[me@localhost trunk]$ java -version
java version "1.7.0_51"
Java(TM) SE Runtime Environment (build 1.7.0_51-b13)
Java HotSpot(TM) Server VM (build 24.51-b03, mixed mode)
The decimal number can be rounded by the inbuilt format() method supported by Java. Syntax: System. out.
To format a number for the current Locale, use one of the factory class methods: myString = NumberFormat. getInstance(). format(myNumber);
In order to round float and double numbers in Java, we use the java. lang. Math. round() method.
I could track down this issue to class java.text.DigitList
line 522.
The situation is that it thinks the decimal digits 6.0289
are already rounded (which is correct when comparing to the equivalent BigDecimal
representation 6.208899…
) and decides to not round up again. The problem is that this decision makes sense only in the case that the digit resulting from rounding up is 5
, not when it is bigger than 5
. Note how the code for HALF_DOWN
correctly differentiates between the digit=='5'
and digit>'5'
case.
This is a bug, obviously, and a strange one given the fact that the code for doing similar right (just for the other direction) is right below the broken one.
case HALF_UP:
if (digits[maximumDigits] >= '5') {
// We should not round up if the rounding digits position is
// exactly the last index and if digits were already rounded.
if ((maximumDigits == (count - 1)) &&
(alreadyRounded))
return false;
// Value was exactly at or was above tie. We must round up.
return true;
}
break;
case HALF_DOWN:
if (digits[maximumDigits] > '5') {
return true;
} else if (digits[maximumDigits] == '5' ) {
if (maximumDigits == (count - 1)) {
// The rounding position is exactly the last index.
if (allDecimalDigits || alreadyRounded)
/* FloatingDecimal rounded up (value was below tie),
* or provided the exact list of digits (value was
* an exact tie). We should not round up, following
* the HALF_DOWN rounding rule.
*/
return false;
else
// Value was above the tie, we must round up.
return true;
}
// We must round up if it gives a non null digit after '5'.
for (int i=maximumDigits+1; i<count; ++i) {
if (digits[i] != '0') {
return true;
}
}
}
break;
The reason why this doesn’t happen to the other number is that 6.2088
is not the result of rounding up (again, compare to the BigDecimal
output 6.208800…
). So in this case it will round up.
1.8.0_40-b25
on 03 March 2015 (release notes)Thanks to the research in Holger's answer, I was able to develop a runtime patch and my employer has released it free under the terms of the GPLv2 license with Classpath Exception1 (the same as the OpenJDK source code).
The patch project and source code is hosted on GitHub with more details about this bug as well as links to downloadable binaries. The patch makes no modifications to the installed Java files on disk, and it should be safe for use on all versions of Oracle Java >= 6 and at least through version 8 (including fixed versions).
When the patch detects bytecode signatures that suggest the bug is present, it replaces the HALF_UP
switch case with a revised implementation:
if (digits[maximumDigits] > '5') {
return true;
} else if (digits[maximumDigits] == '5') {
return maximumDigits != (count - 1)
|| allDecimalDigits
|| !alreadyRounded;
}
// else
return false; // in original switch(), was: break;
1 I am not a lawyer, but my understanding is that GPLv2 w/ CPE allows commercial use in binary form without GPL applying to the combined work.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With