Skip to Main Content

Rounding behavior JDK 8 vs. JDK 7

Karl EilebrechtDec 11 2015 — edited Dec 13 2015

UPDATE / RESOLVED

In the meantime I have found a good explanation given by the guys at Open JDK: https://bugs.openjdk.java.net/browse/JDK-8029896

The problem is indeed related to IEEE 754 number representation. Older JDKs like JDK 7 incorrectly implemented rounding double value representations close to ties (like 0.005).

All the <input> cases I listed earlier can be explained by adding System.outprintln((new BigDecimal(<input>)).toString()).

As you can see none of them can be represented exactly.

Here are some examples:

                                       

inputclosest representationJDK7 round HALF_EVENJDK8 round HALF_EVEN
0.0050.0050000000000000001040834085586084256647154688835144042968750.000.01
0.0150.014999999999999999444888487687421729788184165954589843750.020.01
0.0250.0250000000000000013877787807814456755295395851135253906250.020.03
0.0650.0650000000000000022204460492503130808472633361816406250.060.07
0.0750.074999999999999997224442438437108648940920829772949218750.080.07
0.0850.085000000000000006106226635438360972329974174499511718750.080.09
0.1550.15499999999999999888977697537484345957636833190917968750.160.15
0.1650.16500000000000000777156117237609578296542167663574218750.160.17
0.1750.1749999999999999888977697537484345957636833190917968750.180.17

If you now apply rounding with RoundingMode.HALF_EVEN it turns out that JDK7-behavior is wrong while JDK8 behavior is correct. This is the problem with IEEE 754, not with Java. Programs that now show different (unacceptable) behavior compared to JDK7 should be adjusted to using BigDecimal.

The change affects > 4% of all rounding operations from n decimals to n-1 decimals with n > 1.

------------------------ original post ---------------------------------------

I have read some articles about the improved rounding behavior of the NumberFormat class in JDK 8. This was for example discussed here: http://stackoverflow.com/questions/22797964/is-inconsistency-in-rounding-between-java-7-and-java-8-a-bug

