36

I'm seeing inconsistencies in rounding in the DecimalFormat class between java 7 and java 8. Here is my test case:

DecimalFormatTest.java

import java.text.DecimalFormat;

public class DecimalFormatTest {
    public static void main(String[] args) {
        DecimalFormat format = (DecimalFormat) DecimalFormat.getInstance();
        format.setDecimalSeparatorAlwaysShown(true);
        format.setMinimumFractionDigits(1);
        format.setMaximumFractionDigits(1);
        System.out.println(format.format(83.65));
    }
}

In Java(TM) SE Runtime Environment (build 1.7.0_51-b13) the output is:

83.6

In Java(TM) SE Runtime Environment (build 1.8.0-b132) the output is:

83.7

Is this a regression bug? Or were the rounding rules changed with the release of Java 8?

Asaph
  • 159,146
  • 25
  • 197
  • 199
  • 8
    See https://bugs.openjdk.java.net/browse/JDK-8029896. – mellamokb Apr 01 '14 at 22:19
  • I don't care about the implementation of `BigDecimal`, this is inconsistent behavior. You can either [round half to even](http://en.wikipedia.org/wiki/Rounding#Round_half_to_even) or [round half to odd](http://en.wikipedia.org/wiki/Rounding#Round_half_to_odd), but not both. This should be addressed. – 2rs2ts Apr 01 '14 at 22:23
  • 1
    @2rs2ts: It has nothing to do with the implementation of `BigDecimal` - that is just used to demonstrate the issue. The problem is `83.65` cannot be exactly stored into a floating-point value. – mellamokb Apr 01 '14 at 22:31
  • @mellamokb Oh, I see my confusion. Thanks for pointing it out. – 2rs2ts Apr 02 '14 at 15:06

2 Answers2

23

It looks like this was a long-standing bug in JDK 7 that was finally fixed. See for example:

There is a draft plan to provide the following advisory with JDK 8 which explains the issue:

--------------------------------------------------------------------- Area: Core Libraries / java.text

Synopsis: A wrong rounding behavior of JDK7 has been fixed. The rounding behavior of NumberFormat/DecimalFormat format() method has changed when value is very close to a tie sitting exactly at the rounding position specified in the formatting pattern.

Nature of Incompatibility: behavioral

Description: When using NumberFormat/DecimalFormat classes, the rounding behavior of previous JDK versions was wrong in some corner cases. This wrong behaviour happened when calling format() method with a value that is very close to a tie, while rounding position specified by the pattern of the NumberFormat/DecimalFormat instance used is exactly sitting at the position of the tie. In that case wrong double rounding or erroneous non-rounding behavior happened.

As an example, while using default recommended NumberFormatFormat API form: NumberFormat nf = java.text.NumberFormat.getInstance() followed by nf.format(0.8055d), value 0.8055d is recorded in the computer as 0.80549999999999999378275106209912337362766265869140625 since this value cannot be represented exactly in binary format. Here default rounding rule is "half-even", and the result of calling format() in JDK7 is a wrong output of "0.806", while correct result is "0.805" since value recorded in memory by the computer is "below" the tie.

This new behavior is also implemented for all rounding positions that might be defined by any pattern chosen by the programmer (non default ones).

RFE 7131459


