3

I am using C++ and I would like to format doubles in the following obvious way. I have tried playing with 'fixed' and 'scientific' using stringstream, but I am unable to achieve this desired output.

double d = -5; // print "-5"
double d = 1000000000; // print "1000000000"
double d = 3.14; // print "3.14"
double d = 0.00000000001; // print "0.00000000001"
// Floating point error is acceptable:
double d = 10000000000000001; // print "10000000000000000"

As requested, here are the things I've tried:

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

using namespace std;

string obvious_format_attempt1( double d )
{
    stringstream ss;
    ss.precision(15);
    ss << d;
    return ss.str();
}

string obvious_format_attempt2( double d )
{
    stringstream ss;
    ss.precision(15);
    ss << fixed;
    ss << d;
    return ss.str();
}

int main(int argc, char *argv[]) 
{
    cout << "Attempt #1" << endl;
    cout << obvious_format_attempt1(-5) << endl;
    cout << obvious_format_attempt1(1000000000) << endl;
    cout << obvious_format_attempt1(3.14) << endl;
    cout << obvious_format_attempt1(0.00000000001) << endl;
    cout << obvious_format_attempt1(10000000000000001) << endl;

    cout << endl << "Attempt #2" << endl;
    cout << obvious_format_attempt2(-5) << endl;
    cout << obvious_format_attempt2(1000000000) << endl;
    cout << obvious_format_attempt2(3.14) << endl;
    cout << obvious_format_attempt2(0.00000000001) << endl;
    cout << obvious_format_attempt2(10000000000000001) << endl;

    return 0;
}

That prints the following:

Attempt #1
-5
1000000000
3.14
1e-11
1e+16

