1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +00:00

Merge pull request #7712 from drinkcat/parse_number_large_hex-fix

uucore: num_parser: Limit precision when computing 2**exp
This commit is contained in:
Sylvestre Ledru 2025-04-10 04:29:12 -04:00 committed by GitHub
commit 64cf3f5dae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -8,7 +8,7 @@
// spell-checker:ignore powf copysign prec inity infinit bigdecimal extendedbigdecimal biguint underflowed
use bigdecimal::{
BigDecimal,
BigDecimal, Context,
num_bigint::{BigInt, BigUint, Sign},
};
use num_traits::Signed;
@ -286,6 +286,32 @@ fn make_error<'a>(overflow: bool, negative: bool) -> ExtendedParserError<'a, Ext
}
}
/// Compute bd**exp using exponentiation by squaring algorithm, while maintaining the
/// precision specified in ctx (the number of digits would otherwise explode).
// TODO: We do lose a little bit of precision, and the last digits may not be correct.
// TODO: Upstream this to bigdecimal-rs.
fn pow_with_context(bd: BigDecimal, exp: u32, ctx: &bigdecimal::Context) -> BigDecimal {
if exp == 0 {
return 1.into();
}
fn trim_precision(bd: BigDecimal, ctx: &bigdecimal::Context) -> BigDecimal {
if bd.digits() > ctx.precision().get() {
bd.with_precision_round(ctx.precision(), ctx.rounding_mode())
} else {
bd
}
}
let bd = trim_precision(bd, ctx);
let ret = if exp % 2 == 0 {
pow_with_context(bd.square(), exp / 2, ctx)
} else {
&bd * pow_with_context(bd.square(), (exp - 1) / 2, ctx)
};
trim_precision(ret, ctx)
}
// Construct an ExtendedBigDecimal based on parsed data
fn construct_extended_big_decimal<'a>(
digits: BigUint,
@ -339,13 +365,14 @@ fn construct_extended_big_decimal<'a>(
// Confusingly, exponent is in base 2 for hex floating point numbers.
// Note: We cannot overflow/underflow BigDecimal here, as we will not be able to reach the
// maximum/minimum scale (i64 range).
let pow2 = BigDecimal::from_bigint(BigInt::from(2).pow(abs_exponent.to_u32().unwrap()), 0);
if !exponent.is_negative() {
bd * pow2
let base: BigDecimal = if !exponent.is_negative() {
2.into()
} else {
bd / pow2
}
BigDecimal::from(2).inverse()
};
let pow2 = pow_with_context(base, abs_exponent.to_u32().unwrap(), &Context::default());
bd * pow2
} else {
// scale != 0, which means that integral_only is not set, so only base 10 and 16 are allowed.
unreachable!();
@ -771,6 +798,26 @@ mod tests {
ExtendedBigDecimal::extended_parse("0xf.fffffffffffffffffffff")
);
// Test very large exponents (they used to take forever as we kept all digits in the past)
// Wolfram Alpha can get us (close to?) these values with a bit of log trickery:
// 2**3000000000 = 10**log_10(2**3000000000) = 10**(3000000000 * log_10(2))
// TODO: We do lose a little bit of precision, and the last digits are not be correct.
assert_eq!(
Ok(ExtendedBigDecimal::BigDecimal(
// Wolfram Alpha says 9.8162042336235053508313854078782835648991393286913072670026492205522618203568834202759669215027003865... × 10^903089986
BigDecimal::from_str("9.816204233623505350831385407878283564899139328691307267002649220552261820356883420275966921514831318e+903089986").unwrap()
)),
ExtendedBigDecimal::extended_parse("0x1p3000000000")
);
assert_eq!(
Ok(ExtendedBigDecimal::BigDecimal(
// Wolfram Alpha says 1.3492131462369983551036088935544888715959511045742395978049631768570509541390540646442193112226520316... × 10^-9030900
BigDecimal::from_str("1.349213146236998355103608893554488871595951104574239597804963176857050954139054064644219311222656999e-9030900").unwrap()
)),
// Couldn't get a answer from Wolfram Alpha for smaller negative exponents
ExtendedBigDecimal::extended_parse("0x1p-30000000")
);
// ExtendedBigDecimal overflow/underflow
assert!(matches!(
ExtendedBigDecimal::extended_parse(&format!("0x1p{}", u32::MAX as u64 + 1)),