losing precision converting from java BigDecimal to double

45,992

Solution 1

You've reached the maximum precision for a double with that number. It can't be done. The value gets rounded up in this case. The conversion from BigDecimal is unrelated and the precision problem is the same either way. See this for example:

System.out.println(Double.parseDouble("299792.4579999984"));
System.out.println(Double.parseDouble("299792.45799999984"));
System.out.println(Double.parseDouble("299792.457999999984"));

Output is:

299792.4579999984
299792.45799999987
299792.458

For these cases double has more than 3 digits of precision after the decimal point. They just happen to be zeros for your number and that's the closest representation you can fit into a double. It's closer for it to round up in this case, so your 9's seem to disappear. If you try this:

System.out.println(Double.parseDouble("299792.457999999924"));

You'll notice that it keeps your 9's because it was closer to round down:

299792.4579999999

If you require that all of the digits in your number be preserved then you'll have to change your code that operates on double. You could use BigDecimal in place of them. If you need performance then you might want to explore BCD as an option, although I'm not aware of any libraries offhand.


In response to your update: the maximum exponent for a double-precision floating-point number is actually 1023. That's not your limiting factor here though. Your number exceeds the precision of the 52 fractional bits that represent the significand, see IEEE 754-1985.

Use this floating-point conversion to see your number in binary. The exponent is 18 since 262144 (2^18) is nearest. If you take the fractional bits and go up or down one in binary, you can see there's not enough precision to represent your number:

299792.457999999900 // 0010010011000100000111010100111111011111001110110101
299792.457999999984 // here's your number that doesn't fit into a double
299792.458000000000 // 0010010011000100000111010100111111011111001110110110
299792.458000000040 // 0010010011000100000111010100111111011111001110110111

Solution 2

The problem is that a double can hold 15 digits, while a BigDecimal can hold an arbitrary number. When you call toDouble(), it attempts to apply a rounding mode to remove the excess digits. However, since you have a lot of 9's in the output, that means that they keep getting rounded up to 0, with a carry to the next-highest digit.

To keep as much precision as you can, you need to change the BigDecimal's rounding mode so that it truncates:

BigDecimal bd1 = new BigDecimal("12345.1234599999998");
System.out.println(bd1.doubleValue());

BigDecimal bd2 = new BigDecimal("12345.1234599999998", new MathContext(15, RoundingMode.FLOOR));
System.out.println(bd2.doubleValue());

Solution 3

Only that many digits are printed so that, when parsing the string back to double, it will result in the exact same value.

Some detail can be found in the javadoc for Double#toString

How many digits must be printed for the fractional part of m or a? There must be at least one digit to represent the fractional part, and beyond that as many, but only as many, more digits as are needed to uniquely distinguish the argument value from adjacent values of type double. That is, suppose that x is the exact mathematical value represented by the decimal representation produced by this method for a finite nonzero argument d. Then d must be the double value nearest to x; or if two double values are equally close to x, then d must be one of them and the least significant bit of the significand of d must be 0.

Share:
45,992
amanagg1204
Author by

amanagg1204

Updated on January 18, 2020

Comments

  • amanagg1204
    amanagg1204 over 4 years

    I am working with an application that is based entirely on doubles, and am having trouble in one utility method that parses a string into a double. I've found a fix where using BigDecimal for the conversion solves the issue, but raises another problem when I go to convert the BigDecimal back to a double: I'm losing several places of precision. For example:

    import java.math.BigDecimal;
    import java.text.DecimalFormat;
    
    public class test {
        public static void main(String [] args){
            String num = "299792.457999999984";
            BigDecimal val = new BigDecimal(num);
            System.out.println("big decimal: " + val.toString());
            DecimalFormat nf = new DecimalFormat("#.0000000000");
            System.out.println("double: "+val.doubleValue());
            System.out.println("double formatted: "+nf.format(val.doubleValue()));
        }
    }
    

    This produces the following output:

    $ java test
    big decimal: 299792.457999999984
    double: 299792.458
    double formatted: 299792.4580000000
    

    The formatted double demonstrates that it's lost the precision after the third place (the application requires those lower places of precision).

    How can I get BigDecimal to preserve those additional places of precision?

    Thanks!


    Update after catching up on this post. Several people mention this is exceeding the precision of the double data type. Unless I'm reading this reference incorrectly: http://java.sun.com/docs/books/jls/third_edition/html/typesValues.html#4.2.3 then the double primitive has a maximum exponential value of Emax = 2K-1-1, and the standard implementation has K=11. So, the max exponent should be 511, no?

  • Anon
    Anon about 13 years
    And this applies to the OP's question how? Have you looked in the JLS to learn about narrowing primitive conversions?
  • Anon
    Anon about 13 years
    Almost right; change the rounding mode, and you've solved the OP's problem
  • WhiteFang34
    WhiteFang34 about 13 years
    @Anon: What are you talking about? There is no solving the OP's problem using a double.
  • Anon
    Anon about 13 years
    See my answer, or reread the OP's question to understand his/her issue with losing the 9's
  • WhiteFang34
    WhiteFang34 about 13 years
    @Anon: It doesn't matter if you round up or down, the precision is still lost. The OP stated that precision must be preserved. More 9's doesn't mean more precision, it just means you're rounding down.
  • Anon
    Anon about 13 years
    To quote the OP: "it's lost the precision after the third place" He/she isn't looking for the 17 digits or whatever, but keeping the 15 digits that a double can hold. Whether or not that's a valid concern, it is his/her concern.
  • WhiteFang34
    WhiteFang34 about 13 years
    Perhaps the OP has mistakenly thought that precision was only preserved to 3 digits after the decimal. If the OP only needs a few more digits after those then the 0's are actually the closest representation. In which case there's nothing to do. Forcing a floor rounding would lose more precision than necessary.
  • amanagg1204
    amanagg1204 about 13 years
    It's pretty obvious now that you point it out. I do need to contain all of the digits of precision, but it appears that it's maintaining 11 places -- with rounding -- which may be enough. If not I'll need to refactor to BigDecimal. Thanks!
  • amanagg1204
    amanagg1204 about 13 years
    After reading up more, added an update above. Please refer and clarify, thanks!
  • Andrzej Doyle
    Andrzej Doyle almost 13 years
    But a better solution in general would be to use BigDecimal throughout, which is intended to be used for arbitrary-precision numbers.
  • Java Drinker
    Java Drinker almost 13 years
    agreed, although somehow I got the impression that the OP was looking for an alternative to BigDecimal.
  • tc.
    tc. almost 11 years
    This loses the exponent range of the double for large numbers and does not benefit small numbers. If you're going to go down this route, it makes more sense to just use "fixed point" (with the fractional part as a numerator over 2^63 or 2^64, for example). Alternatively, represent the number as a sum of doubles (i.e. an approximation plus some error terms); there are even ways to do accurate sums with this (e.g. in Python's [math.fsum()]), but I'm not sure how easy this generalizes to multiplication/division.