1

I'm reading this, but really I can't get why text-float-text guarantee 6 digits, instead float-text-float should 9 (considering single precision).

Converting text-float-text store into a float the correct precision. Only when printing occurs the "rounded" version. But so its a "printer" fault.

Code:

int main()
{  
    float decimalFloat = 8.589973e9;    
    char const *decimalString = "8.589973e9";
    float const floatFromDecimalString = strtof(decimalString, nullptr);
    std::cout << decimalString << std::endl << std::scientific << floatFromDecimalString << std::endl;
    std::cout << "text-float-text: 6 digit preserved, not 7" << std::endl << std::endl;

    std::cout << "but the value is correctly converted..." << std::endl;
    std::cout << std::bitset<sizeof decimalFloat*8>(*(long unsigned int*)(&decimalFloat)) << std::endl;
    std::cout << std::bitset<sizeof floatFromDecimalString*8>(*(long unsigned int*)(&floatFromDecimalString)) << std::endl;        
} 

The binary is preserved. Its equal between declaring floor directly or after the conversion from the same decimal stored as string:

01010000000000000000000000100110

Why we need digits10? The number of preserved digits is max_digits10. If print rounds "badly", well... it seems a problem of the printer.

One should know that the actual float value significant digits are max_digits10 and not digits10 (even if you are looking at digits10 once printed).

markzzz
  • 47,390
  • 120
  • 299
  • 507
  • 1
    What are `text-float-text` and `float-text-float`? – Oliver Charlesworth Nov 05 '17 at 16:04
  • I've put a reference link: https://stackoverflow.com/questions/22458355/what-is-the-purpose-of-max-digits10-and-how-is-it-different-from-digits10/22458961#22458961 – markzzz Nov 05 '17 at 16:10
  • 1
    Those terms don't appear on that page. – Oliver Charlesworth Nov 05 '17 at 16:12
  • 1
    Ok, I think you mean a series of conversions, right? i.e. starting with a string representation, convert to float, and then back again? If so, could you clarify your code sample, and also make it clear what the results are, and what you expected. – Oliver Charlesworth Nov 05 '17 at 16:34
  • "6 digits preserved" means just that, 6 decimal digits are guaranteed but 7 is not. But it could be that *some* values for the 7th digit would be preserved, but there is not enough bits for all values 1..9, perhaps just 1..3 will fit. In the other direction you might have to store more than 6 decimal digits to preserve *all* the bits of the float value. – Bo Persson Nov 05 '17 at 16:55
  • But the actual value stored on the float actually preserve 9 valid digit. Why use 6? – markzzz Nov 05 '17 at 19:49
  • @Oliver Charlesworth: from wiki "This gives from 6 to 9 significant decimal digits precision. If a decimal string with at most 6 significant digits is converted to IEEE 754 single-precision representation, and then converted back to a decimal string with the same number of digits, the final result should match the original string. If an IEEE 754 single-precision number is converted to a decimal string with at least 9 significant digits, and then converted back to single-precision representation, the final result must match the original number". Why with string is preserved only 6 digit? Whats – markzzz Nov 05 '17 at 19:56
  • Because a single-precision float has 23 mantissa bits, which is equivalent to log10(2^23) = 6.92 digits. – Oliver Charlesworth Nov 05 '17 at 20:01
  • But max_digits10 says 9. So I believe It can preserve up to 9 values? – markzzz Nov 05 '17 at 20:02
  • You already linked to an answer that explains the difference between `digits10` and `max_digits10`, right? – Oliver Charlesworth Nov 05 '17 at 20:04

3 Answers3

3

Sometimes some code to show counters examples helps. This is C code, yet the float characteristics are the same in C++/C.

What's the reason why “text-float-text” guarantee 6 digit.

7 decimal digits do not round trip. Consider text like "9999999e3". The value converts to a float. Yet with only an effective 24-bit significant binary digits, the next float is 1024 away. As subsequent text values in the region are 1e3 or 1,000 away, eventually nearby text values convert to the same float.

6 decimal digits always works, as the step in subsequent text values is always smaller than the step in binary digits.

void text_to_float_test(void) {
  unsigned long ten = 10*1000*1000;
  float f1,f2;
  for (unsigned long i = ten; i>0; i--) {
    char s1[40];
    sprintf(s1+0, "%lue3", i);
    sscanf(s1, "%f", &f1);
    char s2[40];
    sprintf(s2 + 0, "%lue3", i-1);
    sscanf(s2, "%f", &f2);
    if (f1 == f2) {
      printf("\"%s\" and \"%s\" both convert to %.*e\n", s1, s2, 7-1, f1);
      return;
    }
  }
  puts("Done");
}

Output

"9999979e3" and "9999978e3" both convert to 9.999978e+09

but “float-text-float” does 9?

Between each power-of-2 pairs, there are typically 223 different float. Both FP values 1.000000954e+01 and the next float 1.000001049e+01 both convert to the same text when only 8 significant decimal digits are used.

Deeper: between 8 and 16 there are 223 different float linearly distributed owing to the binary encoding of FP numbers. 1/8 of them are between 10 and 11 or 1,048,576. Using only 10.xxx xxx only makes for 1,000,000 different text. More decimal digits are needed.

#include <math.h>
#include <stdio.h>

