-
Notifications
You must be signed in to change notification settings - Fork 160
Description
tl;dr:
There already is implementation that allows to calculate
It may be useful to users.
The API can look like this:
FASTFLOAT_CONSTEXPR20
typename std::enable_if<is_supported_float_type<double>::value, double>::type
multiply_integer_and_power_of_10(uint64_t mantissa,
int decimal_exponent) noexcept;
I haven't come up with a good name, so the name is (intentionally?) ugly and it should be improved.
If you are interested in it, I have the draft code ready.
Motivation
We alread have it so why not?
Other than that, the obvious motivation is that it is not trivial to multiply an integer by a power of 10 to get a specific result.
Warning
Disclaimer: everything I write is to the best of my knowledge, I may be wrong.
This warning is here so I don't litter the rest of the text with "my understanding is that..." and the like.
E.g. it is easy to come up with this example:
uninteresting stuff
struct Format {
double n;
bool hex = false;
friend std::ostream &operator<<(std::ostream &s, Format f) {
char b[24];
const auto size = (f.hex ? std::to_chars(b, b + 24, f.n, std::chars_format::hex) : std::to_chars(b, b + 24, f.n)).ptr - b;
s.write(b, size);
return s;
}
};
void print(std::ostream &s, double n) { s << Format{ n }; }
template<typename T>
void print(std::ostream &s, T n) { s << n; }
#define STRINGIZE2(s) #s
#define STRINGIZE(s) STRINGIZE2(s)
#define PRINT(x) do { std::cout << STRINGIZE(x) " = "; print(std::cout, x); } while(false)
#define COMPARE_WITH_EXPECTED(v, e) do { \
PRINT(v); std::cout << " (" << (v == expected ? "\x1b[92m==\x1b[0m" : "\x1b[91m!=\x1b[0m") << "expected)\n"; \
} while(false)
#define COMPARE(m, ex) do { \
const auto expected = m##e##ex; \
std::cout << "\'" STRINGIZE(m##e##ex) "\' = " << Format{expected} << '\n'; \
std::cout << ' '; COMPARE_WITH_EXPECTED(m * pow(10., ex), expected); \
std::cout << ' '; COMPARE_WITH_EXPECTED(m * 1##e##ex, expected); \
std::cout << ' '; COMPARE_WITH_EXPECTED(multiply_integer_and_power_of_10(m, ex), expected); \
} while (false)
COMPARE(12345678, 23);
which outputs
'12345678e23' = 1.2345678e+30
12345678 * pow(10., 23) = 1.2345678000000001e+30 (!=expected)
12345678 * 1e23 = 1.2345677999999998e+30 (!=expected)
multiply_integer_and_power_of_10(12345678, 23) = 1.2345678e+30 (==expected)
Notice that multiplying by pow(10., 23)
overshoots the exact value, and multiplying by hardcoded power of 10 1e23
undershoots.
Either of the behavior may or may not be desirable, but it's nice to have an option with correct rounding.
Parsing strings (with HUGE caveat)
You can imagine a UI control where the user can input mantissa and decimal exponent separately, in this case, instead of "printing" the whole number into a buffer and then calling from_chars()
, the programmer can interpret the mantissa and the exponent parts separately and just call multiply_integer_and_power_of_10()
.
Another example is the need to parse strings where the string is inconvenient or impossible to acquire in full, e.g. when it is received via network. So again the programmer can record significant decimal digits, track the decimal point and interpret decimal exponent without needing a buffer (of unknown size), and compose the floating point number using multiply_integer_and_power_of_10()
, e.g.:
0.000...(like, 100 zeros)...1234567890123456789e000...(opps, more zeros)...23
Since this number has (no more than) 19 significant decimal digits multiply_integer_and_power_of_10()
will give the correct result.
With this exaggerated example I'm trying to illustrate that in such cases it is non-trivial to replicate the behavior of from_chars()
and get the correct result that it would give.
Maybe there should be an interface receiving input iterators or something, but that's the whole separate issue.
The HUGE caveat is that uint64_t
can only fully accommodate at most 19 decimal digits, i.e. values <=9`999`999`999`999`999`999.
UI control can be restricted to 19 significant decimal digits and multiply_integer_and_power_of_10()
will work perfectly in that case.
In other cases, if the number that eventually arrives is more than 19 significant decimal digits we run into all sorts of problems, of which the most difficult to solve is "tie to even".
"Tie to even" problem example. (Click/tap to expand.)
To illustrate the problem we can write a program like this:
#define PRINTLN(x) do { PRINT(x); std::cout << '\n'; } while(false)
auto test = [](auto s) {
double d;
fast_float::from_chars(s.data(), s.data() + s.size(), d);
std::cout << Format{ d } << " " << Format{ d, true } << "\n\n";
};
const auto even0 = "281474976710656"sv;
PRINTLN(even0); test(even0);
const auto halfway0 = "281474976710656.03125"sv;
PRINTLN(halfway0); test(halfway0);
const auto odd = "281474976710656.0625"sv;
PRINTLN(odd); test(odd);
const auto halfway1 = "281474976710656.09375"sv;
PRINTLN(halfway1); test(halfway1);
const auto even1 = "281474976710656.125"sv;
PRINTLN(even1); test(even1);
which outputs
even0 = 281474976710656
281474976710656 1p+48
halfway0 = 281474976710656.03125
281474976710656 1p+48
odd = 281474976710656.0625
281474976710656.06 1.0000000000001p+48
halfway1 = 281474976710656.09375
281474976710656.1 1.0000000000002p+48
even1 = 281474976710656.125
281474976710656.1 1.0000000000002p+48
Notice that halfway0
, which is excatly half way between even0
and odd
(different by the least significant bit: 1.0000000000001p+48), is rounded down to even0
,
while halfway1
between odd
and even1
is rounded up to the next number even1
.
Nobody knows how to solve this problem in general other than by brute force multiplication using extended precision (which is significantly non-trivial).
Multiplying floating point values by powers of 10
Among many examples, converting units with different SI prefixes seems to be the most obvious one.
It is not very hard to come up with this example:
using std::chrono::duration, std::chrono::duration_cast;
const auto seconds = duration<double>{ 1234.5678901e-18 };
PRINTLN(seconds.count());
const auto attoseconds = duration_cast<duration<double, std::atto>>(seconds);
PRINTLN(attoseconds.count());
PRINTLN(seconds.count() * pow(10, 18));
which outputs
seconds.count() = 1.2345678901e-15
attoseconds.count() = 1234.5678900999999
seconds.count() * pow(10, 18) = 1234.5678900999999
This result may or may not be desirable, but merely switching the SI prefix and getting result other than 1234.5678901
is probably surprising to the user.
Having a hypothetical function inverse to multiply_integer_and_power_of_10()
, e.g.
int64_t decompose_to_integer_and_power_of_10(double number, int decimalExponent) {
// https://github.com/jk-jeon/dragonbox by Junekey Jeon
const auto [mantissa, exponent, isNegative] = jkj::dragonbox::to_decimal(number);
decimalExponent = exponent;
const auto m = static_cast<int64_t>(mantissa);
return isNegative ? -m : m;
}
we can generally perfectly multiply by powers of 10 with unsurprising result:
int e;
const auto m = decompose_to_integer_and_power_of_10(seconds.count(), e);
std::cout << "seconds.count() = " << m << " * 10^" << e << '\n';
PRINTLN(multiply_integer_and_power_of_10(m, e + 18));
outputs
seconds.count() = 12345678901 * 10^-25
multiply_integer_and_power_of_10(m, e + 18) = 1234.5678901
Again this may or may not be desirable, but it's nice to have this option.
One more example in the other direction:
const auto femtoseconds = duration<double, std::femto>{ 1234.56789e18 };
PRINTLN(femtoseconds.count());
const auto seconds = duration_cast<duration<double>>(femtoseconds);
PRINTLN(seconds.count());
PRINTLN(femtoseconds.count() * pow(10, -15));
int e;
const auto m = decompose_to_integer_and_power_of_10(femtoseconds.count(), e);
std::cout << "femtoseconds.count() = " << m << " * 10^" << e << '\n';
PRINTLN(multiply_integer_and_power_of_10(m, e - 15));
outputs
femtoseconds.count() = 1.23456789e+21
seconds.count() = 1234567.8900000001
femtoseconds.count() * pow(10, -15) = 1234567.8900000001
femtoseconds.count() = 123456789 * 10^13
multiply_integer_and_power_of_10(m, e - 15) = 1234567.89