As far as I understand, the new behavior shall better align to the well-known problems with IEEE 754 (https://en.wikipedia.org/wiki/IEEE_floating_point) regarding number representation (eliminate some errorneous corner cases). This should be a minor change, but it isn't somehow (see below).

I ran the code given below with JDK 7 (1.7.0_10) and JDK 8 (1.8.0_65) on Mac OS.

It simply compares rounding 1 decimal first performed on a regular double and then on the corresponding BigDecimal. The default RoundingMode is HALF_EVEN.

Surprising to me is that compared to JDK 7 the number of cases where the rounding mechanism of JDK 8 deviates from the JDK 7 behavior is much higher than I expected.

For JDK 7 all output is 0% (means same result for BigDecimal double). For JDK 8 results look different:

Range [0.0..100000.0), step=0.1 -> diff=0.00%, diffAtTie=0.00%

Range [0.00..10000.00), step=0.01 -> diff=4.00%, diffAtTie=4.00%

Range [0.000..1000.000), step=0.001 -> diff=4.80%, diffAtTie=4.80%

Range [0.0000..100.0000), step=0.0001 -> diff=4.96%, diffAtTie=4.96%

Range [0.00000..10.00000), step=0.00001 -> diff=4.99%, diffAtTie=4.99%

Range [0.000000..1.000000), step=0.000001 -> diff=5.00%, diffAtTie=5.00%

Range [0.0..-100000.0), step=-0.1 -> diff=0.00%, diffAtTie=0.00%

Range [0.00..-10000.00), step=-0.01 -> diff=4.00%, diffAtTie=4.00%

Range [0.000..-1000.000), step=-0.001 -> diff=4.80%, diffAtTie=4.80%

Range [0.0000..-100.0000), step=-0.0001 -> diff=4.96%, diffAtTie=4.96%

Range [0.00000..-10.00000), step=-0.00001 -> diff=4.99%, diffAtTie=4.99%

Range [0.000000..-1.000000), step=-0.000001 -> diff=5.00%, diffAtTie=5.00%

Here my questions:

  • Has this non-trivial change been announced anywhere (maybe explained in detail)?
  • Is this also surprising to you or am I on the wrong page?
  • Can anybody explain the error distribution phenomenon? Starting with the second decimal suddenly the error ratio grows from 4% up to 5 % - and it is always the "HALF" (tie) which gets rounded to the "wrong" side.

Thanks!

Karl

EDIT: As requested here some example values JDK7 (1.7.0_10) vs. JDK8 (1.8.0_65) on Mac OS El Capitan 10.11.2.

                                                                                                                                                                                                                                                                                                                                                                                                                                                             

For generating the small sample below I used a simpler generated source code, which can be found at the bottom of the table.

The sample only shows rounding from 3 to 2 decimals, but the phenomenon can be seen on any rounding from n to n-1 decimals with n > 1.

As you can see the values in the red columns are different compared to JDK7. This is what makes me nervous (round about 5%).

The BigDecimal columns are just for information. The difference between passing a double vs. passing a String to BigDecimal is plausible (precision loss).

                                                                                                                                                                                                                                                                                                                                                                                                                                                              

                                                                                                                                                                                                                                                                                                                                                                                                                                                               

inputJDK7 nf.format(
input)
JDK7   nf.format(
Double.
parseDoub
le(
"input"))
JDK7 nf.format(
new   BigDecimal(
input))
JDK7 nf.format(
new   BigDecimal(
"input"))
JDK8 nf.format(
input)
JDK8   nf.format(
Double.
parseDoub
le("input"))
JDK8 nf.format(
new   BigDecimal(
input))
JDK8 nf.format(
new   BigDecimal(
"input"))
0.0050.000.000.010.000.010.010.010.00
0.0150.020.020.010.020.010.010.010.02
0.0250.020.020.030.020.030.030.030.02
0.0650.060.060.070.060.070.070.070.06
0.0750.080.080.070.080.070.070.070.08
0.0850.080.080.090.080.090.090.090.08
0.1550.160.160.150.160.150.150.150.16
0.1650.160.160.170.160.170.170.170.16
0.1750.180.180.170.180.170.170.170.18
0.2150.220.220.210.220.210.210.210.22
0.2250.220.220.230.220.230.230.230.22
0.2350.240.240.230.240.230.230.230.24
0.2650.260.260.270.260.270.270.270.26
0.2950.300.300.290.300.290.290.290.30
0.3250.320.320.330.320.330.330.330.32
0.3550.360.360.350.360.350.350.350.36
0.3850.380.380.390.380.390.390.390.38
0.4050.400.400.410.400.410.410.410.40
0.4150.420.420.410.420.410.410.410.42
0.4350.440.440.430.440.430.430.430.44
0.4450.440.440.450.440.450.450.450.44
0.4650.460.460.470.460.470.470.470.46
0.4750.480.480.470.480.470.470.470.48
0.4950.500.500.490.500.490.490.490.50
0.5050.500.500.510.500.510.510.510.50
0.5250.520.520.530.520.530.530.530.52
0.5450.540.540.550.540.550.550.550.54
0.5750.580.580.570.580.570.570.570.58
0.5950.600.600.590.600.590.590.590.60
0.6150.620.620.610.620.610.610.610.62
0.6450.640.640.650.640.650.650.650.64
0.6650.660.660.670.660.670.670.670.66
0.6850.680.680.690.680.690.690.690.68
0.6950.700.700.690.700.690.690.690.70
0.7150.720.720.710.720.710.710.710.72
0.7350.740.740.730.740.730.730.730.74
0.7650.760.760.770.760.770.770.770.76
0.7850.780.780.790.780.790.790.790.78
0.8050.800.800.810.800.810.810.810.80
0.8150.820.820.810.820.810.810.810.82
0.8350.840.840.830.840.830.830.830.84
0.8550.860.860.850.860.850.850.850.86
0.8850.880.880.890.880.890.890.890.88
0.9050.900.900.910.900.910.910.910.90
0.9250.920.920.930.920.930.930.930.92
0.9550.960.960.950.960.950.950.950.96
0.9750.980.980.970.980.970.970.970.98
0.9951.001.000.991.000.990.990.991.00
1.0151.021.021.011.021.011.011.011.02

NumberFormat nf = NumberFormat.getInstance(Locale.US);

assertEquals(RoundingMode.HALF_EVEN, nf.getRoundingMode());

nf.setGroupingUsed(false);

nf.setMaximumFractionDigits(2);

nf.setMinimumFractionDigits(2);

System.out.println("0.005;" + nf.format(0.005) + ";" + nf.format(Double.parseDouble("0.005")) + ";"

+ nf.format(new BigDecimal(0.005)) + ";" + nf.format(new BigDecimal("0.005")));

System.out.println("0.015;" + nf.format(0.015) + ";" + nf.format(Double.parseDouble("0.015")) + ";"

+ nf.format(new BigDecimal(0.015)) + ";" + nf.format(new BigDecimal("0.015")));

System.out.println("0.025;" + nf.format(0.025) + ";" + nf.format(Double.parseDouble("0.025")) + ";"

+ nf.format(new BigDecimal(0.025)) + ";" + nf.format(new BigDecimal("0.025")));

System.out.println("0.065;" + nf.format(0.065) + ";" + nf.format(Double.parseDouble("0.065")) + ";"

+ nf.format(new BigDecimal(0.065)) + ";" + nf.format(new BigDecimal("0.065")));

System.out.println("0.075;" + nf.format(0.075) + ";" + nf.format(Double.parseDouble("0.075")) + ";"

+ nf.format(new BigDecimal(0.075)) + ";" + nf.format(new BigDecimal("0.075")));

System.out.println("0.085;" + nf.format(0.085) + ";" + nf.format(Double.parseDouble("0.085")) + ";"

+ nf.format(new BigDecimal(0.085)) + ";" + nf.format(new BigDecimal("0.085")));

System.out.println("0.155;" + nf.format(0.155) + ";" + nf.format(Double.parseDouble("0.155")) + ";"

+ nf.format(new BigDecimal(0.155)) + ";" + nf.format(new BigDecimal("0.155")));

System.out.println("0.165;" + nf.format(0.165) + ";" + nf.format(Double.parseDouble("0.165")) + ";"

+ nf.format(new BigDecimal(0.165)) + ";" + nf.format(new BigDecimal("0.165")));

System.out.println("0.175;" + nf.format(0.175) + ";" + nf.format(Double.parseDouble("0.175")) + ";"

+ nf.format(new BigDecimal(0.175)) + ";" + nf.format(new BigDecimal("0.175")));

System.out.println("0.215;" + nf.format(0.215) + ";" + nf.format(Double.parseDouble("0.215")) + ";"

+ nf.format(new BigDecimal(0.215)) + ";" + nf.format(new BigDecimal("0.215")));

System.out.println("0.225;" + nf.format(0.225) + ";" + nf.format(Double.parseDouble("0.225")) + ";"

+ nf.format(new BigDecimal(0.225)) + ";" + nf.format(new BigDecimal("0.225")));

System.out.println("0.235;" + nf.format(0.235) + ";" + nf.format(Double.parseDouble("0.235")) + ";"

+ nf.format(new BigDecimal(0.235)) + ";" + nf.format(new BigDecimal("0.235")));

System.out.println("0.265;" + nf.format(0.265) + ";" + nf.format(Double.parseDouble("0.265")) + ";"

+ nf.format(new BigDecimal(0.265)) + ";" + nf.format(new BigDecimal("0.265")));

System.out.println("0.295;" + nf.format(0.295) + ";" + nf.format(Double.parseDouble("0.295")) + ";"

+ nf.format(new BigDecimal(0.295)) + ";" + nf.format(new BigDecimal("0.295")));

System.out.println("0.325;" + nf.format(0.325) + ";" + nf.format(Double.parseDouble("0.325")) + ";"

+ nf.format(new BigDecimal(0.325)) + ";" + nf.format(new BigDecimal("0.325")));

System.out.println("0.355;" + nf.format(0.355) + ";" + nf.format(Double.parseDouble("0.355")) + ";"

+ nf.format(new BigDecimal(0.355)) + ";" + nf.format(new BigDecimal("0.355")));

System.out.println("0.385;" + nf.format(0.385) + ";" + nf.format(Double.parseDouble("0.385")) + ";"

+ nf.format(new BigDecimal(0.385)) + ";" + nf.format(new BigDecimal("0.385")));

System.out.println("0.405;" + nf.format(0.405) + ";" + nf.format(Double.parseDouble("0.405")) + ";"

+ nf.format(new BigDecimal(0.405)) + ";" + nf.format(new BigDecimal("0.405")));

System.out.println("0.415;" + nf.format(0.415) + ";" + nf.format(Double.parseDouble("0.415")) + ";"

+ nf.format(new BigDecimal(0.415)) + ";" + nf.format(new BigDecimal("0.415")));

System.out.println("0.435;" + nf.format(0.435) + ";" + nf.format(Double.parseDouble("0.435")) + ";"

+ nf.format(new BigDecimal(0.435)) + ";" + nf.format(new BigDecimal("0.435")));

System.out.println("0.445;" + nf.format(0.445) + ";" + nf.format(Double.parseDouble("0.445")) + ";"

+ nf.format(new BigDecimal(0.445)) + ";" + nf.format(new BigDecimal("0.445")));

System.out.println("0.465;" + nf.format(0.465) + ";" + nf.format(Double.parseDouble("0.465")) + ";"

+ nf.format(new BigDecimal(0.465)) + ";" + nf.format(new BigDecimal("0.465")));

System.out.println("0.475;" + nf.format(0.475) + ";" + nf.format(Double.parseDouble("0.475")) + ";"

+ nf.format(new BigDecimal(0.475)) + ";" + nf.format(new BigDecimal("0.475")));

System.out.println("0.495;" + nf.format(0.495) + ";" + nf.format(Double.parseDouble("0.495")) + ";"

+ nf.format(new BigDecimal(0.495)) + ";" + nf.format(new BigDecimal("0.495")));

System.out.println("0.505;" + nf.format(0.505) + ";" + nf.format(Double.parseDouble("0.505")) + ";"

+ nf.format(new BigDecimal(0.505)) + ";" + nf.format(new BigDecimal("0.505")));

System.out.println("0.525;" + nf.format(0.525) + ";" + nf.format(Double.parseDouble("0.525")) + ";"

+ nf.format(new BigDecimal(0.525)) + ";" + nf.format(new BigDecimal("0.525")));

System.out.println("0.545;" + nf.format(0.545) + ";" + nf.format(Double.parseDouble("0.545")) + ";"

+ nf.format(new BigDecimal(0.545)) + ";" + nf.format(new BigDecimal("0.545")));

System.out.println("0.575;" + nf.format(0.575) + ";" + nf.format(Double.parseDouble("0.575")) + ";"

+ nf.format(new BigDecimal(0.575)) + ";" + nf.format(new BigDecimal("0.575")));

System.out.println("0.595;" + nf.format(0.595) + ";" + nf.format(Double.parseDouble("0.595")) + ";"

+ nf.format(new BigDecimal(0.595)) + ";" + nf.format(new BigDecimal("0.595")));

System.out.println("0.615;" + nf.format(0.615) + ";" + nf.format(Double.parseDouble("0.615")) + ";"

+ nf.format(new BigDecimal(0.615)) + ";" + nf.format(new BigDecimal("0.615")));

System.out.println("0.645;" + nf.format(0.645) + ";" + nf.format(Double.parseDouble("0.645")) + ";"

+ nf.format(new BigDecimal(0.645)) + ";" + nf.format(new BigDecimal("0.645")));

System.out.println("0.665;" + nf.format(0.665) + ";" + nf.format(Double.parseDouble("0.665")) + ";"

+ nf.format(new BigDecimal(0.665)) + ";" + nf.format(new BigDecimal("0.665")));

System.out.println("0.685;" + nf.format(0.685) + ";" + nf.format(Double.parseDouble("0.685")) + ";"

+ nf.format(new BigDecimal(0.685)) + ";" + nf.format(new BigDecimal("0.685")));

System.out.println("0.695;" + nf.format(0.695) + ";" + nf.format(Double.parseDouble("0.695")) + ";"

+ nf.format(new BigDecimal(0.695)) + ";" + nf.format(new BigDecimal("0.695")));

System.out.println("0.715;" + nf.format(0.715) + ";" + nf.format(Double.parseDouble("0.715")) + ";"

+ nf.format(new BigDecimal(0.715)) + ";" + nf.format(new BigDecimal("0.715")));

System.out.println("0.735;" + nf.format(0.735) + ";" + nf.format(Double.parseDouble("0.735")) + ";"

+ nf.format(new BigDecimal(0.735)) + ";" + nf.format(new BigDecimal("0.735")));

System.out.println("0.765;" + nf.format(0.765) + ";" + nf.format(Double.parseDouble("0.765")) + ";"

+ nf.format(new BigDecimal(0.765)) + ";" + nf.format(new BigDecimal("0.765")));

System.out.println("0.785;" + nf.format(0.785) + ";" + nf.format(Double.parseDouble("0.785")) + ";"

+ nf.format(new BigDecimal(0.785)) + ";" + nf.format(new BigDecimal("0.785")));

System.out.println("0.805;" + nf.format(0.805) + ";" + nf.format(Double.parseDouble("0.805")) + ";"

+ nf.format(new BigDecimal(0.805)) + ";" + nf.format(new BigDecimal("0.805")));

System.out.println("0.815;" + nf.format(0.815) + ";" + nf.format(Double.parseDouble("0.815")) + ";"

+ nf.format(new BigDecimal(0.815)) + ";" + nf.format(new BigDecimal("0.815")));

System.out.println("0.835;" + nf.format(0.835) + ";" + nf.format(Double.parseDouble("0.835")) + ";"

+ nf.format(new BigDecimal(0.835)) + ";" + nf.format(new BigDecimal("0.835")));

System.out.println("0.855;" + nf.format(0.855) + ";" + nf.format(Double.parseDouble("0.855")) + ";"

+ nf.format(new BigDecimal(0.855)) + ";" + nf.format(new BigDecimal("0.855")));

System.out.println("0.885;" + nf.format(0.885) + ";" + nf.format(Double.parseDouble("0.885")) + ";"

+ nf.format(new BigDecimal(0.885)) + ";" + nf.format(new BigDecimal("0.885")));

System.out.println("0.905;" + nf.format(0.905) + ";" + nf.format(Double.parseDouble("0.905")) + ";"

+ nf.format(new BigDecimal(0.905)) + ";" + nf.format(new BigDecimal("0.905")));

System.out.println("0.925;" + nf.format(0.925) + ";" + nf.format(Double.parseDouble("0.925")) + ";"

+ nf.format(new BigDecimal(0.925)) + ";" + nf.format(new BigDecimal("0.925")));

System.out.println("0.955;" + nf.format(0.955) + ";" + nf.format(Double.parseDouble("0.955")) + ";"

+ nf.format(new BigDecimal(0.955)) + ";" + nf.format(new BigDecimal("0.955")));

System.out.println("0.975;" + nf.format(0.975) + ";" + nf.format(Double.parseDouble("0.975")) + ";"

+ nf.format(new BigDecimal(0.975)) + ";" + nf.format(new BigDecimal("0.975")));

System.out.println("0.995;" + nf.format(0.995) + ";" + nf.format(Double.parseDouble("0.995")) + ";"

+ nf.format(new BigDecimal(0.995)) + ";" + nf.format(new BigDecimal("0.995")));

System.out.println("1.015;" + nf.format(1.015) + ";" + nf.format(Double.parseDouble("1.015")) + ";"

+ nf.format(new BigDecimal(1.015)) + ";" + nf.format(new BigDecimal("1.015")));


package comp7;

import static org.junit.Assert.assertEquals;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.NumberFormat;
import java.util.Locale;

import org.junit.Test;

public class JavaRoundTest {

    @Test
    public void testJavaNumberFormatRounding() throws Exception {
        int total = 1_000_000;
        long time = System.currentTimeMillis();
        for (boolean negate : new boolean[] { false, true }) {
            for (int decimals = 1; decimals < 7; decimals++) {
                performRun(total, negate, decimals);
            }
        }
        System.out.println("Finished after " + (System.currentTimeMillis() - time) + " ms");

    }

    private void performRun(int upperBound, boolean negate, int decimals) {
        NumberFormat testedNumberFormat = NumberFormat.getInstance(Locale.US);
        assertEquals(RoundingMode.HALF_EVEN, testedNumberFormat.getRoundingMode());
        testedNumberFormat.setGroupingUsed(false);
        testedNumberFormat.setMaximumFractionDigits(decimals - 1);
        testedNumberFormat.setMinimumFractionDigits(decimals - 1);
        DiffCountResult dcr = computeRun(testedNumberFormat, upperBound, negate, decimals);
        double diffShare = ((double) dcr.diffCount / upperBound);
        double diffAtTieShare = ((double) dcr.diffAtTieCount / upperBound);
        NumberFormat percNumberFormat = NumberFormat.getPercentInstance(Locale.US);
        percNumberFormat.setMaximumFractionDigits(2);
        percNumberFormat.setMinimumFractionDigits(2);
        percNumberFormat.setGroupingUsed(true);

        System.out.println("Range [" + makeDecimalString(0, negate, decimals) + ".."
                + makeDecimalString(upperBound, negate, decimals) + "), step=" + makeDecimalString(1, negate, decimals)
                + " -> diff=" + percNumberFormat.format(new BigDecimal(diffShare)) + ", diffAtTie="
                + percNumberFormat.format(new BigDecimal(diffAtTieShare)));
    }

    private DiffCountResult computeRun(NumberFormat nf, int upperBound, boolean negate, int decimals) {
        DiffCountResult dcr = new DiffCountResult();
        for (int i = 0; i < upperBound; i++) {
            String sNum = makeDecimalString(i, negate, decimals);
            String sRoundedDouble = nf.format(Double.parseDouble(sNum));
            String sRoundedBigDecimal = nf.format(new BigDecimal(sNum));
            if (!sRoundedDouble.equals(sRoundedBigDecimal)) {
                dcr.diffCount++;
                if (sNum.charAt(sNum.length() - 1) == '5') {
                    dcr.diffAtTieCount++;
                }
            }
        }
        return dcr;
    }

    static String makeDecimalString(int sourceValue, boolean negate, int decimals) {
        String res = "0.";
        String sourceString = "" + sourceValue;
        int sourceLen = sourceString.length();
        if (sourceLen <= decimals) {
            int padLen = decimals - sourceLen;
            for (int i = 0; i < padLen; i++) {
                res = res + "0";
            }
            res = res + sourceString;
        } else {
            int dotPos = sourceLen - decimals;
            res = sourceString.substring(0, dotPos) + "." + sourceString.substring(dotPos);
        }
        if (sourceValue > 0 && negate) {
            res = "-" + res;
        }
        return res;
    }

    static class DiffCountResult {
        int diffCount = 0;
        int diffAtTieCount = 0;
    }

}
Comments
Post Details
Added on Dec 11 2015
2 comments
2,360 views