int float_to_text(float x0, float x1, int significant_digits) {
  char s0[100];
  char sn[100];
  while (x0 <= x1) {
    sprintf(s0, "%.*e", significant_digits-1, x0);
    float xn = nextafterf(x0, x0*2);  // next higher float
    sprintf(sn, "%.*e", significant_digits-1, xn);
    if (strcmp(s0,sn) == 0) {
      printf("%2d significant_digits: %.12e and the next float %.12e both are \"%s\"\n", 
          significant_digits, x0, xn, s0);
      fflush(stdout);
      return 1;
    }
    x0 = xn;
  }
  return 0;
}

void float_to_text_test(float x0) {
  int significant_digits = 5;
  while (float_to_text(x0, x0*2, significant_digits)) {
    significant_digits++;
  }
  printf("%2d significant digits needed %.*e to %.*e\n", //
      significant_digits, significant_digits, x0, significant_digits, x0*2);
}

int main(void) {
  float_to_text_test(8.0);
}

Output

 5 significant_digits: 8.000000000000e+00 and the next float 8.000000953674e+00 both are "8.0000e+00"
 6 significant_digits: 8.000000000000e+00 and the next float 8.000000953674e+00 both are "8.00000e+00"
 7 significant_digits: 8.000009536743e+00 and the next float 8.000010490417e+00 both are "8.000010e+00"
 8 significant_digits: 1.000000953674e+01 and the next float 1.000001049042e+01 both are "1.0000010e+01"
 9 significant digits needed 8.000000000e+00 to 1.600000000e+01
chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
2

Decimal→Binary→Decimal

Consider the seven-digit decimal floating-point values 9,999,979•103 (9,999,979,000) and 9,999,978•103 (9,999,978,000). When you convert these to binary floating-point with 24-bit significands, you get 1001 0101 0000 0010 1110 0100•210 (9,999,978,496) in both cases, because that is the closest binary floating-point value to each of the numbers. (The next lower and higher binary floating-point numbers are 1001 0101 0000 0010 1110 0011•210 (9,999,977,472) and 1001 0101 0000 0010 1110 0101•210 (9,999,979,520).)

Therefore, 24-bit significands cannot distinguish all decimal floating-point numbers with seven-digit significands. We can do at most six digits.

Binary→Decimal→Binary

Consider the two 24-bit-significant binary floating-point values 1111 1111 1111 1111 1111 1101•23 (134,217,704) and 1111 1111 1111 1111 1111 1100•23 (134,217,696). If you convert these to decimal floating-point with eight-digit significands, you get 13,421,770•101 in both cases. Then you cannot tell them apart. So you need at least nine decimal digits.

You can think of this as some “chunking” that is forced by where the digit positions lie. At the top of a decimal number, we need a bit big enough to exceed 5 in the first digit. But, the nearest power of two is not necessarily going to start with 5 in that position—it might start with 6, or 7, or 8, or 9, so there is some wastage in it. At the bottom, we need a bit lower than 1 in the last digit. But the nearest power of two is does not necessarily start with 9 in the next lower position. It might start with 8 or 7 or 6 or even 5. So again, there is some wastage. To go from binary to decimal to binary, you need enough decimal digits to fit around the wastage, so you need extra decimal digits. To go from decimal to binary to decimal, you have to keep the decimal digits few enough so that they plus the wastage fit inside the binary, so you need fewer decimal digits.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
2

What's the reason why “text-float-text” guarantee 6 digit but “float-text-float” does 9?

  1. From a binary FP point-of-view, a leading decimal digit of 1 to 9 contains different amount of information: 1 to 3+ bits.

  2. Where absolute precision changes is different for float (0.125, 0.5, 2.0 1.6, etc) and decimal text ("0.001", "0.1", "10.0", "`10000.0", etc.).

It is the effect of those two that cause the wobbling precision.

To see this, let us use the pigeon hole principle.


With text having n significant decimal digits, it has the form of
-1sign × 1_to_9.(n-1)decimal_digits × 10exponent

C++ typically encodes float as binary32. Most values are in the form:
-1sign × 1.23_bit_fraction × 2exponent - offset


Text-float-text

Consider a "worst-case" condition where text contains lots of information - its most significant digit is closer to 9 than 1.

In the range [1.0e9 ... 10.0e9), and using 7 significant decimal digits, text values are spaced 1000 apart.

In the select range [233 ... 234) or [8.589934592e9 ... 17.179869184e9), there are 223 different float linearly spaced 1024 apart.

9.999872000e9 and
9.999744000e9 can exactly encoded as float and as 7 digit decimal text. Difference is
0.000128000e9 or 128,000.

Between them are 127 different 7 digit decimal text values and 124 different float. If code tries to encode all 127 of those text values to float and back to the same text, it will succeed only 124 times.

Example: "9.999851e9" and "9.999850e9" both convert to float 9.999850496000e+09

Instead, if text values are only 6 significant decimal digits, the round-trip always works.


float-text-float

Consider a "worst-case" condition where text contains little information - its most significant digit is closer to 1 than 9.

In the range [8.0 ... 16.0), there are 223 or 8,388,608 different float linearly spaced.

In the range [10.0 ... 11.0), there are 1/8 × 223 or 1,048,576 different float values.

In the range [10.000000 ... 11.000000), and using 8 significant decimal digits, there are 1,000,000 different text values.

If code tries to encode all 1,048,576 of those float values to text with only 8 decimal digits and then back to the same float, it will succeed only 1,000,000 times.

9 decimal digits are needed.

chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256