ITADN

Feature request: expose multiplication of integer by power of 10 to the user

#319Closedtoughengineer 创建于 2025-09-01
T
toughengineercommented
# tl;dr: There already is implementation that allows to calculate $`integer \times10 \textasciicircum decimalExponent`$ with correct rounding. It may be useful to users. The API can look like this: ```c++ 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: <details><summary>uninteresting stuff</summary> ```c++ 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) ``` </details> ```c++ #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". <details><summary>"Tie to even" problem example. (Click/tap to expand.)</summary> To illustrate the problem we can write a program like this: ```c++ #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.000000000000<ins>1</ins>p+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). </details> ## 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: ```c++ 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. ```c++ 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: ```c++ 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: ```c++ 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 ```
关闭于 2025-09-29 2 条评论