diff --git a/src/bigint.rs b/src/bigint.rs index 891eeb46..eac085f7 100644 --- a/src/bigint.rs +++ b/src/bigint.rs @@ -12,12 +12,12 @@ use core::{i128, u128}; use core::{i64, u64}; use num_integer::{Integer, Roots}; -use num_traits::{Num, One, Pow, Signed, Zero}; +use num_traits::{Num, One, Pow, Signed, ToPrimitive, Zero}; use self::Sign::{Minus, NoSign, Plus}; use crate::big_digit::BigDigit; -use crate::biguint::to_str_radix_reversed; +use crate::biguint::{fmt_exp_slow, to_str_radix_reversed}; use crate::biguint::{BigUint, IntDigits, U32Digits, U64Digits}; mod addition; @@ -174,6 +174,44 @@ impl fmt::UpperHex for BigInt { } } +// When we bump the MSRV to >= 1.42, we can add a fast-path where this fits into an i64. + +impl fmt::LowerExp for BigInt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // The max digits that can fit into a f64, which ensures + // that printing won't have rounding errors. + const MAX_FLOAT_DIGITS: usize = 14; + if f.precision().filter(|x| x < &MAX_FLOAT_DIGITS).is_some() { + let as_f64 = self.to_f64().unwrap(); + if as_f64.is_finite() { + fmt::LowerExp::fmt(&as_f64, f) + } else { + fmt_exp_slow(self.magnitude(), f, false, !self.is_negative()) + } + } else { + fmt_exp_slow(self.magnitude(), f, false, !self.is_negative()) + } + } +} + +impl fmt::UpperExp for BigInt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // The max digits that can fit into a f64, which ensures + // that printing won't have rounding errors. + const MAX_FLOAT_DIGITS: usize = 14; + if f.precision().filter(|&x| x < MAX_FLOAT_DIGITS).is_some() { + let as_f64 = self.to_f64().unwrap(); + if as_f64.is_finite() { + fmt::UpperExp::fmt(&as_f64, f) + } else { + fmt_exp_slow(self.magnitude(), f, true, !self.is_negative()) + } + } else { + fmt_exp_slow(self.magnitude(), f, true, !self.is_negative()) + } + } +} + // !-2 = !...f fe = ...0 01 = +1 // !-1 = !...f ff = ...0 00 = 0 // ! 0 = !...0 00 = ...f ff = -1 diff --git a/src/biguint.rs b/src/biguint.rs index 271a8837..26311270 100644 --- a/src/biguint.rs +++ b/src/biguint.rs @@ -4,11 +4,11 @@ use crate::std_alloc::{String, Vec}; use core::cmp; use core::cmp::Ordering; use core::default::Default; -use core::fmt; +use core::fmt::{self, Write}; use core::hash; use core::mem; use core::str; -use core::{u32, u64, u8}; +use core::{f64, u32, u64, u8}; use num_integer::{Integer, Roots}; use num_traits::{Num, One, Pow, ToPrimitive, Unsigned, Zero}; @@ -143,6 +143,129 @@ impl fmt::Octal for BigUint { } } +// When we bump the MSRV to >= 1.42, we can add a fast-path where this fits into a u64. + +impl fmt::LowerExp for BigUint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // The max digits that can fit into a f64, which ensures + // that printing won't have rounding errors. + const MAX_FLOAT_DIGITS: usize = 14; + if f.precision().filter(|x| x < &MAX_FLOAT_DIGITS).is_some() { + let as_f64 = self.to_f64().unwrap(); + if as_f64.is_finite() { + fmt::LowerExp::fmt(&as_f64, f) + } else { + fmt_exp_slow(self, f, false, true) + } + } else { + fmt_exp_slow(self, f, false, true) + } + } +} + +impl fmt::UpperExp for BigUint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // The max digits that can fit into a f64, minus 1 to ensure there + // aren't any rounding errors. + const MAX_FLOAT_DIGITS: u32 = f64::DIGITS - 1; + if f.precision() + .filter(|&x| x < (MAX_FLOAT_DIGITS as usize)) + .is_some() + { + let as_f64 = self.to_f64().unwrap(); + if as_f64.is_finite() { + fmt::UpperExp::fmt(&as_f64, f) + } else { + fmt_exp_slow(self, f, true, true) + } + } else { + fmt_exp_slow(self, f, true, true) + } + } +} + +pub(crate) fn fmt_exp_slow( + x: &BigUint, + f: &mut fmt::Formatter<'_>, + upper: bool, + is_nonneg: bool, +) -> fmt::Result { + let precision = f.precision().unwrap_or(16); + let mut digits = to_str_radix_reversed(x, 10); + let digit_count = digits.len(); + let end_carry = round_at(&mut digits, digit_count.saturating_sub(precision)); + let mut buffer = String::with_capacity(precision + 5); + debug_assert!( + digits.len() > 1, + "Values with fewer digits should use direct formatting" + ); + let mut iter = digits.iter().rev(); + if end_carry { + buffer.push_str("1."); + } else { + buffer.push(*iter.next().unwrap() as char); + buffer.push('.'); + } + for &ch in iter.take(precision) { + buffer.push(ch as char); + } + // Add trailing zeroes when explicit precision given + if f.precision().is_some() { + for _ in 0..precision.saturating_sub(digits.len() - 1) { + buffer.push('0'); + } + } + buffer.push(if upper { 'E' } else { 'e' }); + let exponent = if end_carry { + digit_count + } else { + digit_count - 1 + }; + write!(buffer, "{}", exponent)?; + // pad_integral seems a little weird, but it does do the right thing, and + // the equivalent that f32 and f64 use is internal-only. + f.pad_integral(is_nonneg, "", &buffer) +} + +fn round_at(chars: &mut [u8], last: usize) -> bool { + if last == 0 { + return false; + } + let mut carry = char_rounds_up(chars[last - 1]); + for digit in &mut chars[last..] { + if !carry { + return false; + } else { + carry = increment_digit(digit); + } + } + carry +} + +fn char_rounds_up(ch: u8) -> bool { + match ch { + b'0'..=b'4' => false, + b'5'..=b'9' => true, + _ => unreachable!("{} is not a decimal digit", ch), + } +} + +fn increment_digit(ch: &mut u8) -> bool { + *ch = match *ch { + b'0' => b'1', + b'1' => b'2', + b'2' => b'3', + b'3' => b'4', + b'5' => b'6', + b'6' => b'7', + b'7' => b'8', + b'8' => b'9', + b'9' => b'0', + _ => unreachable!("{} is not a decimal digit", ch), + }; + *ch == b'0' +} + impl Zero for BigUint { #[inline] fn zero() -> BigUint {