Skip to content

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

@toughengineer

Description

@toughengineer

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:

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions