1

I am calculating the number of significant numbers past the decimal point. My program discards any numbers that are spaced more than 7 orders of magnitude apart after the decimal point. Expecting some error with doubles, I accounted for very small numbers popping up when subtracting ints from doubles, even when it looked like it should equal zero (To my knowledge this is due to how computers store and compute their numbers). My confusion is why my program does not handle this unexpected number given this random test value.

Having put many cout statements it would seem that it messes up when it tries to cast the final 2. Whenever it casts it casts to 1 instead.

bool flag = true;
long double test = 2029.00012;
int count = 0;

while(flag)
{

    test = test - static_cast<int>(test);   

    if(test <= 0.00001) 
{
    flag = false;
}

test *= 10;

count++;

}

The solution I found was to cast only once at the beginning, as rounding may produce a negative and terminate prematurely, and to round thenceforth. The interesting thing is that both trunc and floor also had this issue, seemingly turning what should be a 2 into a 1.

My Professor and I were both quite stumped as I fully expected small numbers to appear (most were in the 10^-10 range), but was not expecting that casting, truncing, and flooring would all also fail.

Matt Quick
  • 70
  • 9
  • What's the biggest number that fits into your `int` type? – Kerrek SB Jan 29 '19 at 23:56
  • 4
    What you're doing is misguided. Floating-point numbers can't store decimal digits accurately, so the number stored may actually be 2029.000119999999 for example, which will give you the wrong number of digits. Don't use floating-point if you need accurate values. – interjay Jan 29 '19 at 23:59
  • I've read your question several times and I still can't figure out what you're asking about. Can you show some specific operation that produces a result other than you expect, show us what operation, what inputs, what output you expect and what output you get? Showing us some complex code and saying its output surprises you provides way too much to untangle. If your question is actually about casting a double to an integer, can you show us what double you cast, what integer you got, and what integer you expected? – David Schwartz Jan 30 '19 at 00:00
  • double is not a decimal type. you are right at edge of how accurate a double can be with decimal digits. It is like to be 2029.00011999999999 and every time it is multiplied by 10, the last digits will change. – Garr Godfrey Jan 30 '19 at 00:05
  • It is unclear what you mean by "my program does not handle this unexpected number given this random test value". What doesn't this program handle? – eerorika Jan 30 '19 at 00:05
  • Why you want to use this approach when you have `std::modf` and `std::fmodf`? – Jack Jan 30 '19 at 00:36
  • 1
    I am stumped by the fact that your professor is stumped by this. – paddy Jan 30 '19 at 00:49

3 Answers3

2

It is important to understand that not all rational numbers are representable in finite precision. Also, it is important to understand that set of numbers which are representable in finite precision in decimal base, is different from the set of numbers that are representable in finite precision in binary base. Finally, it is important to understand that your CPU probably represents floating point numbers in binary.

2029.00012 in particular happens to be a number that is not representable in a double precision IEEE 754 floating point (and it indeed is a double precision literal; you may have intended to use long double instead). It so happens that the closest number that is representable is 2029.000119999999924402800388634204864501953125. So, you're counting the significant digits of that number, not the digits of the literal that you used.

If the intention of 0.00001 was to stop counting digits when the number is close to a whole number, it is not sufficient to check whether the value is less than the threshold, but also whether it is greater than 1 - threshold, as the representation error can go either way:

 if(test <= 0.00001 || test >= 1 - 0.00001)

After all, you can multiple 0.99999999999999999999999999 with 10 many times until the result becomes close to zero, even though that number is very close to a whole number.

eerorika
  • 232,697
  • 12
  • 197
  • 326
  • "_It is important to understand that not all rational numbers are representable in finite precision._" They are actually. Just not with a FP representation. – curiousguy Feb 03 '19 at 03:42
  • @curiousguy only in theory. In practice you run out of memory. – eerorika Feb 03 '19 at 03:45
2

As multiple people have already commented, that won't work because of limitations of floating-point numbers. You had a somewhat correct intuition when you said that you expected "some error" with doubles, but that is ultimately not enough. Running your specific program on my machine, the closest representable double to 2029.00012 is 2029.0001199999999244 (this is actually a truncated value, but it shows the series of 9's well enough). For that reason, when you multiply by 10, you keep finding new significant digits.

Ultimately, the issue is that you are manipulating a base-2 real number like it's a base-10 number. This is actually quite difficult. The most notorious use cases for this are printing and parsing floating-point numbers, and a lot of sweat and blood went into that. For example, it wasn't that long ago that you could trick the official Java implementation into looping endlessly trying to convert a String to a double.

Your best shot might be to just reuse all that hard work. Print to 7 digits of precision, and subtract the number of trailing zeroes from the result:

#include <iostream>
#include <sstream>
#include <iomanip>
#include <string>

int main() {
    long double d = 2029.00012;
    auto double_string = (std::stringstream() << std::fixed << std::setprecision(7) << d).str();
    auto first_decimal_index = double_string.find('.') + 1;
    auto last_nonzero_index = double_string.find_last_not_of('0');
    if (last_nonzero_index == std::string::npos) {
        std::cout << "7 significant digits\n";
    } else if (last_nonzero_index < first_decimal_index) {
        std::cout << -(first_decimal_index - last_nonzero_index + 1) << " significant digits\n";
    } else {
        std::cout << (last_nonzero_index - first_decimal_index) << " significant digits\n";
    }
}

It feels unsatisfactory, but:

  • it correctly prints 5;
  • the "satisfactory" alternative is possibly significantly harder to implement.

It seems to me that your second-best alternative is to read on floating-point printing algorithms and implement just enough of it to get the length of the value that you're going to print, and that's not exactly an introductory-level task. If you decide to go this route, the current state of the art is the Grisu2 algorithm. Grisu2 has the notable benefit that it will always print the shortest base-10 string that will produce the given floating-point value, which is what you seem to be after.

zneak
  • 134,922
  • 42
  • 253
  • 328
  • Quite correct. Back in highschool my CompSci teacher really hammered home that if you subtracts an integer from a double to get zero it might not actually give zero. So it seems my intuition left me open to a blind side from the fact that the computer might not even be able to store the number correctly. I also knew that the cout statement doesn't show me the correct number for variables but I forgot that it will round numbers if they are very close. Thanks and sorry for choosing the other answer over yours. You made some code for me but he explained with more details and examples. – Matt Quick Jan 30 '19 at 20:20
0

If you want sane results, you can't just truncate the digits, because sometimes the floating point number will be a hair less than the rounded number. If you want to fix this via a fluke, change your initialization to be

long double test = 2029.00012L;

If you want to fix it for real,

bool flag = true;
long double test = 2029.00012;
int count = 0;

while (flag)
{
    test = test - static_cast<int>(test + 0.000005);   

    if (test <= 0.00001)
    {
        flag = false;
    }

    test *= 10;

    count++;
}

My apologies for butchering your haphazard indent; I can't abide by them. According to one of my CS professors, "ideally, a computer scientist never has to worry about the underlying hardware." I'd guess your CS professor might have similar thoughts.

Ed Grimm
  • 548
  • 5
  • 13
  • This isn't an underlying hardware question, though. You'll have the same issues regardless of the hardware so long as you use IEEE-754 floating-point numbers. This is more worrying about the underlying math. – zneak Jan 30 '19 at 00:52
  • The question my professor dismissed in that fashion wasn't about hardware, either. Though, I will point out in this case, it's pretty much certain that the IEEE-754 floating point implementation was done in hardware not software. I'm not aware of any general purpose processors being made today without support for those built in. – Ed Grimm Jan 30 '19 at 01:00