mellamokb
  • 56,094
  • 12
  • 110
  • 136
  • If the rounding rule is "half-even" then why would OP get 83.7? It is irrelevant how the number is recorded - this is not adherence to the contract of rounding half to even. – 2rs2ts Apr 01 '14 at 22:24
  • 5
    I'm not sure why you're complaining to me about how the JDK works - I'm just relaying information I found. Regardless, the reason why OP gets 83.7, is because the computer is technically rounding the number `83.650000000000005684341886080801486968994140625`, which is correctly rounded up to 83.7. Basically, if you want to work with money-like values, then don't use floating-point types. – mellamokb Apr 01 '14 at 22:27
  • 4
    @2rs2ts because 83.65 is not really 83.65. The new behaviour is correct. – assylias Apr 01 '14 at 22:30
  • 4
    i wonder how this affects the million financial programs written in java !! – rupps Apr 01 '14 at 22:32
  • 5
    Hopefully, 999999 of them use BigDecimals for money, not doubles or floats. – Dawood ibn Kareem Apr 01 '14 at 22:38
  • Actually, the new behaviour feels wrong to me - the old behaviour seems to be correct. I shall have to get my hands on a copy of the IEEE754 spec. I really don't trust Oracle to have got this right. – Dawood ibn Kareem Apr 01 '14 at 22:39
  • 4
    @rupps - Any financial program that uses floating point was broken *before* this change. I'm afraid, if such a program exists, then the blame for this regression should fall entirely on the programmer. I remember being told not to use FP for money in my CS degree in the late 1970's. It was well known back in the 1960's even ... – Stephen C Apr 01 '14 at 22:39
  • 2
    Not entirely on the programmer. On the programmer, the code reviewer, the system tester, and whoever else was involved in the enforcement of programming standards. – Dawood ibn Kareem Apr 01 '14 at 22:42
  • Well I don't work in the financial industry, however I refuse to believe that programs in banks don't make use of floating point. Is that true? Besides, they could be all broken before this change, but the result was consistent across JVM's. Now the problem looks like there's gonna be inconsistencies between JVM's of the same vendor. Out of curiosity, do you think IBM's JVM has the same problem? – rupps Apr 01 '14 at 22:44
  • @rupps - 1) Yes it is true. Certainly, not for computations involving customer money. AFAIK it is forbidden by the rules of accounting. 2) No idea. – Stephen C Apr 01 '14 at 22:48
  • 1
    @rupps - I have worked for a bank and also for a wealth management company. I can assure you, using floats or doubles for money would have been completely unacceptable at either of those organisations. – Dawood ibn Kareem Apr 02 '14 at 04:05
  • @david so what do you use then? specialized libraries? – rupps Apr 02 '14 at 04:06
  • 2
    Typically, someone writes a `Money` class, which is basically a wrapper around a `BigDecimal`, maybe with a constructor that checks that there are 2 decimal places. It might have some formatting methods, or these might be defined somewhere else. It might also store a currency, as well as the actual `BigDecimal`; and have some validation to make sure you don't add USD to GBP. Some organisations might prefer their `Money` class to be a wrapper for a `long`, representing a number of cents. But yeah, not a `float` or a `double` anywhere in sight. – Dawood ibn Kareem Apr 02 '14 at 04:12
15

As mentioned in other answers to this question, JDK 8 made intentional changes to DecimalFormat rounding in issue JDK-7131459: DecimalFormat produces wrong format() results when close to a tie.

However, those changes introduced a real bug filed as JDK-8039915: Wrong NumberFormat.format() HALF_UP rounding when last digit exactly at rounding position greater than 5. For example:

99.9989 -> 100.00
99.9990 ->  99.99

To put it simply, this demonstrates a case where a higher number rounds down, and a lower number rounds up: (x <= y) != (round(x) <= round(y)). It appears to only affect the HALF_UP rounding mode, which is the kind of rounding taught in grade school arithmetic classes: 0.5 rounds away from zero, always.

This issue exists in both Oracle and OpenJDK releases of Java 8 and updates 8u5, 8u11, 8u20, 8u25, and 8u31.

Oracle fixed this bug in Java 8 update 40

An unofficial runtime patch is available for earlier versions

Thanks to research by Holger in this answer to a related question, 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 works at runtime so no modifications are made to the 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 u40 and later).

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.

Community
  • 1
  • 1
William Price
  • 4,033
  • 1
  • 35
  • 54
  • Any idea why it is still marked "in progress" here: http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8039915 ? I just tested in oracle 8.0.45 and I'm seeing a problem with half_up rounding. – Dave L. Jun 24 '15 at 15:50
  • @david No clue why the status is still in-progress. If you're still having a problem with 8u45 you might be seeing the effect of JDK-7131459 (see link in answer), which created both _intentional_ and _unintended_ differences. The bug I reported as fixed in 8u40 corrected **only** the _unintended_ differences. You might also look through some of the [test cases](https://github.com/PROSPricing/jdk8patch-halfupround/blob/master/src/main/java/com/pros/java/text/SelfTest.java#L225) on my patch project. – William Price Jun 24 '15 at 19:35
  • @david If you navigate to the JIRA links in my answer (instead of bugs.java.com) they're marked as "Resolved / Fixed" – William Price Jun 24 '15 at 19:45
  • yes i know- i thought maybe that was for openjdk and still in progress for oracle jdk. could be it was reopened too since it is not working properly. i can provide a test case later. – Dave L. Jun 24 '15 at 19:49