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)?
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With