diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 63adcca..0798a1e 100755 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -389,6 +389,8 @@ cargo-publish: - master allow_failure: true cache: [] + # disable downloading artifacts + dependencies: [] variables: CARGO_HOME: /usr/local/cargo script: diff --git a/Cargo.lock b/Cargo.lock index c8aaa0b..6916b3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,7 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "bigdecimal" -version = "0.4.6" +version = "0.4.7" dependencies = [ "autocfg", "libm", diff --git a/Cargo.toml b/Cargo.toml index 1f233ef..4803944 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bigdecimal" -version = "0.4.6" +version = "0.4.7" authors = ["Andrew Kubera"] description = "Arbitrary precision decimal numbers" documentation = "https://docs.rs/bigdecimal" @@ -15,6 +15,7 @@ keywords = [ categories = [ "mathematics", "science", "no-std" ] license = "MIT/Apache-2.0" autobenches = false +edition = "2015" [lib] bench = false diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..cb2c026 --- /dev/null +++ b/Justfile @@ -0,0 +1,43 @@ + +SED := `command -v gsed || command -v sed` + +help: + @just --list + +# git tasks to start next development version +prepare-dev-version v: + git checkout trunk + {{SED}} -zE -i 's/(name = "bigdecimal"\nversion )= [^\n]*/\1= "{{v}}+dev"/' Cargo.toml Cargo.lock + git add Cargo.toml Cargo.lock + git commit -m 'Begin v{{v}} development' + + +# git tasks to run to merge trunk into master +prepare-release v: + git checkout trunk + cargo clippy + {{SED}} -zE -i 's/(name = "bigdecimal"\nversion )= [^\n]*/\1= "{{v}}"/' Cargo.toml Cargo.lock + git add Cargo.toml Cargo.lock + git commit -m 'Version {{v}}' + git checkout master + git merge trunk --no-ff -m 'v{{v}}' + # git tag 'v{{v}}' + +# enable and run benchmarks +benchmark *args: + scripts/benchmark-bigdecimal {{args}} + +# enable and run property-tests +run-property-tests: + scripts/bigdecimal-property-tests test + + +# enable property test dependencies +enable-property-tests: + scripts/bigdecimal-property-tests enable + + +# print decimals with various formatting rules +run-formatting-example: + cargo run --example formatting-examples + diff --git a/build.rs b/build.rs index a92fc24..ccb1e18 100644 --- a/build.rs +++ b/build.rs @@ -15,6 +15,9 @@ fn main() { let ac = autocfg::new(); ac.emit_rustc_version(1, 70); + // abs_diff + ac.emit_rustc_version(1, 60); + // slice::fill ac.emit_rustc_version(1, 50); diff --git a/src/arithmetic/cbrt.rs b/src/arithmetic/cbrt.rs index a75b0cf..beb6e51 100644 --- a/src/arithmetic/cbrt.rs +++ b/src/arithmetic/cbrt.rs @@ -46,11 +46,16 @@ pub(crate) fn impl_cbrt_uint_scale( let (mut new_scale, remainder) = shifted_scale.div_rem(&3); - if remainder > 0 { - new_scale += 1; - exp_shift += (3 - remainder) as u64; - } else if remainder < 0 { - exp_shift += remainder.neg() as u64; + match remainder.cmp(&0) { + Ordering::Greater => { + new_scale += 1; + exp_shift += (3 - remainder) as u64; + } + Ordering::Less => { + exp_shift += remainder.neg() as u64; + } + Ordering::Equal => { + } } // clone-on-write copy of digits diff --git a/src/arithmetic/mod.rs b/src/arithmetic/mod.rs index e96efb3..07bcf18 100644 --- a/src/arithmetic/mod.rs +++ b/src/arithmetic/mod.rs @@ -136,6 +136,20 @@ pub(crate) fn diff_usize(a: usize, b: usize) -> (Ordering, usize) { } } +/// Return absolute difference between two numbers +#[cfg(rustc_1_60)] +#[allow(clippy::incompatible_msrv)] +#[allow(dead_code)] +pub(crate) fn abs_diff(x: i64, y: i64) -> u64 { + x.abs_diff(y) +} + +#[cfg(not(rustc_1_60))] +#[allow(dead_code)] +pub(crate) fn abs_diff(x: i64, y: i64) -> u64 { + (x as i128 - y as i128).to_u64().unwrap_or(0) +} + /// Add carry to given number, returning trimmed value and storing overflow back in carry /// diff --git a/src/arithmetic/sqrt.rs b/src/arithmetic/sqrt.rs index d0c0ff8..23ddfff 100644 --- a/src/arithmetic/sqrt.rs +++ b/src/arithmetic/sqrt.rs @@ -5,7 +5,7 @@ use crate::*; pub(crate) fn impl_sqrt(n: &BigUint, scale: i64, ctx: &Context) -> BigDecimal { // Calculate the number of digits and the difference compared to the scale - let num_digits = count_decimal_digits_uint(&n); + let num_digits = count_decimal_digits_uint(n); let scale_diff = BigInt::from(num_digits) - scale; // Calculate the number of wanted digits and the exponent we need to raise the original value to diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index 58c7370..d55f71e 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -107,8 +107,9 @@ fn dynamically_format_decimal( let trailing_zero_threshold = trailing_zero_threshold as u64; // use exponential form if decimal point is outside - // the upper and lower thresholds of the decimal - if leading_zero_threshold < leading_zero_count { + // the upper and lower thresholds of the decimal, + // and precision was not requested + if f.precision().is_none() && leading_zero_threshold < leading_zero_count { format_exponential(this, f, abs_int, "E") } else if trailing_zero_threshold < trailing_zeros { // non-scientific notation @@ -119,6 +120,39 @@ fn dynamically_format_decimal( } +pub(crate) struct FullScaleFormatter<'a>(pub BigDecimalRef<'a>); + +impl fmt::Display for FullScaleFormatter<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let n = self.0; + let non_negative = matches!(n.sign, Sign::Plus | Sign::NoSign); + + let mut digits = n.digits.to_string(); + + if n.scale <= 0 { + digits.extend(stdlib::iter::repeat('0').take(n.scale.neg() as usize)); + } else if n.scale < digits.len() as i64 { + digits.insert(digits.len() - n.scale as usize, '.'); + } else { + let mut digit_vec = digits.into_bytes(); + + let dest_str_size = n.scale as usize + 2; + let digit_count = digit_vec.len(); + let leading_char_idx = dest_str_size - digit_count; + + digit_vec.resize(dest_str_size, b'0'); + digit_vec.copy_within(0..digit_count, leading_char_idx); + fill_slice(&mut digit_vec[..digit_count.min(leading_char_idx)], b'0'); + + digit_vec[1] = b'.'; + digits = String::from_utf8(digit_vec).unwrap(); + } + + f.pad_integral(non_negative, "", &digits) + } +} + + fn format_full_scale( this: BigDecimalRef, f: &mut fmt::Formatter, @@ -188,7 +222,7 @@ fn zero_right_pad_integer_ascii_digits( // did not explicitly request precision, so we'll only // implicitly right-pad if less than this threshold. - if matches!(target_scale, None) && integer_zero_count > 20 { + if target_scale.is_none() && integer_zero_count > 20 { // no padding return; } @@ -253,7 +287,6 @@ fn format_ascii_digits_with_integer_and_fraction( digit_scale -= scale_diff as u64; } Some(zeros_to_add) => { - debug_assert_eq!(zeros_to_add, 1); digits_ascii_be.resize(digits_ascii_be.len() + zeros_to_add, b'0'); digit_scale = 0; } @@ -862,6 +895,7 @@ mod test { } impl_case!(fmt_default: "{}" => "9999999"); + impl_case!(fmt_d1: "{:.1}" => "9999999.0"); impl_case!(fmt_d8: "{:.8}" => "9999999.00000000"); impl_case!(fmt_e: "{:e}" => "9.999999e+6"); @@ -893,6 +927,21 @@ mod test { impl_case!(fmt_010d3: "{:010.3}" => "019073.972"); } + mod dec_10950633712399d557 { + use super::*; + + fn test_input() -> BigDecimal { + "10950633712399.557".parse().unwrap() + } + + impl_case!(fmt_default: "{}" => "10950633712399.557"); + impl_case!(fmt_d0: "{:.0}" => "10950633712400"); + impl_case!(fmt_d1: "{:.1}" => "10950633712399.6"); + impl_case!(fmt_d2: "{:.2}" => "10950633712399.56"); + impl_case!(fmt_d3: "{:.3}" => "10950633712399.557"); + impl_case!(fmt_d4: "{:.4}" => "10950633712399.5570"); + } + mod dec_n90037659d6902 { use super::*; @@ -981,15 +1030,20 @@ mod test { "491326e-12".parse().unwrap() } - impl_case!(fmt_default: "{}" => "4.91326E-7"); - impl_case!(fmt_d0: "{:.0}" => "5E-7"); - impl_case!(fmt_d1: "{:.1}" => "4.9E-7"); - impl_case!(fmt_d3: "{:.3}" => "4.913E-7"); - impl_case!(fmt_d5: "{:.5}" => "4.91326E-7"); - impl_case!(fmt_d6: "{:.6}" => "4.913260E-7"); - - impl_case!(fmt_d9: "{:.9}" => "4.913260000E-7"); - impl_case!(fmt_d20: "{:.20}" => "4.91326000000000000000E-7"); + impl_case!(fmt_default: "{}" => "4.91326E-7"); + impl_case!(fmt_d0: "{:.0}" => "0"); + impl_case!(fmt_d1: "{:.1}" => "0.0"); + impl_case!(fmt_d3: "{:.3}" => "0.000"); + impl_case!(fmt_d5: "{:.5}" => "0.00000"); + impl_case!(fmt_d6: "{:.7}" => "0.0000005"); + impl_case!(fmt_d9: "{:.9}" => "0.000000491"); + impl_case!(fmt_d20: "{:.20}" => "0.00000049132600000000"); + + impl_case!(fmt_d0e: "{:.0E}" => "5E-7"); + impl_case!(fmt_d1e: "{:.1E}" => "4.9E-7"); + impl_case!(fmt_d3e: "{:.3E}" => "4.913E-7"); + impl_case!(fmt_d5e: "{:.5E}" => "4.91326E-7"); + impl_case!(fmt_d6e: "{:.6E}" => "4.913260E-7"); } mod dec_0d00003102564500 { @@ -1024,8 +1078,12 @@ mod test { } impl_case!(fmt_default: "{}" => "1E-10000"); - impl_case!(fmt_d1: "{:.1}" => "1.0E-10000"); - impl_case!(fmt_d4: "{:.4}" => "1.0000E-10000"); + impl_case!(fmt_d: "{:.0}" => "0"); + impl_case!(fmt_d1: "{:.1}" => "0.0"); + impl_case!(fmt_d4: "{:.4}" => "0.0000"); + + impl_case!(fmt_d1E: "{:.1E}" => "1.0E-10000"); + impl_case!(fmt_d4E: "{:.4E}" => "1.0000E-10000"); } mod dec_1e100000 { @@ -1320,6 +1378,8 @@ mod proptests { use super::*; use paste::paste; use proptest::prelude::*; + use proptest::num::f64::NORMAL as NormalF64; + macro_rules! impl_parsing_test { ($t:ty) => { @@ -1363,6 +1423,18 @@ mod proptests { impl_parsing_test!(from-float f32); impl_parsing_test!(from-float f64); + proptest! { + #![proptest_config(ProptestConfig::with_cases(32_000))] + + #[test] + fn float_formatting(f in NormalF64, prec in 0..21usize) { + let d = BigDecimal::from_f64(f).unwrap(); + let f_fmt = format!("{f:.prec$}"); + let d_fmt = format!("{d:.prec$}").replace("+", ""); + prop_assert_eq!(f_fmt, d_fmt); + } + } + proptest! { #![proptest_config(ProptestConfig::with_cases(1000))] diff --git a/src/impl_num.rs b/src/impl_num.rs index c86681c..ac5df68 100644 --- a/src/impl_num.rs +++ b/src/impl_num.rs @@ -1,20 +1,26 @@ //! Code for num_traits -use num_traits::{Num, FromPrimitive, ToPrimitive, AsPrimitive}; +use num_traits::{Zero, Num, Signed, FromPrimitive, ToPrimitive, AsPrimitive}; use num_bigint::{BigInt, Sign, ToBigInt}; +#[cfg(not(feature = "std"))] +use num_traits::float::FloatCore; + +use crate::stdlib; use stdlib::str::FromStr; use stdlib::string::{String, ToString}; use stdlib::convert::TryFrom; use stdlib::ops::Neg; +use stdlib::cmp::Ordering; use crate::BigDecimal; +use crate::BigDecimalRef; use crate::ParseBigDecimalError; #[cfg(not(feature = "std"))] // f64::powi is only available in std, no_std must use libm -fn powi(x: f64, n: f64) -> f64 { - libm::pow(x, n) +fn powi(x: f64, n: i32) -> f64 { + libm::pow(x, n as f64) } #[cfg(feature = "std")] @@ -101,36 +107,143 @@ impl Num for BigDecimal { } } + impl ToPrimitive for BigDecimal { + fn to_i64(&self) -> Option { + self.to_ref().to_i64() + } + fn to_i128(&self) -> Option { + self.to_ref().to_i128() + } + fn to_u64(&self) -> Option { + self.to_ref().to_u64() + } + fn to_u128(&self) -> Option { + self.to_ref().to_u128() + } + fn to_f64(&self) -> Option { + self.to_ref().to_f64() + } +} + +impl ToPrimitive for BigDecimalRef<'_> { fn to_i64(&self) -> Option { match self.sign() { - Sign::Minus | Sign::Plus => self.with_scale(0).int_val.to_i64(), + Sign::Plus if self.scale == 0 => self.digits.to_i64(), + Sign::Minus if self.scale == 0 => { + self.digits.to_u64().and_then( + |d| match d.cmp(&(i64::MAX as u64 + 1)) { + Ordering::Less => Some((d as i64).neg()), + Ordering::Equal => Some(i64::MIN), + Ordering::Greater => None, + } + ) + } + Sign::Plus | Sign::Minus => self.to_owned_with_scale(0).int_val.to_i64(), Sign::NoSign => Some(0), } } fn to_i128(&self) -> Option { match self.sign() { - Sign::Minus | Sign::Plus => self.with_scale(0).int_val.to_i128(), + Sign::Plus if self.scale == 0 => self.digits.to_i128(), + Sign::Minus if self.scale == 0 => { + self.digits.to_u128().and_then( + |d| match d.cmp(&(i128::MAX as u128 + 1)) { + Ordering::Less => Some((d as i128).neg()), + Ordering::Equal => Some(i128::MIN), + Ordering::Greater => None, + } + ) + } + Sign::Plus | Sign::Minus => self.to_owned_with_scale(0).int_val.to_i128(), Sign::NoSign => Some(0), } } fn to_u64(&self) -> Option { match self.sign() { - Sign::Plus => self.with_scale(0).int_val.to_u64(), + Sign::Plus if self.scale == 0 => self.digits.to_u64(), + Sign::Plus => self.to_owned_with_scale(0).int_val.to_u64(), Sign::NoSign => Some(0), Sign::Minus => None, } } fn to_u128(&self) -> Option { match self.sign() { - Sign::Plus => self.with_scale(0).int_val.to_u128(), + Sign::Plus if self.scale == 0 => self.digits.to_u128(), + Sign::Plus => self.to_owned_with_scale(0).int_val.to_u128(), Sign::NoSign => Some(0), Sign::Minus => None, } } fn to_f64(&self) -> Option { - self.int_val.to_f64().map(|x| x * powi(10f64, self.scale.neg().as_())) + let copy_sign_to_float = |f: f64| if self.sign == Sign::Minus { f.neg() } else { f }; + + if self.digits.is_zero() { + return Some(0.0); + } + if self.scale == 0 { + return self.digits.to_f64().map(copy_sign_to_float); + } + + // borrow bugint value + let (mut int_cow, mut scale) = self.to_cow_biguint_and_scale(); + + // approximate number of base-10 digits + let digit_count = ((int_cow.bits() + 1) as f64 * stdlib::f64::consts::LOG10_2).floor() as u64; + + // trim trailing digits, 19 at a time, leaving about 25 + // which should be more than accurate enough for direct + // conversion to f64 + const N: u64 = 25; + let digits_to_remove = digit_count.saturating_sub(N); + let ten_to_19 = 10u64.pow(19); + let iter_count = digits_to_remove / 19; + for _ in 0..iter_count { + *int_cow.to_mut() /= ten_to_19; + scale -= 19; + } + + match scale.to_i32().and_then(|x| x.checked_neg()) { + Some(pow) if 0 <= pow => { + // 'simple' integer case + let f = int_cow.to_f64().map(copy_sign_to_float)?; + (f * powi(10.0, pow)).into() + } + Some(exp) => { + // format decimal as floating point and let the default parser generate the f64 + #[cfg(not(feature = "std"))] + { + let s = format!("{}e{}", int_cow, exp); + s.parse().map(copy_sign_to_float).ok() + } + + #[cfg(feature = "std")] + { + use std::io::Write; + + // save allocation of a String by using local buffer of bytes + // since we know the size will be small + // + // ~ 1 '-' + (N+19) digits + 1 'e' + 11 i32 digits = 32 + N + // (plus a little extra for safety) + let mut buf = [0u8; 50 + N as usize]; + write!(&mut buf[..], "{}e{}", int_cow, exp).ok()?; + let i = buf.iter().position(|&c| c == 0)?; + let s = stdlib::str::from_utf8(&buf[..i]).ok()?; + s.parse().map(copy_sign_to_float).ok() + } + } + None => { + // exponenent too big for i32: return appropriate infinity + let result = if self.sign != Sign::Minus { + f64::INFINITY + } else { + f64::NEG_INFINITY + }; + result.into() + } + } } } @@ -188,4 +301,83 @@ mod test { } } + + mod to_f64 { + use super::*; + use paste::paste; + use crate::stdlib; + + + macro_rules! impl_case { + ($name:ident: $f:expr) => { + #[test] + fn $name() { + let f: f64 = $f; + let s = format!("{}", f); + let n: BigDecimal = s.parse().unwrap(); + let result = n.to_f64().unwrap(); + assert_eq!(result, f, "src='{}'", s); + } + }; + ($name:ident: $src:literal => $expected:expr) => { + #[test] + fn $name() { + let n: BigDecimal = $src.parse().unwrap(); + assert_eq!(n.to_f64().unwrap(), $expected); + } + }; + } + + impl_case!(case_zero: 0.0); + impl_case!(case_neg_zero: -0.0); + impl_case!(case_875en6: 0.000875); + impl_case!(case_f64_min: f64::MIN); + impl_case!(case_f64_max: f64::MAX); + impl_case!(case_f64_min_pos: f64::MIN_POSITIVE); + impl_case!(case_pi: stdlib::f64::consts::PI); + impl_case!(case_neg_e: -stdlib::f64::consts::E); + impl_case!(case_1en500: 1e-500); + impl_case!(case_3en310: 3e-310); + impl_case!(case_0d001: 0.001); + + impl_case!(case_pos2_224en320: 2.224e-320); + impl_case!(case_neg2_224en320: -2.224e-320); + + impl_case!(case_12d34: "12.34" => 12.34); + impl_case!(case_0d14: "0.14" => 0.14); + impl_case!(case_3d14: "3.14" => 3.14); + impl_case!(case_54e23: "54e23" => 54e23); + impl_case!(case_n54e23: "-54e23" => -54e23); + impl_case!(case_12en78: "12e-78" => 12e-78); + impl_case!(case_n12en78: "-12e-78" => -1.2e-77); + impl_case!(case_n1en320: "-1e-320" => -1e-320); + impl_case!(case_1d0001en920: "1.0001e-920" => 0.0); + impl_case!(case_50000d0000: "50000.0000" => 50000.0); + + impl_case!(case_13100e4: "13100e4" => 131000000.0); + + impl_case!(case_44223e9999: "44223e9999" => f64::INFINITY); + impl_case!(case_neg44223e9999: "-44223e9999" => f64::NEG_INFINITY); + } +} + + +#[cfg(all(test, property_tests))] +mod proptests { + use super::*; + use paste::paste; + use proptest::prelude::*; + use proptest::num::f64::{NORMAL as NormalF64, SUBNORMAL as SubnormalF64}; + + proptest! { + #![proptest_config(ProptestConfig::with_cases(20_000))] + + #[test] + fn to_f64_roundtrip(f in NormalF64 | SubnormalF64) { + let d = BigDecimal::from_f64(f).unwrap(); + let v = d.to_f64(); + prop_assert!(v.is_some()); + prop_assert_eq!(f, v.unwrap()); + } + } } diff --git a/src/impl_ops.rs b/src/impl_ops.rs index 784a4e0..a52b8a0 100644 --- a/src/impl_ops.rs +++ b/src/impl_ops.rs @@ -449,7 +449,7 @@ impl Neg for BigDecimal { } } -impl<'a> Neg for &'a BigDecimal { +impl Neg for &BigDecimal { type Output = BigDecimal; #[inline] diff --git a/src/impl_ops_add.rs b/src/impl_ops_add.rs index ac099e4..4322302 100644 --- a/src/impl_ops_add.rs +++ b/src/impl_ops_add.rs @@ -95,7 +95,7 @@ impl Add for BigInt { } } -impl<'a> Add<&'a BigDecimal> for BigInt { +impl Add<&BigDecimal> for BigInt { type Output = BigDecimal; fn add(self, rhs: &BigDecimal) -> BigDecimal { @@ -103,7 +103,7 @@ impl<'a> Add<&'a BigDecimal> for BigInt { } } -impl<'a> Add> for BigInt { +impl Add> for BigInt { type Output = BigDecimal; fn add(self, rhs: BigDecimalRef<'_>) -> BigDecimal { @@ -121,7 +121,7 @@ impl Add for &BigInt { } } -impl<'a> Add<&'a BigDecimal> for &BigInt { +impl Add<&BigDecimal> for &BigInt { type Output = BigDecimal; #[inline] @@ -130,7 +130,7 @@ impl<'a> Add<&'a BigDecimal> for &BigInt { } } -impl<'a> Add> for &BigInt { +impl Add> for &BigInt { type Output = BigDecimal; #[inline] diff --git a/src/impl_ops_div.rs b/src/impl_ops_div.rs index 9f36b9f..99aef24 100644 --- a/src/impl_ops_div.rs +++ b/src/impl_ops_div.rs @@ -56,7 +56,7 @@ impl<'a> Div<&'a BigDecimal> for BigDecimal { forward_ref_val_binop!(impl Div for BigDecimal, div); -impl<'a, 'b> Div<&'b BigDecimal> for &'a BigDecimal { +impl Div<&BigDecimal> for &BigDecimal { type Output = BigDecimal; #[inline] diff --git a/src/impl_ops_mul.rs b/src/impl_ops_mul.rs index a1cbc43..3a24897 100644 --- a/src/impl_ops_mul.rs +++ b/src/impl_ops_mul.rs @@ -42,7 +42,7 @@ impl<'a> Mul<&'a BigDecimal> for BigDecimal { } } -impl<'a> Mul for &'a BigDecimal { +impl Mul for &BigDecimal { type Output = BigDecimal; #[inline] @@ -51,7 +51,7 @@ impl<'a> Mul for &'a BigDecimal { } } -impl<'a, 'b> Mul<&'b BigDecimal> for &'a BigDecimal { +impl Mul<&BigDecimal> for &BigDecimal { type Output = BigDecimal; #[inline] @@ -77,7 +77,7 @@ impl Mul for BigDecimal { } } -impl<'a> Mul<&'a BigInt> for BigDecimal { +impl Mul<&BigInt> for BigDecimal { type Output = BigDecimal; #[inline] @@ -87,7 +87,7 @@ impl<'a> Mul<&'a BigInt> for BigDecimal { } } -impl<'a> Mul for &'a BigDecimal { +impl Mul for &BigDecimal { type Output = BigDecimal; #[inline] @@ -97,7 +97,7 @@ impl<'a> Mul for &'a BigDecimal { } } -impl<'a, 'b> Mul<&'a BigInt> for &'b BigDecimal { +impl Mul<&BigInt> for &BigDecimal { type Output = BigDecimal; #[inline] @@ -128,7 +128,7 @@ impl Mul for BigInt { } } -impl<'a> Mul for &'a BigInt { +impl Mul for &BigInt { type Output = BigDecimal; #[inline] @@ -147,7 +147,7 @@ impl<'a> Mul for &'a BigInt { } } -impl<'a, 'b> Mul<&'a BigDecimal> for &'b BigInt { +impl Mul<&BigDecimal> for &BigInt { type Output = BigDecimal; #[inline] @@ -163,7 +163,7 @@ impl<'a, 'b> Mul<&'a BigDecimal> for &'b BigInt { } } -impl<'a> Mul<&'a BigDecimal> for BigInt { +impl Mul<&BigDecimal> for BigInt { type Output = BigDecimal; #[inline] @@ -181,7 +181,7 @@ impl<'a> Mul<&'a BigDecimal> for BigInt { forward_val_assignop!(impl MulAssign for BigDecimal, mul_assign); -impl<'a> MulAssign<&'a BigDecimal> for BigDecimal { +impl MulAssign<&BigDecimal> for BigDecimal { #[inline] fn mul_assign(&mut self, rhs: &BigDecimal) { if rhs.is_one() { @@ -192,7 +192,7 @@ impl<'a> MulAssign<&'a BigDecimal> for BigDecimal { } } -impl<'a> MulAssign<&'a BigInt> for BigDecimal { +impl MulAssign<&BigInt> for BigDecimal { #[inline] fn mul_assign(&mut self, rhs: &BigInt) { if rhs.is_one() { diff --git a/src/impl_ops_rem.rs b/src/impl_ops_rem.rs index 56bad43..6b3e9bf 100644 --- a/src/impl_ops_rem.rs +++ b/src/impl_ops_rem.rs @@ -17,7 +17,7 @@ impl Rem for BigDecimal { } } -impl<'a> Rem<&'a BigDecimal> for BigDecimal { +impl Rem<&BigDecimal> for BigDecimal { type Output = BigDecimal; #[inline] @@ -35,7 +35,7 @@ impl<'a> Rem<&'a BigDecimal> for BigDecimal { } } -impl<'a> Rem for &'a BigDecimal { +impl Rem for &BigDecimal { type Output = BigDecimal; #[inline] @@ -55,7 +55,7 @@ impl<'a> Rem for &'a BigDecimal { } } -impl<'a, 'b> Rem<&'b BigDecimal> for &'a BigDecimal { +impl Rem<&BigDecimal> for &BigDecimal { type Output = BigDecimal; #[inline] diff --git a/src/impl_trait_from_str.rs b/src/impl_trait_from_str.rs index 4dd760f..c54065f 100644 --- a/src/impl_trait_from_str.rs +++ b/src/impl_trait_from_str.rs @@ -22,7 +22,7 @@ mod tests { fn $name() { let dec = BigDecimal::from_str($input).unwrap(); assert_eq!(dec.int_val, $int.into()); - assert_eq!(dec.scale, -$exp); + assert_eq!(dec.scale, -($exp)); } }; } diff --git a/src/lib.rs b/src/lib.rs index f92c6ed..7f089e1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,11 +43,14 @@ #![allow(clippy::style)] #![allow(clippy::excessive_precision)] #![allow(clippy::unreadable_literal)] +#![allow(clippy::unusual_byte_groupings)] +#![allow(clippy::needless_late_init)] #![allow(clippy::needless_return)] #![allow(clippy::suspicious_arithmetic_impl)] #![allow(clippy::suspicious_op_assign_impl)] #![allow(clippy::redundant_field_names)] #![allow(clippy::approx_constant)] +#![allow(clippy::wrong_self_convention)] #![cfg_attr(test, allow(clippy::useless_vec))] #![allow(unused_imports)] @@ -88,6 +91,7 @@ use self::stdlib::str::FromStr; use self::stdlib::string::{String, ToString}; use self::stdlib::fmt; use self::stdlib::Vec; +use self::stdlib::borrow::Cow; use num_bigint::{BigInt, BigUint, ParseBigIntError, Sign}; use num_integer::Integer as IntegerTrait; @@ -572,6 +576,24 @@ impl BigDecimal { (self.int_val.clone(), self.scale) } + /// Take BigDecimal and split into `num::BigInt` of digits, and the scale + /// + /// Scale is number of digits after the decimal point, can be negative. + /// + pub fn into_bigint_and_scale(self) -> (BigInt, i64) { + (self.int_val, self.scale) + } + + + /// Return digits as borrowed Cow of integer digits, and its scale + /// + /// Scale is number of digits after the decimal point, can be negative. + /// + pub fn as_bigint_and_scale(&self) -> (Cow<'_, BigInt>, i64) { + let cow_int = Cow::Borrowed(&self.int_val); + (cow_int, self.scale) + } + /// Convert into the internal big integer value and an exponent. Note that a positive /// exponent indicates a negative power of 10. /// @@ -856,6 +878,41 @@ impl BigDecimal { ////////////////////////// // Formatting methods + /// Create string of decimal in standard decimal notation. + /// + /// Unlike standard formatter, this never prints the number in + /// scientific notation. + /// + /// # Panics + /// If the magnitude of the exponent is _very_ large, this may + /// cause out-of-memory errors, or overflowing panics. + /// + /// # Examples + /// ``` + /// # use bigdecimal::BigDecimal; + /// let n: BigDecimal = "123.45678".parse().unwrap(); + /// assert_eq!(&n.to_plain_string(), "123.45678"); + /// + /// let n: BigDecimal = "1e-10".parse().unwrap(); + /// assert_eq!(&n.to_plain_string(), "0.0000000001"); + /// ``` + pub fn to_plain_string(&self) -> String { + let mut output = String::new(); + self.write_plain_string(&mut output).expect("Could not write to string"); + output + } + + /// Write decimal value in decimal notation to the writer object. + /// + /// # Panics + /// If the exponent is very large or very small, the number of + /// this will print that many trailing or leading zeros. + /// If exabytes, this will likely panic. + /// + pub fn write_plain_string(&self, wtr: &mut W) -> fmt::Result { + write!(wtr, "{}", impl_fmt::FullScaleFormatter(self.to_ref())) + } + /// Create string of this bigdecimal in scientific notation /// /// ``` @@ -1202,6 +1259,12 @@ impl BigDecimalRef<'_> { } } + /// Borrow digits as Cow + pub(crate) fn to_cow_biguint_and_scale(&self) -> (Cow<'_, BigUint>, i64) { + let cow_int = Cow::Borrowed(self.digits); + (cow_int, self.scale) + } + /// Sign of decimal pub fn sign(&self) -> Sign { self.sign @@ -1478,13 +1541,14 @@ mod bigdecimal_tests { ("-170141183460469231731687303715884105728", -170141183460469231731687303715884105728), ("12.34", 12), ("3.14", 3), + ("-123.90", -123), ("50", 50), ("0.001", 0), ]; for (s, ans) in vals { - let calculated = BigDecimal::from_str(s).unwrap().to_i128().unwrap(); + let calculated = BigDecimal::from_str(s).unwrap().to_i128(); - assert_eq!(ans, calculated); + assert_eq!(Some(ans), calculated); } } @@ -1504,23 +1568,6 @@ mod bigdecimal_tests { } } - #[test] - fn test_to_f64() { - let vals = vec![ - ("12.34", 12.34), - ("3.14", 3.14), - ("50", 50.), - ("50000", 50000.), - ("0.001", 0.001), - ]; - for (s, ans) in vals { - let diff = BigDecimal::from_str(s).unwrap().to_f64().unwrap() - ans; - let diff = diff.abs(); - - assert!(diff < 1e-10); - } - } - #[test] fn test_from_i8() { let vals = vec![ @@ -1593,37 +1640,40 @@ mod bigdecimal_tests { assert!(BigDecimal::try_from(f64::NAN).is_err()); } - #[test] - fn test_equal() { - let vals = vec![ - ("2", ".2e1"), - ("0e1", "0.0"), - ("0e0", "0.0"), - ("0e-0", "0.0"), - ("-0901300e-3", "-901.3"), - ("-0.901300e+3", "-901.3"), - ("-0e-1", "-0.0"), - ("2123121e1231", "212.3121e1235"), - ]; - for &(x, y) in vals.iter() { - let a = BigDecimal::from_str(x).unwrap(); - let b = BigDecimal::from_str(y).unwrap(); - assert_eq!(a, b); - } - } + mod equals { + use super::*; - #[test] - fn test_not_equal() { - let vals = vec![ - ("2", ".2e2"), - ("1e45", "1e-900"), - ("1e+900", "1e-900"), - ]; - for &(x, y) in vals.iter() { - let a = BigDecimal::from_str(x).unwrap(); - let b = BigDecimal::from_str(y).unwrap(); - assert!(a != b, "{} == {}", a, b); + macro_rules! impl_case { + ($name:ident: $input_a:literal == $input_b:literal) => { + #[test] + fn $name() { + let a: BigDecimal = $input_a.parse().unwrap(); + let b: BigDecimal = $input_b.parse().unwrap(); + assert_eq!(&a, &b); + assert_eq!(a.clone(), b.clone()); + } + }; + ($name:ident: $input_a:literal != $input_b:literal) => { + #[test] + fn $name() { + let a: BigDecimal = $input_a.parse().unwrap(); + let b: BigDecimal = $input_b.parse().unwrap(); + assert_ne!(&a, &b); + assert_ne!(a.clone(), b.clone()); + } + }; } + + impl_case!(case_2: "2" == ".2e1"); + impl_case!(case_0e1: "0e1" == "0.0"); + impl_case!(case_n0: "-0" == "0.0"); + impl_case!(case_n901d3: "-901.3" == "-0.901300e+3"); + impl_case!(case_n0901300en3: "-901.3" == "-0901300e-3"); + impl_case!(case_2123121e1231: "2123121e1231" == "212.3121e1235"); + + impl_case!(case_ne_2: "2" != ".2e2"); + impl_case!(case_ne_1e45: "1e45" != "1e-900"); + impl_case!(case_ne_1e900: "1e+900" != "1e-900"); } #[test] @@ -1937,12 +1987,12 @@ mod bigdecimal_tests { ]; for s in true_vals { - let d = BigDecimal::from_str(&s).unwrap(); + let d = BigDecimal::from_str(s).unwrap(); assert!(d.is_integer()); } for s in false_vals { - let d = BigDecimal::from_str(&s).unwrap(); + let d = BigDecimal::from_str(s).unwrap(); assert!(!d.is_integer()); } } @@ -2040,6 +2090,28 @@ mod bigdecimal_tests { } } + mod to_plain_string { + use super::*; + + macro_rules! impl_test { + ($name:ident: $input:literal => $expected:literal) => { + #[test] + fn $name() { + let n: BigDecimal = $input.parse().unwrap(); + let s = n.to_plain_string(); + assert_eq!(&s, $expected); + } + }; + } + + impl_test!(case_zero: "0" => "0"); + impl_test!(case_1en18: "1e-18" => "0.000000000000000001"); + impl_test!(case_n72e4: "-72e4" => "-720000"); + impl_test!(case_95517338e30: "95517338e30" => "95517338000000000000000000000000000000"); + impl_test!(case_29478en30: "29478e-30" => "0.000000000000000000000000029478"); + impl_test!(case_30740d4897: "30740.4897" => "30740.4897"); + } + #[test] fn test_signed() { assert!(!BigDecimal::zero().is_positive()); diff --git a/src/rounding.rs b/src/rounding.rs index 172c5bc..6e408a2 100644 --- a/src/rounding.rs +++ b/src/rounding.rs @@ -296,7 +296,7 @@ pub(crate) struct InsigData { } impl InsigData { - /// Build from insig data and lazily calcuated trailing-zeros callable + /// Build from insig data and lazily calculated trailing-zeros callable pub fn from_digit_and_lazy_trailing_zeros( rounder: NonDigitRoundingData, insig_digit: u8, diff --git a/src/with_std.rs b/src/with_std.rs index 0d0ef58..1d127cf 100644 --- a/src/with_std.rs +++ b/src/with_std.rs @@ -16,7 +16,6 @@ mod stdlib { slice, str, string, - i8, f32, f64, };