|
5 | 5 |
|
6 | 6 | //! Utilities for parsing numbers in various formats
|
7 | 7 |
|
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; |
9 | 11 |
|
10 | 12 | use bigdecimal::{
|
11 | 13 | BigDecimal, Context,
|
@@ -375,30 +377,66 @@ fn make_error<'a>(overflow: bool, negative: bool) -> ExtendedParserError<'a, Ext
|
375 | 377 | }
|
376 | 378 | }
|
377 | 379 |
|
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 { |
383 | 388 | if exp == 0 {
|
384 | 389 | return 1.into();
|
385 | 390 | }
|
386 | 391 |
|
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()) |
390 | 404 | } else {
|
391 | 405 | bd
|
392 | 406 | }
|
393 | 407 | }
|
394 | 408 |
|
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() |
398 | 421 | } 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 | + )) |
400 | 425 | };
|
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); |
402 | 440 | }
|
403 | 441 |
|
404 | 442 | // Construct an ExtendedBigDecimal based on parsed data
|
@@ -444,22 +482,20 @@ fn construct_extended_big_decimal<'a>(
|
444 | 482 | let bd = BigDecimal::from_bigint(signed_digits, 0)
|
445 | 483 | / BigDecimal::from_bigint(BigInt::from(16).pow(scale as u32), 0);
|
446 | 484 |
|
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 | + }; |
453 | 493 |
|
454 | 494 | // Confusingly, exponent is in base 2 for hex floating point numbers.
|
| 495 | + let base: BigDecimal = 2.into(); |
455 | 496 | // Note: We cannot overflow/underflow BigDecimal here, as we will not be able to reach the
|
456 | 497 | // 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()); |
463 | 499 |
|
464 | 500 | bd * pow2
|
465 | 501 | } else {
|
|
0 commit comments