Formatting n significant digits in C++ without scientific notation
Asked Answered
C

3

11

I want to format a floating point value to n significant digits but never using scientific notation (even if it would be shorter).

The format specification %f doesn't deal in significant digits, and %g will sometimes give me scientific notation (which is inappropriate for my use).

I want values in the form "123", "12.3", "1.23" or "0.000000123".

Is there an elegant way to do this using C++ or boost?

Comras answered 20/6, 2013 at 10:19 Comment(3)
So do you want 123456 to be printed as 123000 too?Theorist
@MatsPetersson, yes that's what I'm saying (but in practice I'm formatting a percentage value to 3 significant digits so the issue won't arise)Comras
Might be a good one, but I haven't tested: https://mcmap.net/q/870599/-double-to-string-without-scientific-notation-or-trailing-zeros-efficientlyHayes
U
13

The best way I know (and use it in my own code) is

#include <string>
#include <math.h>
#include <sstream>
#include <iomanip>

int round(double number)
{
    return (number >= 0) ? (int)(number + 0.5) : (int)(number - 0.5);
}

std::string format(double f, int n)
{
    if (f == 0) {
        return "0";
    }            
    int d = (int)::ceil(::log10(f < 0 ? -f : f)); /*digits before decimal point*/
    double order = ::pow(10., n - d);
    std::stringstream ss;
    ss << std::fixed << std::setprecision(std::max(n - d, 0)) << round(f * order) / order;
    return ss.str();
}

c++11 has std::round so you won't need my version of with a new compiler.

The trick I'm exploiting here is to get the precision you want by taking the base 10 log to count the number of digits before the decimal and subtracting this from the precision you want.

It satisfies @Mats Petersson's requirement too, so will work in all cases.

The bit I don't like is the initial check for zero (so the log function doesn't blow up). Suggestions for improvement / direct editing of this answer most welcome.

Underhill answered 20/6, 2013 at 10:44 Comment(6)
Hardly elegant but closest so far! Thanks. Still prints in S.N. though: format (0.00000123456, 3) prints 1.23e-06Comras
Ok try my amended answer. Pint says it's perfect (but even less elegant now due to precision max).Underhill
Looks good, thanks a lot. The whole issue is frustrating as the "significant digits" logic is clearly present in the standard library, but it's inseparable from the S.N. aspect.Comras
That is elegant enough, from my point of view :) Thanks for that.Hayes
Here’s one issue (which I will attempt to fix myself): if f is a power of ten and n > log10 f, then the output will have an extra zero at the end. For example format(100.0, 3) is “100.0”, while it should be “100” (or ideally “100.”, with a trailing decimal point).Allstar
In answer to the issue pointed out by @bdesham, using floor(..) + 1 instead of ceil(..) correctly formats powers of ten.Nozzle
C
0

std::fixed and std::setprecision (and <iomanip> in general) are your friends.

std::cout << 0.000000123 << '\n';

prints 1.23e-07 and

std::cout << std::setprecision(15) << std::fixed << 0.000000123 << '\n';

prints 0.000000123000000

Just remember that floating-point numbers have limited precision, so

std::cout << std::fixed << 123456789012345678901234567890.0 << '\n';

will print 123456789012345677877719597056.000000 (probably not what you want)

Cheongsam answered 20/6, 2013 at 10:25 Comment(1)
neither of those are what I want. "1.23e-07" is s.n., which I said I wanted to avoid. "0.000000123000000" is a fixed number of decimal points, whereas I said a wanted a fixed number of significant digits.Comras
H
0

I think you'll have to remove trailing zeros by yourself :

string trimString(string str)
{
    string::size_type s;
    for(s=str.length()-1; s>0; --s)
    {
        if(str[s] == '0') str.erase(s,1);
        else break;
    }
    if(str[s] == '.') str.erase(s,1);
    return str;
}

Usage :

double num = 0.000000123;
stringstream ss;
ss << num;
ss.str("");
ss << std::setprecision(15) << std::fixed << num; // outputs 0.000000123000000
string str;
ss >> str;
str = trimString(str);
cout << str << endl;  // outputs 0.000000123

Put together :

string format(int prec, double d) {
    stringstream ss;
    ss << d;
    ss.str("");
    ss << std::setprecision(prec) << std::fixed << d;

    string str;
    ss >> str;
    string::size_type s;
    for(s=str.length() - 1; s > 0; --s)
    {
        if(str[s] == '0') str.erase(s,1);
        else break;
    }
    if(str[s] == '.') str.erase(s,1);
    return str;
}

Usage :

double num = 0.000000123;
cout << format(15, num) << std::endl;

If someone knows a better way ...

Hayes answered 20/6, 2013 at 10:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.