Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add exponential formatting to BigInt #214

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
42 changes: 40 additions & 2 deletions src/bigint.rs
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
127 changes: 125 additions & 2 deletions src/biguint.rs
Expand Up @@ -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};
Expand Down Expand Up @@ -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);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we need rounding if there were more digits than the precision required?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! Added.

// 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pad_integral feels weird in this case, but I'm not sure it's wrong. The standard library uses its internal pad_formatted_parts for exponential integers, but at a glance I don't see what would be different.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's definitely weird, but it does seem to do the right thing. I've added a comment to that effect.

}

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 {
Expand Down