Attempt #2
-5.000000000000000
1000000000.000000000000000
3.140000000000000
0.000000000010000
10000000000000000.000000000000000
CustomCalc
  • 2,172
  • 3
  • 9
  • 8
  • 2
    Have you looked at [this page](http://www.cplusplus.com/reference/ios/scientific/)? Please provide the code you are using and describe the manner in which it does not meet your needs. – Jeff Hammond Aug 31 '15 at 00:22
  • 2
    This may be of interest: https://github.com/cppformat/cppformat – Galik Aug 31 '15 at 00:26
  • I think, but I'm not sure, that your aim is to have _the shortest possible decimal representation which, when read back by `operator>>` gives you the original value in memory_. This shows that `3.14` and `3.140` are the same - you can always remove trailing zeroes. But this statement also explains why non-zero digits can be removed. – MSalters Aug 31 '15 at 14:25

4 Answers4

1

There is no way for a program to KNOW how to format the numbers in the way that you are describing, unless you write some code to analyze the numbers in some way - and even that can be quite hard.

What is required here is knowing the input format in your source code, and that's lost as soon as the compiler converts the decimal input source code into binary form to store in the executable file.

One alternative that may work is to output to a stringstream, and then from that modify the output to strip trailing zeros. Something like this:

string obvious_format_attempt2( double d )
{
    stringstream ss;
    ss.precision(15);
    ss << fixed;
    ss << d;
    string res = ss.str();
    // Do we have a dot?
    if ((string::size_type pos = res.rfind('.')) != string::npos)
    {
       while(pos > 0 && (res[pos] == '0' || res[pos] == '.')
       {
           pos--;
       }
       res = res.substr(pos);
    }
    return res;
}

I haven't actually tired it, but as a rough sketch, it should work. Caveats are that if you have something like 0.1, it may well print as 0.09999999999999285 or some such, becuase 0.1 can not be represented in exact form as a binary.

Mats Petersson
  • 119,687
  • 13
  • 121
  • 204
  • I think something like this may be the only answer (format using standard C++ functions, and then manually "erase" the padded zeros). Perhaps I can optimize to not require a while loop. – CustomCalc Aug 31 '15 at 03:22
1

Formatting binary floating-point numbers accurately is quite tricky and was traditionally wrong. A pair of papers published in 1990 in the same journal settled that decimal values converted to binary floating-point numbers and back can have their values restored assuming they don't use more decimal digits than a specific constraint (in C++ represented using std::numeric_limits<T>::digits10 for the appropriate type T):

At the time these papers were published the C formatting directives for binary floating points ("%f", "%e", and "%g") were well established and they didn't get changed to the take the new results into account. The problem with the specification of these formatting directives is that "%f" assumes to count the digits after the decimal point and there is no format specifier asking to format numbers with a certain number of digits but not necessarily starting to count at the decimal point (e.g., to format with a decimal point but potentially having many leading zeros).

The format specifiers are still not improved, e.g., to include another one for non-scientific notation possibly involving many zeros, for that matter. Effectively, the power of the Steele/White's algorithm isn't fully exposed. The C++ formatting, sadly, didn't improve over the situation and just delegates the semantics to the C formatting directives.

The approach of not setting std::ios_base::fixed and using a precision of std::numeric_limits<double>::digits10 is the closest approximation of floating-point formatting the C and C++ standard libraries offer. The exact format requested could be obtained by getting the digits using using formatting with std::ios_base::scientific, parsing the result, and rewriting the digits afterwards. To give this process a nice stream-like interface it could be encapsulated with a std::num_put<char> facet.

An alternative could be the use of Double-Conversion. This implementation uses an improved (faster) algorithm for the conversion. It also exposes interfaces to get the digits in some form although not directly as a character sequence if I recall correctly.

Dietmar Kühl
  • 141,209
  • 12
  • 196
  • 356
0

You can't do what you want to do, because decimal numbers are not representable exactly in floating point format. In otherwords, double can't precisely hold 3.14 exactly, it stores everything as fractions of powers of 2, so it stores it as something like 3 + 9175/65536 or thereabouts (do it on your calculator and you'll get 3.1399993896484375. (I realize that 65536 is not the right denominator for IEEE double, but the gist of it is correct).

This is known as the round trip problem. You can't reliable do

double x = 3.14;
cout << magic << x;

and get "3.14"

If you must solve the round-trip problem, then don't use floating point. Use a custom "decimal" class, or use a string to hold the value.

Here's a decimal class you could use: https://stackoverflow.com/a/15320495/364818

Community
  • 1
  • 1
Mark Lakata
  • 18,024
  • 5
  • 88
  • 112
  • I edited my question to clarify that floating point error is acceptable. My issue is with the formatting, not the accuracy. – CustomCalc Aug 31 '15 at 03:20
  • Decimal values with up to `std::numeric_limits::digits10` (which is `16`) can be accurately restored. Since binary floating point values may be normalized (they are if IEEE 754 is used), trailing fractional zeros may not be recovered, though. Still the value doesn't change as these zeros don't affect the value. The relevant papers are Clinger's ["How to read floating-point numbers accurately"](http://www.cesura17.net/~will/professional/research/papers/howtoread.pdf) and Steele/White's ["How to print floating-point numbers accurately"](http://dl.acm.org/ft_gateway.cfm?id=93559). – Dietmar Kühl Aug 31 '15 at 09:55
  • 1
    Pedantic aside: The exact IEEE double that closest to 3.14 is (1.0+2567051787601183.0/4503599627370496.0)*2. Non-pedantic aside: C++11 offers a solution to the round robin problem, the `hexfloat` format. This is the same as the `printf` `%a` format, which has been a part of C since 1999. This works great for checkpoint/restart, serialize/deserialize, and marshal/unmarshal. It doesn't work at all if the output is intended to be human-readable. – David Hammen Aug 31 '15 at 13:16
0

I am using C++ and I would like to format doubles in the following obvious way.

Based on your samples, I assume you want

  • Fixed rather than scientific notation,
  • A reasonable (but not excessive) amount of precision (this is for user display, so a small bit of rounding is okay),
  • Trailing zeros truncated, and
  • Decimal point truncated as well if the number looks like an integer.

The following function does just that:

    #include <cmath>
    #include <iomanip>
    #include <sstream>
    #include <string>


    std::string fixed_precision_string (double num) {

        // Magic numbers
        static const int prec_limit = 14;       // Change to 15 if you wish
        static const double log10_fuzz = 1e-15; // In case log10 is slightly off
        static const char decimal_pt = '.';     // Better: use std::locale

        if (num == 0.0) {
            return "0";
        }

        std::string result;

        if (num < 0.0) {
            result = '-';
            num = -num;
        }

        int ndigs = int(std::log10(num) + log10_fuzz);
        std::stringstream ss;
        if (ndigs >= prec_limit) {
            ss << std::fixed
               << std::setprecision(0)
               << num;
            result += ss.str();
        }
        else {
            ss << std::fixed
               << std::setprecision(prec_limit-ndigs)
               << num;
            result += ss.str();
            auto last_non_zero = result.find_last_not_of('0');
            if (result[last_non_zero] == decimal_pt) {
                result.erase(last_non_zero); 
            }
            else if (last_non_zero+1 < result.length()) {
                result.erase(last_non_zero+1);
            }
        }
        return result;
    }

If you are using a computer that uses IEEE floating point, changing prec_limit to 16 is unadvisable. While this will let you properly print 0.9999999999999999 as such, it also prints 5.1 as 5.0999999999999996 and 9.99999998 as 9.9999999800000001. This is from my computer, your results may vary due to a different library.

Changing prec_limit to 15 is okay, but it still leads to numbers that don't print "correctly". The value specified (14) works nicely so long as you aren't trying to print 1.0-1e-15.

You could do even better, but that might require discarding the standard library (see Dietmar Kühl's answer).

David Hammen
  • 30,597
  • 8
  • 54
  • 98