0

As we all know, floating point numbers can't exactly represent most numbers. I'm not asking a question about the precision of floats or doubles.

In a program, floating point numbers "come from somewhere". Some might originate by promoting an integer, others as numeric literals.

int x = 3;
double xd = x;
float xf = 3.0f;
double xd2 = 3.0;

Of course, some floating point numbers come from calculations involving other numbers.

double yd = std::cos(4.0);

In my problem, I will sometimes read in floating point numbers from a text file, and other times, I will receive them from a complex function that I must treat as a black box. The creator of the text file may choose to enter as many significant figures as they like -- they might only use three, or perhaps eight.

I will then perform some computations using these numbers and I would like to know how many significant figures were implied when they were created.

For argument, consider that I am performing an adaptive piecewise least squares fit to the input points. I will continue splitting my piecewise segments until a certain tolerance is achieved. I want to (in part) base the tolerance on the significant figures of the input data -- don't fit to 10^-8 if the data are rounded to the nearest 10^-3.

Others (below) have asked similar questions (but not quite the same). For example, I'm not particularly concerned with a representation output to the user in a pretty form. I'm not particularly concerned with recovering the same floating point representation from an output text value.

How to calculate the number of significant decimal digits of a c++ double?

How can I test for how many significant figures a float has in C++?

I'd like to calculate the sig figs based purely on the value of the double itself. I don't want to do a bunch of text processing on the original data file.

In most cases, floating point numbers will end up with a large series of 0000000 or 99999999 in the middle of them. An intuitive problem statement is that I'm interested in figuring out where that repeating 0 or 9 sequence begins. However, I'd prefer to not do this with a looping rounded string conversion approach. I'm hoping for a fairly direct and efficient way to figure this out.

Perhaps something as simple at looking at the least significant 'on' bit and then figure out its magnitude?

Community
  • 1
  • 1
