📜 ⬆️ ⬇️

Loss of accuracy from Double to Float or "Where are the pennies missing?"

Conversion of numbers from one type to another is usually conducted in such a way as not to lose unnecessary numbers, i.e. from smaller type to more capacious. But what if the previous developer used the conversion from Double to Float and the pennies began to disappear in the reports?
The article provides an overview of converting floating numbers to Java:
99999999.33333333 -> 100000000.0000000 98888888.33333333 -> 98888888.0000000 2974815.78000000 -> 2974815.7500000 

Let's see what this transformation leads to and why it happens that way. After all, it would seem that once the numbers used in the project are far from the maximum values ​​of the float and double types, then converting it from the first to the second should not entail negative consequences in most cases.

It is better to back up any thoughts with concrete examples, so immediately the code that was born first on the leaps and bounds of real numbers, but then, influenced by the discussion on stackoverflow about such a conversion , it turned into something more intriguing.
 public class Main { static void testDoubleToFloat(double d) { float f = (float) d; System.out.println(); System.out.println(String.format("double %.10f\t%s", d, Long.toBinaryString(Double.doubleToRawLongBits(d)))); System.out.println(String.format("float %.10f\t %s", f, Integer.toBinaryString(Float.floatToRawIntBits(f)))); } public static void main(String[] args) { System.out.println(String.format("double: %.10f / %.10f", Double.MIN_VALUE, Double.MAX_VALUE)); System.out.println(String.format("float: %.10f / %.10f", Float.MIN_VALUE, Float.MAX_VALUE)); /*  ,        double. */ testDoubleToFloat(99999999.0 + 1.0 / 3.0); //   testDoubleToFloat(98888888.0 + 1.0 / 3.0); //     testDoubleToFloat(2974815.78); testDoubleToFloat(-2974815.78); } } 

Execution result
 double: 0.0000000000 / 179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.0000000000 float: 0.0000000000 / 340282346638528860000000000000000000000.0000000000 double 99999999.3333333300 100000110010111110101111000001111111101010101010101010101010101 float 100000000.0000000000 1001100101111101011110000100000 double 98888888.3333333300 100000110010111100100111011001011100001010101010101010101010101 float 98888888.0000000000 1001100101111001001110110010111 double 2974815.7800000000 100000101000110101100100010111111100011110101110000101000111101 float 2974815.7500000000 1001010001101011001000101111111 double -2974815.7800000000 1100000101000110101100100010111111100011110101110000101000111101 float -2974815.7500000000 11001010001101011001000101111111 

Same for
 /opt/jdk1.7/bin/java -version java version "1.7.0_25" Java(TM) SE Runtime Environment (build 1.7.0_25-b15) Java HotSpot(TM) 64-Bit Server VM (build 23.25-b01, mixed mode) 

And for
 java -version java version "1.7.0_25" OpenJDK Runtime Environment (IcedTea 2.3.12) (7u25-2.3.12-4ubuntu3) OpenJDK 64-Bit Server VM (build 23.7-b01, mixed mode) 


Conversion method


To demonstrate that various constructions convert double to float in the same way, we add various methods and compare the results:
 public class Main { static void testDoubleToFloat(double d) { float f = (float) d; Float f2 = new Float(d); float f3 = Float.parseFloat(new Double(d).toString()); float f4 = Float.parseFloat(String.format("%.10f", d)); System.out.println(); System.out.println(String.format("double %.10f\t%s", d, Long.toBinaryString(Double.doubleToRawLongBits(d)))); System.out.println(String.format("float %.10f\t %s", f, Integer.toBinaryString(Float.floatToRawIntBits(f)))); System.out.println(String.format("Float %.10f\t %s", f2, Integer.toBinaryString(Float.floatToRawIntBits(f2)))); System.out.println(String.format("float %.10f\t %s", f3, Integer.toBinaryString(Float.floatToRawIntBits(f3)))); System.out.println(String.format("float %.10f\t %s", f4, Integer.toBinaryString(Float.floatToRawIntBits(f4)))); } public static void main(String[] args) { System.out.println(String.format("double: %.10f / %.10f", Double.MIN_VALUE, Double.MAX_VALUE)); System.out.println(String.format("float: %.10f / %.10f", Float.MIN_VALUE, Float.MAX_VALUE)); testDoubleToFloat(99999999.0 + 1.0 / 3.0); testDoubleToFloat(98888888.0 + 1.0 / 3.0); testDoubleToFloat(2974815.78); testDoubleToFloat(-2974815.78); } } 

Execution result
  double: 0.0000000000 / 179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.0000000000 float: 0.0000000000 / 340282346638528860000000000000000000000.0000000000 double 99999999.3333333300 100000110010111110101111000001111111101010101010101010101010101 float 100000000.0000000000 1001100101111101011110000100000 Float 100000000.0000000000 1001100101111101011110000100000 float 100000000.0000000000 1001100101111101011110000100000 float 100000000.0000000000 1001100101111101011110000100000 double 98888888.3333333300 100000110010111100100111011001011100001010101010101010101010101 float 98888888.0000000000 1001100101111001001110110010111 Float 98888888.0000000000 1001100101111001001110110010111 float 98888888.0000000000 1001100101111001001110110010111 float 98888888.0000000000 1001100101111001001110110010111 double 2974815.7800000000 100000101000110101100100010111111100011110101110000101000111101 float 2974815.7500000000 1001010001101011001000101111111 Float 2974815.7500000000 1001010001101011001000101111111 float 2974815.7500000000 1001010001101011001000101111111 float 2974815.7500000000 1001010001101011001000101111111 double -2974815.7800000000 1100000101000110101100100010111111100011110101110000101000111101 float -2974815.7500000000 11001010001101011001000101111111 Float -2974815.7500000000 11001010001101011001000101111111 float -2974815.7500000000 11001010001101011001000101111111 float -2974815.7500000000 11001010001101011001000101111111 


As you can see, the expressions "new Float (d)" and "(float) d" give the same result, since the first uses the second:
 /* * Copyright (c) 1994, 2010, Oracle and/or its affiliates. All rights reserved. * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. */ ... public final class Float extends Number implements Comparable<Float> { ... public Float(double value) { this.value = (float)value; } ... } 

If you deal with the function Float.parseFloat, then it sends us through several other functions to the next line:
  return (float)Double.longBitsToDouble( lbits ); 

Which in the same way converts a double-variable to float.
Thus, we have seen that, at least in openjdk, the most obvious ways of converting double to float are reduced to one construction:
 float f = (float) d; 

Float and double storage formats


In each example, we called the Long.toBinaryString functions for Double and Integer.toBinaryString for Float to demonstrate the low-level storage formats for the created variables. A wonderful article about this has already been written ( What you need to know about floating-point arithmetic , which has become an excellent translation of the English wiki , where it is well described and about double precision ), so here we will look only at what concerns rounding.
The above programs returned the following results:
  double 2974815.7800000000 100000101000110101100100010111111100011110101110000101000111101 float 2974815.7500000000 1001010001101011001000101111111 

The double type is 64 bits, and the float type is 32, but we see 63 and 31 characters - this is the cost of implementing the output, which ends when only zeros are left. Therefore, these numbers should look like this:
  double 2974815.7800000000 0 10000010100 0110101100100010111111100011110101110000101000111101 float 2974815.7500000000 0 10010100 01101011001000101111111 

The first bit is the sign of the number. Further 11 (for double) or 8 bits (for float) - an exponent. After - the mantissa, which plays the most interesting role. First impression: all numbers after the 23rd bit are simply lost when converting from double to float. But let's first try to restore these numbers in order to sort things out in order:

Thus, cutting from the binary 0110 1011 0010 0010 1111 1110 0011 1101 0111 0000 1010 0011 1101 from the double last 29 digits, we get 011 0101 1001 0001 0111 1111 in the float, which gives us a slightly different number.
')

Conclusion


Thus, converting from a higher accuracy format can lead to nontrivial loss of reliability of the numbers used. As for Java itself, it’s better to use the Double type, since working with Float is fraught with conversions from Double with losses. And for storing money use BigDecimal, so as not to lose a penny.

PS


In the error tracking system of the project OpenJDK found tightly related to this topic:

In the traversal for these tasks it is written:
CUSTOMER SUBMITTED WORKAROUND:
Avoiding direct String-to-float conversion by using intermediate doubles.


Proposed solutions


  1. Store money in BigDecimal banks :
      BigDecimal bg = new BigDecimal("2974815.78"); System.out.println(bg); 

  2. Currency class by type:
     class Currency { long value; ... public double asDouble() { return value / 100.0; } ... <   ,  ..   , ,  StringTokenizer> ... } 

    I think the decision will entail other errors / features, although it has the right to life.

Source: https://habr.com/ru/post/201066/


All Articles