Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NumberFormat rounding issue with Java 8 only

Tags:

java

java-8

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)
like image 678
AndrewBourgeois Avatar asked Jun 26 '14 09:06

AndrewBourgeois


People also ask

How do you round decimals in Java?

The decimal number can be rounded by the inbuilt format() method supported by Java. Syntax: System. out.

How do you NumberFormat in Java?

To format a number for the current Locale, use one of the factory class methods: myString = NumberFormat. getInstance(). format(myNumber);

How do you round a float in Java?

In order to round float and double numbers in Java, we use the java. lang. Math. round() method.


2 Answers

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.

like image 90
Holger Avatar answered Sep 19 '22 01:09

Holger


Oracle fixed this bug in Java 8 update 40

  • Bug 8039915 tracks the OpenJDK fix for Java 9
  • Bug 8061380 tracks the backport to OpenJDK 8
  • Public GA release of 8u40 with JDK 1.8.0_40-b25 on 03 March 2015 (release notes)

An unofficial runtime patch is available for earlier versions

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.

like image 32
William Price Avatar answered Sep 21 '22 01:09

William Price