losing precision converting from java BigDecimal to double

后端 未结 5 1468
遥遥无期
遥遥无期 2020-12-15 08:56

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

相关标签:
5条回答
  • 2020-12-15 09:47

    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.

    0 讨论(0)
  • 2020-12-15 09:50

    If it's entirely based on doubles ... why are you using BigDecimal? Wouldn't Double make more sense? If it's too large of value (or too much precision) for that then ... you can't convert it; that would be the reason to use BigDecimal in the first place.

    As to why it's losing precision, from the javadoc

    Converts this BigDecimal to a double. This conversion is similar to the narrowing primitive conversion from double to float as defined in the Java Language Specification: if this BigDecimal has too great a magnitude represent as a double, it will be converted to Double.NEGATIVE_INFINITY or Double.POSITIVE_INFINITY as appropriate. Note that even when the return value is finite, this conversion can lose information about the precision of the BigDecimal value.

    0 讨论(0)
  • 2020-12-15 09:52

    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
    
    0 讨论(0)
  • 2020-12-15 09:56

    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());
    
    0 讨论(0)
  • 2020-12-15 09:57

    You've hit the maximum possible precision for the double. If you would still like to store the value in primitives... one possible way is to store the part before the decimal point in a long

    long l = 299792;
    double d = 0.457999999984;
    

    Since you are not using up (that's a bad choice of words) the precision for storing the decimal section, you can hold more digits of precision for the fractional component. This should be easy enough to do with some rounding etc..

    0 讨论(0)
提交回复
热议问题