Reputation: 6200
I need to format values with 2 significant digits, but never use "e" notation.
Expected output:
9.3
6.6
4.6
3.2
2.3
1.6
1.1
760
520
340
200
96
0
123456 should be printed as 120000. 0.123456 should be printed as 0.12
For context, it uses SI prefixes for every factor 10^3. Tried
std::format("{:.2g}", value);
But it decides on its own when to switch to "e" notation:
9.3
6.6
4.6
3.2
2.3
1.6
1.1
7.6e+02
5.2e+02
3.4e+02
2e+02
96
0
Note: I am specifically asking for a method not using stringstream.
Note: using f
format specifier is wrong. It produces 2 digits after the decimal point, which is not correct:
9.35
6.56
4.60
3.22
2.26
1.58
1.10
760.33
517.10
339.48
204.91
96.35
0.00
Here is a minimal reproducible example (with a bunch of values)
#include <array>
#include <cstdio>
#include <format>
static constexpr std::array<float, 25> vals{
9349.3867f,
6558.6953f,
4600.0f,
3224.8362f,
2258.7588f,
1579.2142f,
1099.9999f,
760.32617f,
517.10278f,
339.47632f,
204.91417f,
96.3461f,
0.0f,
-96.346039f,
-204.91402f,
-339.47632f,
-517.10266f,
-760.32574f,
-1099.9999f,
-1579.2134f,
-2258.7578f,
-3224.8362f,
-4599.9985f,
-6558.6938f,
-9349.3867f
};
int main()
{
for(size_t k = 0; k != std::size(vals); ++k)
{
// None of these produce the expected result
puts(std::format("{:.2g}\n", vals[k]).c_str());
puts(std::format("{:.1e}\n", vals[k]).c_str());
puts(std::format("{:.2f}\n", vals[k]).c_str());
}
}
Upvotes: 7
Views: 186
Reputation: 17486
std::format
controls how basic types (like float) are formatted, and your requirement is not possible with what it allows in its format specification, (which is basically how python format specification works).
you can however create your own type whose formatter follows any rules you want to impose, your specific requirements of 2 significant digits can be done with the following constexpr formatter.
#include <iostream>
#include <format>
#include <charconv>
struct Truncate
{
double value;
};
template<>
struct std::formatter<Truncate, char>
{
template<class ParseContext>
constexpr ParseContext::iterator parse(ParseContext& ctx)
{
return ctx.begin();
}
template<class FmtContext>
constexpr FmtContext::iterator format(Truncate s, FmtContext& ctx) const
{
char buffer[16]{};
auto result = std::to_chars(buffer, buffer + std::size(buffer), s.value, std::chars_format::general, 2);
std::string_view str{ buffer, result.ptr };
auto out_it = ctx.out();
if (auto plus_pos = str.find('+'); plus_pos != str.npos)
{
int zeros = 0;
auto zeros_view = str.substr(plus_pos + 1);
std::from_chars(zeros_view.data(), zeros_view.data() + zeros_view.size(), zeros);
bool has_dot = str.find('.') != str.npos;
if (str[0] != '-')
{
*out_it++ = str[0];
if (has_dot)
{
*out_it++ = str[2];
}
}
else
{
*out_it++ = '-';
*out_it++ = str[1];
if (has_dot)
{
*out_it++ = str[3];
}
}
if (!has_dot)
{
*out_it++ = '0';
}
for (int i = 1; i < zeros; i++)
{
*out_it++ = '0';
}
return out_it;
}
else if (auto neg_pos = str.rfind('-'); neg_pos != 0 && neg_pos != str.npos)
{
int zeros = 0;
auto zeros_view = str.substr(neg_pos + 1);
std::from_chars(zeros_view.data(), zeros_view.data() + zeros_view.size(), zeros);
bool has_dot = str.find('.') != str.npos;
if (str[0] == '-')
{
*out_it++ = '-';
}
*out_it++ = '0';
*out_it++ = '.';
for (int i = 1; i < zeros; i++)
{
*out_it++ = '0';
}
if (str[0] != '-')
{
*out_it++ = str[0];
if (has_dot)
{
*out_it++ = str[2];
}
}
else
{
*out_it++ = str[1];
if (has_dot)
{
*out_it++ = str[3];
}
}
return out_it;
}
return std::ranges::copy(std::move(str), ctx.out()).out;
}
};
int main(int argc, char** argv)
{
{
double value = 7623;
auto str = std::format("{} ", Truncate{ value });
std::cout << value << " -> " << str << '\n';
}
{
double value = -0.0123456;
auto str = std::format("{}", Truncate{ value });
std::cout << value << " -> " << str << '\n';
}
{
double value = 0;
auto str = std::format("{}", Truncate{ value });
std::cout << value << " -> " << str << '\n';
}
{
double value = 1.2;
auto str = std::format("{}", Truncate{ value });
std::cout << value << " -> " << str << '\n';
}
{
double value = 12;
auto str = std::format("{}", Truncate{ value });
std::cout << value << " -> " << str << '\n';
}
{
double value = 0.12;
auto str = std::format("{}", Truncate{ value });
std::cout << value << " -> " << str << '\n';
}
}
7623 -> 7600
-0.000123456 -> -0.00012
0 -> 0
1.2 -> 1.2
12 -> 12
0.12 -> 0.12
This formatter is almost 1.5 times faster than std::format
on a double, but it doesn't handle locale, or width or alignment. (Locale would easily solvable by doing the locale conversion only when writing to output.)
Upvotes: 6