R. Martinho Fernandes
R. Martinho Fernandes

Reputation: 234584

How to produce formatting similar to .NET's '0.###%' in iostreams?

I would like to output a floating-point number as a percentage, with up to three decimal places.

I know that iostreams have three different ways of presenting floats:

These three modes can be seen in effect with this code:

#include <iostream>
#include <iomanip>

int main() {
    double d = 0.00000095;
    double e = 0.95;
    std::cout << std::setprecision(3);
    std::cout.unsetf(std::ios::floatfield);
    std::cout << "d = " << (100. * d) << "%\n";
    std::cout << "e = " << (100. * e) << "%\n";
    std::cout << std::fixed;
    std::cout << "d = " << (100. * d) << "%\n";
    std::cout << "e = " << (100. * e) << "%\n";
    std::cout << std::scientific;
    std::cout << "d = " << (100. * d) << "%\n";
    std::cout << "e = " << (100. * e) << "%\n";
}

// output:
// d = 9.5e-05%
// e = 95%
// d = 0.000%
// e = 95.000%
// d = 9.500e-05%
// e = 9.500e+01%

None of these options satisfies me.

I would like to avoid any scientific notation here as it makes the percentages really hard to read. I want to keep at most three decimal places, and it's ok if very small values show up as zero. However, I would also like to avoid trailing zeros in fractional places for cases like 0.95 above: I want that to display as in the second line, as "95%".

In .NET, I can achieve this with a custom format string like "0.###%", which gives me a number formatted as a percentage with at least one digit left of the decimal separator, and up to three digits right of the decimal separator, trailing zeros skipped: http://ideone.com/uV3nDi

Can I achieve this with iostreams, without writing my own formatting logic (e.g. special casing small numbers)?

Upvotes: 8

Views: 191

Answers (2)

Jerry Coffin
Jerry Coffin

Reputation: 490338

I'm reasonably certain nothing built into iostreams supports this directly.

I think the cleanest way to handle it is to round the number before passing it to an iostream to be printed out:

#include <iostream>
#include <vector>
#include <cmath>

double rounded(double in, int places) {
    double factor = std::pow(10, places);

    return std::round(in * factor) / factor;
}

int main() {
    std::vector<double> values{ 0.000000095123, 0.0095123, 0.95, 0.95123 };

    for (auto i : values)
        std::cout << "value = " << 100. * rounded(i, 5) << "%\n";
}

Due to the way it does rounding, this has a limitation on the magnitude of numbers it can work with. For percentages this probably isn't an issue, but if you were working with a number close to the largest that can be represented in the type in question (double in this case) the multiplication by pow(10, places) could/would overflow and produce bad results.

Though I can't be absolutely certain, it doesn't seem like this would be likely to cause an issue for the problem you seem to be trying to solve.

Upvotes: 4

Bartek Banachewicz
Bartek Banachewicz

Reputation: 39390

This solution is terrible.

I am serious. I don't like it. It's probably slow and the function has a stupid name. Maybe you can use it for test verification, though, because it's so dumb I guess you can easily see it pretty much has to work.

It also assumes decimal separator to be '.', which doesn't have to be the case. The proper point could be obtained by:

char point = std::use_facet< std::numpunct<char> >(std::cout.getloc()).decimal_point();

But that's still not solving the problem, because the characters used for digits could be different and in general this isn't something that should be written in such a way.

Here it is.

template<typename Floating>
std::string formatFloatingUpToN(unsigned n, Floating f) {
    std::stringstream out;
    out << std::setprecision(n) << std::fixed;
    out << f;
    
    std::string ret = out.str();
    
    // if this clause holds, it's all zeroes
    if (std::abs(f) < std::pow(0.1, n))
        return ret;
    
    while (true) {
        if (ret.back() == '0') {
            ret.pop_back();
            continue;
        } else if (ret.back() == '.') {
            ret.pop_back();
            break;
        } else
            break;
    }
        
    return ret;
}

And here it is in action.

Upvotes: 3

Related Questions