Rob McDonald
  • 219
  • 2
  • 7
  • I assume you're aware that you can't do better than approximate the answer. – Veedrac Aug 07 '15 at 21:07
  • In the sense that everything floating point is approximate? Yes. In the sense that the user may input an exact value 3.0, but the algorithm will come back with one sig fig? Yes. – Rob McDonald Aug 07 '15 at 21:08
  • 2
    No; 1.0 is not "approximate". – Lightness Races in Orbit Aug 07 '15 at 21:08
  • No, I mean that the number of significant figures in, say, `1.0` is one *or more*; the exact number is unknowable. – Veedrac Aug 07 '15 at 21:09
  • Veedrac, yes, I understand that and am OK with it. In practice, that won't be a problem because the input data will have many observations, few/none of which will imply more precision than is represented. – Rob McDonald Aug 07 '15 at 21:12
  • 1
    FWIW, the need for base conversions is likely to prevent finding any particularly optimized method. A simple loop (like you've asked to avoid) is probably not too far from optimal. – Veedrac Aug 07 '15 at 21:16
  • Using the value of the double is not a good idea. The value `0.1` has only one significant digit, but it will result in hex `0x3fb999999999999a`, which suggests a large number of significant digits. Even if you don't like it: you will have to use the original text representation to find out. – Rudy Velthuis Aug 07 '15 at 21:50
  • 1
    I too think that you are heading into a dead end. How many "sig figs" are in 0.5? And in 0.50000? The floating point binary representation is exactly the same. You should convey the number of significant digits by some other means. – Vlad Feinstein Aug 07 '15 at 22:00
  • There will be many numbers in each data set, only very few will have values like 0.5 -- I will look for the sig figs of the set as a collective property. – Rob McDonald Aug 07 '15 at 22:36
  • Veedrac, I am not totally opposed to a loop -- my opposition was more against a loop combined with string operations (say coupled sprintf/sscanf). A loop based on truncation/rounding and multiplication/division is OK if that is the only reasonable way. – Rob McDonald Aug 07 '15 at 22:40

2 Answers2

0

Ok, I've come up with something like this....

#include <cstdlib>
#include <cstdio>
#include <cfloat>
#include <cmath>

double sigfigs( double x )
{
    int m = floor( log10( std::abs( x ) ) );

    double pow10i;
    for ( int i = m; i > -26; i-- )
    {
        pow10i = pow( 10, i );
        double y = round( x / pow10i ) * pow10i;
        if ( std::abs( x - y ) < std::abs( x ) * 10.0 * DBL_EPSILON )
            break;
    }

    return pow10i;
}

int main( )
{
    char fmt[10];
    sprintf( fmt, "%%.%de", DBL_DIG + 3 );

    double x[9] = {1.0, 0.1, 1.2, 1.23, 1.234, 1.2345, 100.2, 103000, 100.3001};

    for ( int i = 0; i < 9; i++ )
    {
        printf( "Double: " );
        printf( fmt, x[i] );
        printf( " %f is good to %g.\n", x[i], sigfigs( x[i] ) );
    }

    for ( int i = 0; i < 9; i++ )
    {
        printf( "Double: " );
        printf( fmt, -x[i] );
        printf( " %f is good to %g.\n", -x[i], sigfigs( -x[i] ) );
    }

    exit( 0 );
}

Which gives output:

Double: 1.000000000000000000e+00 1.000000 is good to 1.
Double: 1.000000000000000056e-01 0.100000 is good to 0.1.
Double: 1.199999999999999956e+00 1.200000 is good to 0.1.
Double: 1.229999999999999982e+00 1.230000 is good to 0.01.
Double: 1.233999999999999986e+00 1.234000 is good to 0.001.
Double: 1.234499999999999931e+00 1.234500 is good to 0.0001.
Double: 1.002000000000000028e+02 100.200000 is good to 0.1.
Double: 1.030000000000000000e+05 103000.000000 is good to 1000.
Double: 1.003001000000000005e+02 100.300100 is good to 0.0001.
Double: -1.000000000000000000e+00 -1.000000 is good to 1.
Double: -1.000000000000000056e-01 -0.100000 is good to 0.1.
Double: -1.199999999999999956e+00 -1.200000 is good to 0.1.
Double: -1.229999999999999982e+00 -1.230000 is good to 0.01.
Double: -1.233999999999999986e+00 -1.234000 is good to 0.001.
Double: -1.234499999999999931e+00 -1.234500 is good to 0.0001.
Double: -1.002000000000000028e+02 -100.200000 is good to 0.1.
Double: -1.030000000000000000e+05 -103000.000000 is good to 1000.
Double: -1.003001000000000005e+02 -100.300100 is good to 0.0001.

It mostly seems to work as desired. The pow(10,i) is a bit unfortunate, as is the estimation of the number's base 10 magnitude.

Also, the estimate of the difference between representable doubles is somewhat crude.

Does anyone spot any corner cases where this fails? Does anyone see any obvious ways to improve or optimize this? It would be nice if it was really cheap...

Rob

Rob McDonald
  • 219
  • 2
  • 7
  • Doesn't quite solve the issue I'm afraid. Your second value for 0.1 seems to indicate that there are only 2 significant digits, when in reality, it is closer to 0.100000-ish (honestly not sure how many 0s, but more than1 at least). The traling zeros matter. – Michael Dorgan Aug 08 '15 at 00:13
  • Michael, did you read the problem statement and the discussion to the question? The double value 1.000000000000000056e-01 could result from inputs including 0.1 0.10 0.100 0.1000 etc. The double value is ambiguous in that regard - each of the answers are equally correct. In this situation, there will actually be a series of inputs (say 0.1 0.234 0.563 0.607 0.89), as a group, the answer I'm after is 0.001. – Rob McDonald Aug 08 '15 at 03:11
0

I suggest dividing the problem into two steps:

  1. Find the minimum number of significant digits for each number.
  2. Analyze the distribution of the numbers from step 1.

For step 1, you can use the method described in the prior answer, or a method based on conversion to decimal string followed by regular expression analysis of the string. The minimum number of digits for 0.1 is indeed 1, as reported by the algorithm in that answer.

Before fixing rules for step 2 you should examine the distributions that result from several different sets of actual numbers whose input significant digits you know. If the problem is solvable at all there should be a peak and sharp drop-off at the required number of digits.

Consider the case (0.1 0.234 0.563 0.607 0.89). The results of step 1 would be:

Digits Count
1      1
2      1
3      3

and count zero for 4 or greater, suggesting 3 significant digits overall.

Community
  • 1
  • 1
Patricia Shanahan
  • 25,849
  • 4
  • 38
  • 75
  • Indeed. For the use case I envision, a simple max() should do for the outer loop. – Rob McDonald Aug 10 '15 at 11:58
  • @user52485 If the input contains any black-box calculated values, max may not work. – Patricia Shanahan Aug 10 '15 at 13:18
  • That is a fair point. It then depends on where the 'sig figs' come from in the black box. If the BB internally uses floats and casts them to doubles, it still may be reasonable. On the other hand, if it is an internal tolerance in an iterative algorithm, I'm not sure what would be able to detect it. – Rob McDonald Aug 10 '15 at 19:05