Skip to content

Commit 7939769

Browse files
committed
uucore: num_parser: Update pow_with_context
This is the latest version in akubera/bigdecimal-rs#148 , but the discussion sort of stalled, this is really complicated math, but this new function is a little bit better than the original (at least I hope so).
1 parent 718b1a4 commit 7939769

File tree

1 file changed

+62
-26
lines changed

1 file changed

+62
-26
lines changed

src/uucore/src/lib/features/parser/num_parser.rs

Lines changed: 62 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55

66
//! Utilities for parsing numbers in various formats
77
8-
// spell-checker:ignore powf copysign prec inity infinit infs bigdecimal extendedbigdecimal biguint underflowed
8+
// spell-checker:ignore powf copysign prec ilog inity infinit infs bigdecimal extendedbigdecimal biguint underflowed muls
9+
10+
use std::num::NonZeroU64;
911

1012
use bigdecimal::{
1113
BigDecimal, Context,
@@ -375,30 +377,66 @@ fn make_error<'a>(overflow: bool, negative: bool) -> ExtendedParserError<'a, Ext
375377
}
376378
}
377379

378-
/// Compute bd**exp using exponentiation by squaring algorithm, while maintaining the
379-
/// precision specified in ctx (the number of digits would otherwise explode).
380-
// TODO: We do lose a little bit of precision, and the last digits may not be correct.
381-
// TODO: Upstream this to bigdecimal-rs.
382-
fn pow_with_context(bd: BigDecimal, exp: u32, ctx: &bigdecimal::Context) -> BigDecimal {
380+
// Compute bd**exp using exponentiation by squaring algorithm, while maintaining the
381+
// precision specified in ctx (the number of digits would otherwise explode).
382+
//
383+
// Algorithm comes from https://en.wikipedia.org/wiki/Exponentiation_by_squaring
384+
//
385+
// TODO: Still pending discussion in https://github.com/akubera/bigdecimal-rs/issues/147,
386+
// we do lose a little bit of precision, and the last digits may not be correct.
387+
fn pow_with_context(bd: &BigDecimal, exp: i64, ctx: &Context) -> BigDecimal {
383388
if exp == 0 {
384389
return 1.into();
385390
}
386391

387-
fn trim_precision(bd: BigDecimal, ctx: &bigdecimal::Context) -> BigDecimal {
388-
if bd.digits() > ctx.precision().get() {
389-
bd.with_precision_round(ctx.precision(), ctx.rounding_mode())
392+
// When performing a multiplication between 2 numbers, we may lose up to 2 digits
393+
// of precision.
394+
// "Proof": https://github.com/akubera/bigdecimal-rs/issues/147#issuecomment-2793431202
395+
const MARGIN_PER_MUL: u64 = 2;
396+
// When doing many multiplication, we still introduce additional errors, add 1 more digit
397+
// per 10 multiplications.
398+
const MUL_PER_MARGIN_EXTRA: u64 = 10;
399+
400+
fn trim_precision(bd: BigDecimal, ctx: &Context, margin: u64) -> BigDecimal {
401+
let prec = ctx.precision().get() + margin;
402+
if bd.digits() > prec {
403+
bd.with_precision_round(NonZeroU64::new(prec).unwrap(), ctx.rounding_mode())
390404
} else {
391405
bd
392406
}
393407
}
394408

395-
let bd = trim_precision(bd, ctx);
396-
let ret = if exp % 2 == 0 {
397-
pow_with_context(bd.square(), exp / 2, ctx)
409+
// Count the number of multiplications we're going to perform, one per "1" binary digit
410+
// in exp, and the number of times we can divide exp by 2.
411+
let mut n = exp.abs() as u64;
412+
// Note: 63 - n.leading_zeros() == n.ilog2, but that's only available in recent Rust versions.
413+
let muls = (n.count_ones() + (63 - n.leading_zeros()) - 1) as u64;
414+
// Note: div_ceil would be nice to use here, but only available in recent Rust versions.
415+
let margin_extra = (muls + MUL_PER_MARGIN_EXTRA / 2) / MUL_PER_MARGIN_EXTRA;
416+
let mut margin = margin_extra + MARGIN_PER_MUL * muls;
417+
418+
let mut bd_y: BigDecimal = 1.into();
419+
let mut bd_x = if exp >= 0 {
420+
bd.clone()
398421
} else {
399-
&bd * pow_with_context(bd.square(), (exp - 1) / 2, ctx)
422+
bd.inverse_with_context(&ctx.with_precision(
423+
NonZeroU64::new(ctx.precision().get() + margin + MARGIN_PER_MUL).unwrap(),
424+
))
400425
};
401-
trim_precision(ret, ctx)
426+
427+
while n > 1 {
428+
if n % 2 == 1 {
429+
bd_y = trim_precision(&bd_x * bd_y, ctx, margin);
430+
margin -= MARGIN_PER_MUL;
431+
n -= 1;
432+
}
433+
bd_x = trim_precision(bd_x.square(), ctx, margin);
434+
margin -= MARGIN_PER_MUL;
435+
n /= 2;
436+
}
437+
debug_assert_eq!(margin, margin_extra);
438+
439+
return trim_precision(bd_x * bd_y, ctx, 0);
402440
}
403441

404442
// Construct an ExtendedBigDecimal based on parsed data
@@ -444,22 +482,20 @@ fn construct_extended_big_decimal<'a>(
444482
let bd = BigDecimal::from_bigint(signed_digits, 0)
445483
/ BigDecimal::from_bigint(BigInt::from(16).pow(scale as u32), 0);
446484

447-
let abs_exponent = exponent.abs();
448-
// Again, pow "only" supports u32 values. Just overflow/underflow if the value provided
449-
// is > 2**32 or < 2**-32.
450-
if abs_exponent > u32::MAX.into() {
451-
return Err(make_error(exponent.is_positive(), negative));
452-
}
485+
// pow_with_context "only" supports i64 values. Just overflow/underflow if the value provided
486+
// is > 2**64 or < 2**-64.
487+
let exponent = match exponent.to_i64() {
488+
Some(exp) => exp,
489+
None => {
490+
return Err(make_error(exponent.is_positive(), negative));
491+
}
492+
};
453493

454494
// Confusingly, exponent is in base 2 for hex floating point numbers.
495+
let base: BigDecimal = 2.into();
455496
// Note: We cannot overflow/underflow BigDecimal here, as we will not be able to reach the
456497
// maximum/minimum scale (i64 range).
457-
let base: BigDecimal = if !exponent.is_negative() {
458-
2.into()
459-
} else {
460-
BigDecimal::from(2).inverse()
461-
};
462-
let pow2 = pow_with_context(base, abs_exponent.to_u32().unwrap(), &Context::default());
498+
let pow2 = pow_with_context(&base, exponent, &Context::default());
463499

464500
bd * pow2
465501
} else {

0 commit comments

Comments
 (0)