Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

c++

iostream

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:

  • "default", which displays using either the rules of fixed or scientific, depending on the number of significant digits desired as defined by setprecision;

  • fixed, which displays a fixed number of decimal places defined by setprecision; and

  • scientific, which displays a fixed number of decimal places but using scientific notation, i.e. mantissa + exponent of the radix.

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)?

like image 864
R. Martinho Fernandes Avatar asked Feb 04 '14 12:02

R. Martinho Fernandes


2 Answers

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.

like image 179
Jerry Coffin Avatar answered Oct 16 '22 17:10

Jerry Coffin


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.

like image 22
Bartek Banachewicz Avatar answered Oct 16 '22 17:10

Bartek Banachewicz