From ee5e9f229a5d57691f3005fe980faa2ebe252755 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Tue, 5 Mar 2024 03:16:28 -0500 Subject: [PATCH 01/50] Begin v0.4.4 development --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b388e00..543d4a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bigdecimal" -version = "0.4.3" +version = "0.4.4+dev" authors = ["Andrew Kubera"] description = "Arbitrary precision decimal numbers" documentation = "https://docs.rs/bigdecimal" From 5eb7c228dea390de538aa6d35eb84d76fbf8f9d7 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sun, 21 Jan 2024 13:40:00 -0500 Subject: [PATCH 02/50] Add missed lockfile-generation command in circleci config --- .circleci/config.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 01f2348..4274ccf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -129,6 +129,10 @@ workflows: name: "lint-test-build:1.56" release: true version: "1.56" + pre-steps: + - checkout + - run: + command: cargo generate-lockfile - lint-check From d6d6deb2c555e70682f2ebcc21f1f60fd11fee16 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sun, 22 Oct 2023 19:42:50 -0400 Subject: [PATCH 03/50] Add method BigDecimalRef::clone_into --- src/lib.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index c8b328c..4ed93da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1331,6 +1331,12 @@ impl BigDecimalRef<'_> { pub fn is_zero(&self) -> bool { self.digits.is_zero() } + + /// Clone this value into dest + pub fn clone_into(&self, dest: &mut BigDecimal) { + dest.int_val = num_bigint::BigInt::from_biguint(self.sign, self.digits.clone()); + dest.scale = self.scale; + } } impl<'a> From<&'a BigDecimal> for BigDecimalRef<'a> { From 00c9e7bd32c466b465e858ec915fbd8921a795bf Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sun, 24 Mar 2024 21:39:53 -0400 Subject: [PATCH 04/50] Add method BigDecimal::set_scale --- src/lib.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4ed93da..684159a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -383,9 +383,15 @@ impl BigDecimal { /// Useful for aligning decimals before adding/subtracting. /// fn take_and_scale(mut self, new_scale: i64) -> BigDecimal { + self.set_scale(new_scale); + self + } + + /// Change to requested scale by multiplying or truncating + fn set_scale(&mut self, new_scale: i64) { if self.int_val.is_zero() { self.scale = new_scale; - return self; + return; } match diff(new_scale, self.scale) { @@ -407,8 +413,6 @@ impl BigDecimal { } (Ordering::Equal, _) => {}, } - - self } /// Take and return bigdecimal with the given sign From 255e7343b82a07a0ea6d1bf539bbfce5de357bef Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Thu, 28 Mar 2024 23:09:29 -0400 Subject: [PATCH 05/50] Move Ord & PartialOrd impls to impl_cmp.rs --- src/impl_cmp.rs | 50 +++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 48 ----------------------------------------------- 2 files changed, 50 insertions(+), 48 deletions(-) diff --git a/src/impl_cmp.rs b/src/impl_cmp.rs index 32f7019..de961f9 100644 --- a/src/impl_cmp.rs +++ b/src/impl_cmp.rs @@ -79,6 +79,56 @@ where } +impl PartialOrd for BigDecimal { + #[inline] + fn partial_cmp(&self, other: &BigDecimal) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for BigDecimal { + /// Complete ordering implementation for BigDecimal + /// + /// # Example + /// + /// ``` + /// use std::str::FromStr; + /// + /// let a = bigdecimal::BigDecimal::from_str("-1").unwrap(); + /// let b = bigdecimal::BigDecimal::from_str("1").unwrap(); + /// assert!(a < b); + /// assert!(b > a); + /// let c = bigdecimal::BigDecimal::from_str("1").unwrap(); + /// assert!(b >= c); + /// assert!(c >= b); + /// let d = bigdecimal::BigDecimal::from_str("10.0").unwrap(); + /// assert!(d > c); + /// let e = bigdecimal::BigDecimal::from_str(".5").unwrap(); + /// assert!(e < c); + /// ``` + #[inline] + fn cmp(&self, other: &BigDecimal) -> Ordering { + let scmp = self.sign().cmp(&other.sign()); + if scmp != Ordering::Equal { + return scmp; + } + + match self.sign() { + Sign::NoSign => Ordering::Equal, + _ => { + let tmp = self - other; + match tmp.sign() { + Sign::Plus => Ordering::Greater, + Sign::Minus => Ordering::Less, + Sign::NoSign => Ordering::Equal, + } + } + } + } +} + + + #[cfg(test)] mod test_bigintref { use super::*; diff --git a/src/lib.rs b/src/lib.rs index 684159a..7e1ae72 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1017,54 +1017,6 @@ impl Hash for BigDecimal { } } -impl PartialOrd for BigDecimal { - #[inline] - fn partial_cmp(&self, other: &BigDecimal) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for BigDecimal { - /// Complete ordering implementation for BigDecimal - /// - /// # Example - /// - /// ``` - /// use std::str::FromStr; - /// - /// let a = bigdecimal::BigDecimal::from_str("-1").unwrap(); - /// let b = bigdecimal::BigDecimal::from_str("1").unwrap(); - /// assert!(a < b); - /// assert!(b > a); - /// let c = bigdecimal::BigDecimal::from_str("1").unwrap(); - /// assert!(b >= c); - /// assert!(c >= b); - /// let d = bigdecimal::BigDecimal::from_str("10.0").unwrap(); - /// assert!(d > c); - /// let e = bigdecimal::BigDecimal::from_str(".5").unwrap(); - /// assert!(e < c); - /// ``` - #[inline] - fn cmp(&self, other: &BigDecimal) -> Ordering { - let scmp = self.sign().cmp(&other.sign()); - if scmp != Ordering::Equal { - return scmp; - } - - match self.sign() { - Sign::NoSign => Ordering::Equal, - _ => { - let tmp = self - other; - match tmp.sign() { - Sign::Plus => Ordering::Greater, - Sign::Minus => Ordering::Less, - Sign::NoSign => Ordering::Equal, - } - } - } - } -} - impl Default for BigDecimal { #[inline] From 7bfaac36e4d79d6ba2086cae63d6ffa67bb01d85 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Thu, 28 Mar 2024 23:24:40 -0400 Subject: [PATCH 06/50] Implement Ord and PartialOrd for BigDecimalRef --- src/impl_cmp.rs | 18 ++++++++++++++++-- src/lib.rs | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/impl_cmp.rs b/src/impl_cmp.rs index de961f9..c2893fc 100644 --- a/src/impl_cmp.rs +++ b/src/impl_cmp.rs @@ -86,7 +86,21 @@ impl PartialOrd for BigDecimal { } } +impl PartialOrd for BigDecimalRef<'_> { + fn partial_cmp(&self, other: &BigDecimalRef<'_>) -> Option { + Some(self.cmp(other)) + } +} + + impl Ord for BigDecimal { + #[inline] + fn cmp(&self, other: &BigDecimal) -> Ordering { + self.to_ref().cmp(&other.to_ref()) + } +} + +impl Ord for BigDecimalRef<'_> { /// Complete ordering implementation for BigDecimal /// /// # Example @@ -107,7 +121,7 @@ impl Ord for BigDecimal { /// assert!(e < c); /// ``` #[inline] - fn cmp(&self, other: &BigDecimal) -> Ordering { + fn cmp(&self, other: &BigDecimalRef) -> Ordering { let scmp = self.sign().cmp(&other.sign()); if scmp != Ordering::Equal { return scmp; @@ -116,7 +130,7 @@ impl Ord for BigDecimal { match self.sign() { Sign::NoSign => Ordering::Equal, _ => { - let tmp = self - other; + let tmp = *self - *other; match tmp.sign() { Sign::Plus => Ordering::Greater, Sign::Minus => Ordering::Less, diff --git a/src/lib.rs b/src/lib.rs index 7e1ae72..a1b1a30 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1194,7 +1194,7 @@ impl<'a> Sum<&'a BigDecimal> for BigDecimal { /// assert_eq!(m, "-122.456".parse().unwrap()); /// ``` /// -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Eq)] pub struct BigDecimalRef<'a> { sign: Sign, digits: &'a BigUint, From 4da1c97bf168480a644a9df11d433c259a1614cc Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Fri, 29 Mar 2024 01:41:49 -0400 Subject: [PATCH 07/50] Reimplement Ord, avoiding allocations --- src/impl_cmp.rs | 124 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 110 insertions(+), 14 deletions(-) diff --git a/src/impl_cmp.rs b/src/impl_cmp.rs index c2893fc..46d4409 100644 --- a/src/impl_cmp.rs +++ b/src/impl_cmp.rs @@ -8,11 +8,7 @@ //! &BigDecimal and BigDecimalRef are comparable. //! -use crate::{ - BigDecimal, - BigDecimalRef, - Sign, -}; +use crate::*; use stdlib::cmp::Ordering; use stdlib::iter; @@ -74,7 +70,7 @@ where let scaled = iter::repeat(&0u8).take(trailing_zero_count).chain(scaled_digits.iter()); // return true if all digits are the same - unscaled_digits.iter().zip(scaled).all(|(digit_a, digit_b)| { digit_a == digit_b }) + unscaled_digits.iter().zip(scaled).all(|(digit_a, digit_b)| digit_a == digit_b) } } @@ -122,25 +118,125 @@ impl Ord for BigDecimalRef<'_> { /// ``` #[inline] fn cmp(&self, other: &BigDecimalRef) -> Ordering { + use Ordering::*; + let scmp = self.sign().cmp(&other.sign()); if scmp != Ordering::Equal { return scmp; } - match self.sign() { - Sign::NoSign => Ordering::Equal, - _ => { - let tmp = *self - *other; - match tmp.sign() { - Sign::Plus => Ordering::Greater, - Sign::Minus => Ordering::Less, - Sign::NoSign => Ordering::Equal, + if self.sign() == Sign::NoSign { + return Ordering::Equal; + } + + let result = match arithmetic::diff(self.scale, other.scale) { + (Greater, scale_diff) | (Equal, scale_diff) => { + compare_scaled_biguints(self.digits, other.digits, scale_diff) + } + (Less, scale_diff) => { + compare_scaled_biguints(other.digits, self.digits, scale_diff).reverse() + } + }; + + if other.sign == Sign::Minus { + result.reverse() + } else { + result + } + } +} + + +/// compare scaled uints: a <=> b * 10^{scale_diff} +/// +fn compare_scaled_biguints(a: &BigUint, b: &BigUint, scale_diff: u64) -> Ordering { + use Ordering::*; + + if scale_diff == 0 { + return a.cmp(b); + } + + // if biguints fit it u64 or u128, compare using those (avoiding allocations) + if let Some(result) = compare_scalar_biguints(a, b, scale_diff) { + return result; + } + + let a_digit_count = count_decimal_digits_uint(a); + let b_digit_count = count_decimal_digits_uint(b); + + let digit_count_cmp = a_digit_count.cmp(&(b_digit_count + scale_diff)); + if digit_count_cmp != Equal { + return digit_count_cmp; + } + + let a_digits = a.to_radix_le(10); + let b_digits = b.to_radix_le(10); + + debug_assert_eq!(a_digits.len(), a_digit_count as usize); + debug_assert_eq!(b_digits.len(), b_digit_count as usize); + + let mut a_it = a_digits.iter().rev(); + let mut b_it = b_digits.iter().rev(); + + loop { + match (a_it.next(), b_it.next()) { + (Some(ai), Some(bi)) => { + match ai.cmp(bi) { + Equal => continue, + result => return result, } } + (Some(&ai), None) => { + if ai == 0 && a_it.all(Zero::is_zero) { + return Equal; + } else { + return Greater; + } + } + (None, Some(&bi)) => { + if bi == 0 && b_it.all(Zero::is_zero) { + return Equal; + } else { + return Less; + } + } + (None, None) => { + return Equal; + } } } } +/// Try fitting biguints into primitive integers, using those for ordering if possible +fn compare_scalar_biguints(a: &BigUint, b: &BigUint, scale_diff: u64) -> Option { + let scale_diff = scale_diff.to_usize()?; + + // try u64, then u128 + compare_scaled_uints::(a, b, scale_diff) + .or_else(|| compare_scaled_uints::(a, b, scale_diff)) +} + +/// Implementation comparing biguints cast to generic type +fn compare_scaled_uints<'a, T>(a: &'a BigUint, b: &'a BigUint, scale_diff: usize) -> Option +where + T: num_traits::PrimInt + TryFrom<&'a BigUint> +{ + let ten = T::from(10).unwrap(); + + let a = T::try_from(a).ok(); + let b = T::try_from(b).ok().and_then( + |b| num_traits::checked_pow(ten, scale_diff).and_then( + |p| b.checked_mul(&p))); + + match (a, b) { + (Some(a), Some(scaled_b)) => Some(a.cmp(&scaled_b)), + // if scaled_b doesn't fit in size T, while 'a' does, then a is certainly less + (Some(_), None) => Some(Ordering::Less), + // if scaled_b doesn't fit in size T, while 'a' does, then a is certainly less + (None, Some(_)) => Some(Ordering::Greater), + (None, None) => None, + } +} #[cfg(test)] From 0923b7ed4ef0bb188a8d753d1ccc9f1cef6afeea Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Fri, 29 Mar 2024 01:55:39 -0400 Subject: [PATCH 08/50] Test compare_scaled_biguints --- src/impl_cmp.rs | 63 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/src/impl_cmp.rs b/src/impl_cmp.rs index 46d4409..e678b29 100644 --- a/src/impl_cmp.rs +++ b/src/impl_cmp.rs @@ -240,9 +240,44 @@ where #[cfg(test)] -mod test_bigintref { +mod tests { use super::*; - use stdlib::ops::Neg; + + macro_rules! impl_test { + ($name:ident: $a:literal > $b:literal e $e:literal) => { + impl_test!($name: $a Greater $b e $e); + }; + ($name:ident: $a:literal < $b:literal e $e:literal) => { + impl_test!($name: $a Less $b e $e); + }; + ($name:ident: $a:literal = $b:literal e $e:literal) => { + impl_test!($name: $a Equal $b e $e); + }; + ($name:ident: $a:literal $op:ident $b:literal e $e:literal) => { + #[test] + fn $name() { + let a: BigUint = $a.parse().unwrap(); + let b: BigUint = $b.parse().unwrap(); + + let result = compare_scaled_biguints(&a, &b, $e); + debug_assert_eq!(result, Ordering::$op); + } + }; + } + + impl_test!(case_500_51e1: "500" < "51" e 1); + impl_test!(case_500_44e1: "500" > "44" e 1); + impl_test!(case_5000_50e2: "5000" = "50" e 2); + impl_test!(case_1234e9_12345e9: "1234000000000" < "12345" e 9); + impl_test!(case_1116xx_759xe2: "1116386634271380982470843247639640260491505327092723527088459" < "759522625769651746138617259189939751893902453291243506584717" e 2); + + #[test] + fn test_compare_extreme() { + let teenytiny: BigDecimal = "1e-9223372036854775807".parse().unwrap(); + let one = BigDecimal::from(1u8); + assert!(one > teenytiny); + assert!(teenytiny < one); + } #[test] fn test_borrow_neg_cmp() { @@ -256,4 +291,28 @@ mod test_bigintref { assert_ne!(x_ref.neg(), x_ref); assert_eq!(x_ref.neg().neg(), x_ref); } + + #[cfg(property_tests)] + mod prop { + use super::*; + use proptest::prelude::*; + + proptest! { + #![proptest_config(ProptestConfig { cases: 5000, ..Default::default() })] + + #[test] + fn cmp_matches_f64( + f in proptest::num::f64::NORMAL | proptest::num::f64::SUBNORMAL | proptest::num::f64::ZERO, + g in proptest::num::f64::NORMAL | proptest::num::f64::SUBNORMAL | proptest::num::f64::ZERO + ) { + let a: BigDecimal = BigDecimal::from_f64(f).unwrap(); + let b: BigDecimal = BigDecimal::from_f64(g).unwrap(); + + let expected = PartialOrd::partial_cmp(&f, &g).unwrap(); + let value = a.cmp(&b); + + prop_assert_eq!(expected, value) + } + } + } } From 901f9e455b10d682e6dd447842797ccfd0960c13 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Fri, 29 Mar 2024 13:02:09 -0400 Subject: [PATCH 09/50] Add function arithmetic::checked_diff --- src/arithmetic/mod.rs | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/arithmetic/mod.rs b/src/arithmetic/mod.rs index 46644bc..afc29b8 100644 --- a/src/arithmetic/mod.rs +++ b/src/arithmetic/mod.rs @@ -1,6 +1,7 @@ //! arithmetic routines use crate::*; +use num_traits::CheckedSub; pub(crate) mod addition; pub(crate) mod sqrt; @@ -86,14 +87,28 @@ pub(crate) fn count_decimal_digits_uint(uint: &BigUint) -> u64 { /// Return difference of two numbers, returning diff as u64 pub(crate) fn diff(a: T, b: T) -> (Ordering, u64) where - T: ToPrimitive + stdlib::ops::Sub + stdlib::cmp::Ord + T: ToPrimitive + CheckedSub + stdlib::cmp::Ord { use stdlib::cmp::Ordering::*; + let (ord, diff) = checked_diff(a, b); + + (ord, diff.expect("subtraction overflow")) +} + +/// Return difference of two numbers. If num doesn't fit in u64, return None +pub(crate) fn checked_diff(a: T, b: T) -> (Ordering, Option) +where + T: ToPrimitive + CheckedSub + stdlib::cmp::Ord +{ + use stdlib::cmp::Ordering::*; + + let _try_subtracting = |x:T, y:T| x.checked_sub(&y).and_then(|diff| diff.to_u64()); + match a.cmp(&b) { - Less => (Less, (b - a).to_u64().unwrap()), - Greater => (Greater, (a - b).to_u64().unwrap()), - Equal => (Equal, 0), + Less => (Less, _try_subtracting(b, a)), + Greater => (Greater, _try_subtracting(a, b)), + Equal => (Equal, Some(0)), } } From d30fe8d58ca9ad4374ac80380caac963e478a5df Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Fri, 29 Mar 2024 13:02:54 -0400 Subject: [PATCH 10/50] Use checked_diff in Ord implementation --- src/impl_cmp.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/impl_cmp.rs b/src/impl_cmp.rs index e678b29..0f18a99 100644 --- a/src/impl_cmp.rs +++ b/src/impl_cmp.rs @@ -129,13 +129,21 @@ impl Ord for BigDecimalRef<'_> { return Ordering::Equal; } - let result = match arithmetic::diff(self.scale, other.scale) { - (Greater, scale_diff) | (Equal, scale_diff) => { + let result = match arithmetic::checked_diff(self.scale, other.scale) { + (Greater, Some(scale_diff)) | (Equal, Some(scale_diff)) => { compare_scaled_biguints(self.digits, other.digits, scale_diff) } - (Less, scale_diff) => { + (Less, Some(scale_diff)) => { compare_scaled_biguints(other.digits, self.digits, scale_diff).reverse() } + (res, None) => { + // The difference in scale does not fit in a u64, + // we can safely assume the value of digits do not matter + // (unless we have a 2^64 (i.e. ~16 exabyte) long number + + // larger scale means smaller number, reverse this ordering + res.reverse() + } }; if other.sign == Sign::Minus { From 168734428a67be53de5a2fb0c99c6fc809fb063b Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Fri, 29 Mar 2024 14:18:15 -0400 Subject: [PATCH 11/50] Reimplement equality with safer and faster algorithm --- src/impl_cmp.rs | 123 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 87 insertions(+), 36 deletions(-) diff --git a/src/impl_cmp.rs b/src/impl_cmp.rs index 0f18a99..8d5b484 100644 --- a/src/impl_cmp.rs +++ b/src/impl_cmp.rs @@ -26,52 +26,103 @@ where { fn eq(&self, rhs: &T) -> bool { let rhs: BigDecimalRef<'rhs> = (*rhs).into(); + check_equality_bigdecimal_ref(*self, rhs) + } +} - match (self.sign(), rhs.sign()) { - // both zero - (Sign::NoSign, Sign::NoSign) => return true, - // signs are different - (a, b) if a != b => return false, - // signs are same, do nothing - _ => {} - } +fn check_equality_bigdecimal_ref(lhs: BigDecimalRef, rhs: BigDecimalRef) -> bool { + match (lhs.sign(), rhs.sign()) { + // both zero + (Sign::NoSign, Sign::NoSign) => return true, + // signs are different + (a, b) if a != b => return false, + // signs are same, do nothing + _ => {} + } - let unscaled_int; - let scaled_int; - let trailing_zero_count; - match self.scale.cmp(&rhs.scale) { - Ordering::Greater => { - unscaled_int = self.digits; - scaled_int = rhs.digits; - trailing_zero_count = (self.scale - rhs.scale) as usize; - } - Ordering::Less => { - unscaled_int = rhs.digits; - scaled_int = self.digits; - trailing_zero_count = (rhs.scale - self.scale) as usize; - } - Ordering::Equal => return self.digits == rhs.digits, + let unscaled_int; + let scaled_int; + let trailing_zero_count; + match arithmetic::checked_diff(lhs.scale, rhs.scale) { + (Ordering::Equal, _) => { + return lhs.digits == rhs.digits; } - - if trailing_zero_count < 20 { - let scaled_int = scaled_int * crate::ten_to_the(trailing_zero_count as u64).magnitude(); - return &scaled_int == unscaled_int; + (Ordering::Greater, Some(scale_diff)) => { + unscaled_int = lhs.digits; + scaled_int = rhs.digits; + trailing_zero_count = scale_diff; } + (Ordering::Less, Some(scale_diff)) => { + unscaled_int = rhs.digits; + scaled_int = lhs.digits; + trailing_zero_count = scale_diff; + } + _ => { + // all other cases imply overflow in difference of scale, + // numbers must not be equal + return false; + } + } - let unscaled_digits = unscaled_int.to_radix_le(10); - let scaled_digits = scaled_int.to_radix_le(10); + // try compare without allocating + if trailing_zero_count < 20 { + let pow = ten_to_the_u64(trailing_zero_count as u8); - // different lengths with trailing zeros - if unscaled_digits.len() != scaled_digits.len() + trailing_zero_count { - return false; + let mut a_digits = unscaled_int.iter_u32_digits(); + let mut b_digits = scaled_int.iter_u32_digits(); + + let mut carry = 0; + loop { + match (a_digits.next(), b_digits.next()) { + (Some(next_a), Some(next_b)) => { + let wide_b = match (next_b as u64).checked_mul(pow) { + Some(tmp) => tmp + carry, + None => break, + }; + + let true_b = wide_b as u32; + + if next_a != true_b { + return false; + } + + carry = wide_b >> 32; + } + (None, Some(_)) => { + return false; + } + (Some(a_digit), None) => { + if a_digit != (carry as u32) { + return false + } + carry = 0; + } + (None, None) => { + return carry == 0; + } + } } - // add leading zero digits to digits that need scaled - let scaled = iter::repeat(&0u8).take(trailing_zero_count).chain(scaled_digits.iter()); + // we broke out of loop due to overflow - compare via allocation + let scaled_int = scaled_int * pow; + return &scaled_int == unscaled_int; + } + + let trailing_zero_count = trailing_zero_count.to_usize().unwrap(); - // return true if all digits are the same - unscaled_digits.iter().zip(scaled).all(|(digit_a, digit_b)| digit_a == digit_b) + let unscaled_digits = unscaled_int.to_radix_le(10); + let scaled_digits = scaled_int.to_radix_le(10); + + // different lengths with trailing zeros + if unscaled_digits.len() != scaled_digits.len() + trailing_zero_count { + return false; } + + // add leading zero digits to digits that need scaled + let scaled = iter::repeat(&0u8).take(trailing_zero_count).chain(scaled_digits.iter()); + + // return true if all digits are the same + unscaled_digits.iter().zip(scaled).all(|(digit_a, digit_b)| digit_a == digit_b) } From f4b09ff2d1b574dbe56e20077faf6d459715e2cb Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Fri, 29 Mar 2024 15:46:30 -0400 Subject: [PATCH 12/50] Test new implementations of PartialEq and Ord --- src/impl_cmp.rs | 107 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 76 insertions(+), 31 deletions(-) diff --git a/src/impl_cmp.rs b/src/impl_cmp.rs index 8d5b484..9f2ab5e 100644 --- a/src/impl_cmp.rs +++ b/src/impl_cmp.rs @@ -299,43 +299,88 @@ where #[cfg(test)] -mod tests { +mod test { use super::*; - macro_rules! impl_test { - ($name:ident: $a:literal > $b:literal e $e:literal) => { - impl_test!($name: $a Greater $b e $e); - }; - ($name:ident: $a:literal < $b:literal e $e:literal) => { - impl_test!($name: $a Less $b e $e); - }; - ($name:ident: $a:literal = $b:literal e $e:literal) => { - impl_test!($name: $a Equal $b e $e); - }; - ($name:ident: $a:literal $op:ident $b:literal e $e:literal) => { - #[test] - fn $name() { - let a: BigUint = $a.parse().unwrap(); - let b: BigUint = $b.parse().unwrap(); + mod compare_scaled_biguints { + use super::*; - let result = compare_scaled_biguints(&a, &b, $e); - debug_assert_eq!(result, Ordering::$op); - } - }; + macro_rules! impl_test { + ($name:ident: $a:literal > $b:literal e $e:literal) => { + impl_test!($name: $a Greater $b e $e); + }; + ($name:ident: $a:literal < $b:literal e $e:literal) => { + impl_test!($name: $a Less $b e $e); + }; + ($name:ident: $a:literal = $b:literal e $e:literal) => { + impl_test!($name: $a Equal $b e $e); + }; + ($name:ident: $a:literal $op:ident $b:literal e $e:literal) => { + #[test] + fn $name() { + let a: BigUint = $a.parse().unwrap(); + let b: BigUint = $b.parse().unwrap(); + + let result = compare_scaled_biguints(&a, &b, $e); + assert_eq!(result, Ordering::$op); + } + }; + } + + impl_test!(case_500_51e1: "500" < "51" e 1); + impl_test!(case_500_44e1: "500" > "44" e 1); + impl_test!(case_5000_50e2: "5000" = "50" e 2); + impl_test!(case_1234e9_12345e9: "1234000000000" < "12345" e 9); + impl_test!(case_1116xx459_759xx717e2: "1116386634271380982470843247639640260491505327092723527088459" < "759522625769651746138617259189939751893902453291243506584717" e 2); } - impl_test!(case_500_51e1: "500" < "51" e 1); - impl_test!(case_500_44e1: "500" > "44" e 1); - impl_test!(case_5000_50e2: "5000" = "50" e 2); - impl_test!(case_1234e9_12345e9: "1234000000000" < "12345" e 9); - impl_test!(case_1116xx_759xe2: "1116386634271380982470843247639640260491505327092723527088459" < "759522625769651746138617259189939751893902453291243506584717" e 2); + mod ord { + use super::*; - #[test] - fn test_compare_extreme() { - let teenytiny: BigDecimal = "1e-9223372036854775807".parse().unwrap(); - let one = BigDecimal::from(1u8); - assert!(one > teenytiny); - assert!(teenytiny < one); + macro_rules! impl_test { + ($name:ident: $a:literal < $b:literal) => { + #[test] + fn $name() { + let a: BigDecimal = $a.parse().unwrap(); + let b: BigDecimal = $b.parse().unwrap(); + + assert!(&a < &b); + assert!(&b > &a); + assert_ne!(a, b); + } + }; + } + + impl_test!(case_diff_signs: "-1" < "1"); + impl_test!(case_n1_0: "-1" < "0"); + impl_test!(case_0_1: "0" < "1"); + impl_test!(case_1d2345_1d2346: "1.2345" < "1.2346"); + impl_test!(case_compare_extreme: "1e-9223372036854775807" < "1"); + impl_test!(case_compare_extremes: "1e-9223372036854775807" < "1e9223372036854775807"); + impl_test!(case_small_difference: "472697816888807260.1604" < "472697816888807260.16040000000000000000001"); + impl_test!(case_very_small_diff: "-1.0000000000000000000000000000000000000000000000000001" < "-1"); + } + + mod eq { + use super::*; + + macro_rules! impl_test { + ($name:ident: $a:literal = $b:literal) => { + #[test] + fn $name() { + let a: BigDecimal = $a.parse().unwrap(); + let b: BigDecimal = $b.parse().unwrap(); + + assert_eq!(&a, &b); + assert_eq!(a, b); + } + }; + } + + impl_test!(case_zero: "0" = "0.00"); + impl_test!(case_1_1d00: "1" = "1.00"); + impl_test!(case_n1_n1000en3: "-1" = "-1000e-3"); + impl_test!(case_0d000034500_345en7: "0.000034500" = "345e-7"); } #[test] From 3683c9662c69082cb23591f67bc1b4ff63b978f9 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Fri, 29 Mar 2024 16:17:43 -0400 Subject: [PATCH 13/50] Replace unwrap in count_decimal_digits_uint --- src/arithmetic/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/arithmetic/mod.rs b/src/arithmetic/mod.rs index afc29b8..c6df8fc 100644 --- a/src/arithmetic/mod.rs +++ b/src/arithmetic/mod.rs @@ -75,7 +75,7 @@ pub(crate) fn count_decimal_digits_uint(uint: &BigUint) -> u64 { } let mut digits = (uint.bits() as f64 / LOG2_10) as u64; // guess number of digits based on number of bits in UInt - let mut num = ten_to_the(digits).to_biguint().expect("Ten to power is negative"); + let mut num = ten_to_the_uint(digits); while *uint >= num { num *= 10u8; digits += 1; From 335f9815dfc5038a71fc4c4313c436da96ac7850 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Fri, 29 Mar 2024 18:21:40 -0400 Subject: [PATCH 14/50] Optimize and add new tests to equality check --- src/impl_cmp.rs | 44 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/src/impl_cmp.rs b/src/impl_cmp.rs index 9f2ab5e..4e6c43e 100644 --- a/src/impl_cmp.rs +++ b/src/impl_cmp.rs @@ -64,6 +64,20 @@ fn check_equality_bigdecimal_ref(lhs: BigDecimalRef, rhs: BigDecimalRef) -> bool } } + debug_assert_ne!(trailing_zero_count, 0); + + // multiplying by 10^trailing_zero_count guarantees shifting highest + // bit in 'scaled_int' to beyond the highest bit in 'unscaled_int', + // we know they are not equal + // + // x * 10^t > y + // log(x) + t * log(10) > log(y) + // + let ten_to_trailing_zero_bits = LOG2_10 * (64 - trailing_zero_count.leading_zeros()) as f64; + if ten_to_trailing_zero_bits as u64 + scaled_int.bits() > unscaled_int.bits() { + return false; + } + // try compare without allocating if trailing_zero_count < 20 { let pow = ten_to_the_u64(trailing_zero_count as u8); @@ -109,20 +123,29 @@ fn check_equality_bigdecimal_ref(lhs: BigDecimalRef, rhs: BigDecimalRef) -> bool } let trailing_zero_count = trailing_zero_count.to_usize().unwrap(); - let unscaled_digits = unscaled_int.to_radix_le(10); + + if trailing_zero_count > unscaled_digits.len() { + return false; + } + + // split into digits below the other value, and digits overlapping + let (low_digits, overlap_digits) = unscaled_digits.split_at(trailing_zero_count); + + // if any of the low digits are zero, they are not equal + if low_digits.iter().any(|&d| d != 0) { + return false; + } + let scaled_digits = scaled_int.to_radix_le(10); // different lengths with trailing zeros - if unscaled_digits.len() != scaled_digits.len() + trailing_zero_count { + if overlap_digits.len() != scaled_digits.len() { return false; } - // add leading zero digits to digits that need scaled - let scaled = iter::repeat(&0u8).take(trailing_zero_count).chain(scaled_digits.iter()); - // return true if all digits are the same - unscaled_digits.iter().zip(scaled).all(|(digit_a, digit_b)| digit_a == digit_b) + overlap_digits.iter().zip(scaled_digits.iter()).all(|(digit_a, digit_b)| digit_a == digit_b) } @@ -291,8 +314,9 @@ where (Some(a), Some(scaled_b)) => Some(a.cmp(&scaled_b)), // if scaled_b doesn't fit in size T, while 'a' does, then a is certainly less (Some(_), None) => Some(Ordering::Less), - // if scaled_b doesn't fit in size T, while 'a' does, then a is certainly less + // if a doesn't fit in size T, while 'scaled_b' does, then a is certainly greater (None, Some(_)) => Some(Ordering::Greater), + // neither fits, cannot determine relative size (None, None) => None, } } @@ -359,6 +383,12 @@ mod test { impl_test!(case_compare_extremes: "1e-9223372036854775807" < "1e9223372036854775807"); impl_test!(case_small_difference: "472697816888807260.1604" < "472697816888807260.16040000000000000000001"); impl_test!(case_very_small_diff: "-1.0000000000000000000000000000000000000000000000000001" < "-1"); + + impl_test!(case_1_2p128: "1" < "340282366920938463463374607431768211455"); + impl_test!(case_1_1e39: "1000000000000000000000000000000000000000" < "1e41"); + + impl_test!(case_1d414xxx573: "1.414213562373095048801688724209698078569671875376948073176679730000000000000000000000000000000000000" < "1.41421356237309504880168872420969807856967187537694807317667974000000000"); + impl_test!(case_11d414xxx573: "1.414213562373095048801688724209698078569671875376948073176679730000000000000000000000000000000000000" < "11.41421356237309504880168872420969807856967187537694807317667974000000000"); } mod eq { From e0cf671b78972660d1a3b15a44362395904908cc Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Fri, 29 Mar 2024 19:45:23 -0400 Subject: [PATCH 15/50] Add highest-bit optimization to compare functions --- src/impl_cmp.rs | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/impl_cmp.rs b/src/impl_cmp.rs index 4e6c43e..69be708 100644 --- a/src/impl_cmp.rs +++ b/src/impl_cmp.rs @@ -66,15 +66,9 @@ fn check_equality_bigdecimal_ref(lhs: BigDecimalRef, rhs: BigDecimalRef) -> bool debug_assert_ne!(trailing_zero_count, 0); - // multiplying by 10^trailing_zero_count guarantees shifting highest - // bit in 'scaled_int' to beyond the highest bit in 'unscaled_int', - // we know they are not equal - // - // x * 10^t > y - // log(x) + t * log(10) > log(y) - // - let ten_to_trailing_zero_bits = LOG2_10 * (64 - trailing_zero_count.leading_zeros()) as f64; - if ten_to_trailing_zero_bits as u64 + scaled_int.bits() > unscaled_int.bits() { + // test if unscaled_int is guaranteed to be less than + // scaled_int*10^trailing_zero_count based on highest bit + if highest_bit_lessthan_scaled(unscaled_int, scaled_int, trailing_zero_count) { return false; } @@ -238,6 +232,11 @@ fn compare_scaled_biguints(a: &BigUint, b: &BigUint, scale_diff: u64) -> Orderin return a.cmp(b); } + // check if highest bit of a is less than b * 10^scale_diff + if highest_bit_lessthan_scaled(a, b, scale_diff) { + return Ordering::Less; + } + // if biguints fit it u64 or u128, compare using those (avoiding allocations) if let Some(result) = compare_scalar_biguints(a, b, scale_diff) { return result; @@ -321,6 +320,27 @@ where } } +/// Return highest_bit(a) < highest_bit(b * 10^{scale}) +/// +/// Used for optimization when comparing scaled integers +/// +/// ```math +/// a < b * 10^{scale} +/// log(a) < log(b) + scale * log(10) +/// ``` +/// +fn highest_bit_lessthan_scaled(a: &BigUint, b: &BigUint, scale: u64) -> bool { + let a_bits = a.bits(); + let b_bits = b.bits(); + if a_bits < b_bits { + return true; + } + let log_scale = LOG2_10 * scale as f64; + match b_bits.checked_add(log_scale as u64) { + Some(scaled_b_bit) => a_bits < scaled_b_bit, + None => true, // overflowing u64 means we are definitely bigger + } +} #[cfg(test)] mod test { From b9f283891d715994b07bb1aef1acbadc44709335 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Fri, 29 Mar 2024 19:47:47 -0400 Subject: [PATCH 16/50] Touch build.rs in script/bigdecimal-property-tests After restoring .bak files, the timestamps require updating so cargo and tools re-run it. --- scripts/bigdecimal-property-tests | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/bigdecimal-property-tests b/scripts/bigdecimal-property-tests index 7141277..6f4652c 100755 --- a/scripts/bigdecimal-property-tests +++ b/scripts/bigdecimal-property-tests @@ -33,6 +33,7 @@ restore_disabled_property_tests() { # Restore Cargo.toml with backup mv Cargo.toml.bak Cargo.toml mv build.rs.bak build.rs + touch build.rs } From 8344feccac8c83da863a06a9d7cb25cd932c9bd3 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sat, 30 Mar 2024 17:24:10 -0400 Subject: [PATCH 17/50] Fix clippy suggestions --- src/impl_fmt.rs | 2 +- src/impl_num.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index d4710f9..e5dc677 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -230,7 +230,7 @@ fn format_exponential( // // For the (abs_int.len() - this.scale) == abs_int.len() I couldn't // come up with an example - let exponent = abs_int.len() as i128 + exp as i128 - 1; + let exponent = abs_int.len() as i128 + exp - 1; if abs_int.len() > 1 { // only add decimal point if there is more than 1 decimal digit diff --git a/src/impl_num.rs b/src/impl_num.rs index 32d1e5d..c86681c 100644 --- a/src/impl_num.rs +++ b/src/impl_num.rs @@ -89,8 +89,7 @@ impl Num for BigDecimal { // Return error if anything overflows outside i64 boundary. let scale = decimal_offset .checked_sub(exponent_value) - .map(|scale| scale.to_i64()) - .flatten() + .and_then(|scale| scale.to_i64()) .ok_or_else(|| ParseBigDecimalError::Other( format!("Exponent overflow when parsing '{}'", s)) From ded7f6c7e150589f18d7b70b1e1641892805cbb3 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sun, 14 Apr 2024 14:43:32 -0400 Subject: [PATCH 18/50] Reimplement formatting routines --- src/impl_fmt.rs | 167 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 111 insertions(+), 56 deletions(-) diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index e5dc677..9acd68e 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -96,60 +96,81 @@ fn format_full_scale( debug_assert_ne!(digits.len(), 0); - match f.precision() { - // precision limits the number of digits - we have to round - Some(prec) if prec < digits.len() && 1 < digits.len() => { - apply_rounding_to_ascii_digits(&mut digits, &mut exp, prec, this.sign); - debug_assert_eq!(digits.len(), prec); - }, - _ => { - // not limited by precision - } - }; - - // add the decimal point to 'digits' buffer - match exp.cmp(&0) { - // do not add decimal point for "full" integer - Equal => { - } - - // never decimal point if only one digit long - Greater if digits.len() == 1 => { + if this.scale <= 0 { + if let Some(prec) = f.precision() { + digits.resize(digits.len() + this.scale.neg() as usize, b'0'); + if prec > 0 { + digits.push(b'.'); + digits.resize(digits.len() + prec as usize, b'0'); + } + exp = 0; } + } else { + let scale = this.scale as usize; + let prec = f.precision().unwrap_or(scale); - // we format with scientific notation if exponent is positive - Greater => { - debug_assert!(digits.len() > 1); - - // increase exp by len(digits)-1 (eg [ddddd]E+{exp} => [d.dddd]E+{exp+4}) - exp += digits.len() as i128 - 1; + if scale < digits.len() { + // there are both integer and fractional digits + let integer_digit_count = digits.len() - scale; - // push decimal point and rotate it to index '1' - digits.push(b'.'); - digits[1..].rotate_right(1); - } - - // decimal point is within the digits (ddd.ddddddd) - Less if (-exp as usize) < digits.len() => { - let digits_to_shift = digits.len() - exp.abs() as usize; - digits.push(b'.'); - digits[digits_to_shift..].rotate_right(1); + if prec < scale { + apply_rounding_to_ascii_digits( + &mut digits, &mut exp, integer_digit_count + prec, this.sign + ); + } - // exp = 0 means exponential-part will be ignored in output - exp = 0; - } + if prec != 0 { + digits.insert(integer_digit_count, b'.'); + } - // decimal point is to the left of digits (0.0000dddddddd) - Less => { - let digits_to_shift = exp.abs() as usize - digits.len(); + if scale < prec { + // precision required beyond scale + digits.resize(digits.len() + (prec - scale), b'0'); + } - digits.push(b'0'); - digits.push(b'.'); - digits.extend(stdlib::iter::repeat(b'0').take(digits_to_shift)); - digits.rotate_right(digits_to_shift + 2); + } else { + // there are no integer digits + let leading_zeros = scale - digits.len(); + + match prec.checked_sub(leading_zeros) { + None => { + digits.clear(); + digits.push(b'0'); + if prec > 0 { + digits.push(b'.'); + digits.resize(2 + prec, b'0'); + } + } + Some(0) => { + // precision is at the decimal digit boundary, round one value + let insig_digit = digits[0] - b'0'; + let trailing_zeros = digits[1..].iter().all(|&d| d == b'0'); + let rounded_value = Context::default().round_pair(this.sign, 0, insig_digit, trailing_zeros); + + digits.clear(); + if leading_zeros != 0 { + digits.push(b'0'); + digits.push(b'.'); + digits.resize(1 + leading_zeros, b'0'); + } + digits.push(rounded_value + b'0'); + } + Some(digit_prec) => { + let trailing_zeros = digit_prec.saturating_sub(digits.len()); + if digit_prec < digits.len() { + apply_rounding_to_ascii_digits(&mut digits, &mut exp, digit_prec, this.sign); + } + digits.extend_from_slice(b"0."); + digits.resize(digits.len() + leading_zeros, b'0'); + digits.rotate_right(leading_zeros + 2); - exp = 0; + // add any extra trailing zeros + digits.resize(digits.len() + trailing_zeros, b'0'); + } + } } + // never print exp when in this branch + exp = 0; } // move digits back into String form @@ -177,16 +198,47 @@ fn format_exponential( // 3. Place decimal point after a single digit of the number, or omit if there is only a single digit // 4. Append `E{exponent}` and format the resulting string based on some `Formatter` flags - let mut exp = (this.scale as i128).neg(); - let mut digits = abs_int.into_bytes(); + let exp = (this.scale as i128).neg(); + let digits = abs_int.into_bytes(); - if digits.len() > 1 { - // only modify for precision if there is more than 1 decimal digit - if let Some(prec) = f.precision() { - apply_rounding_to_ascii_digits(&mut digits, &mut exp, prec, this.sign); + format_exponential_bigendian_ascii_digits( + digits, this.sign, exp, f, e_symbol + ) +} + + +fn format_exponential_bigendian_ascii_digits( + mut digits: Vec, + sign: Sign, + mut exp: i128, + f: &mut fmt::Formatter, + e_symbol: &str, +) -> fmt::Result { + // how many zeros to pad at the end of the decimal + let mut extra_trailing_zero_count = 0; + + if let Some(prec) = f.precision() { + // 'prec' is number of digits after the decimal point + let total_prec = prec + 1; + let digit_count = digits.len(); + + match total_prec.cmp(&digit_count) { + Ordering::Equal => { + // digit count is one more than precision - do nothing + } + Ordering::Less => { + // round to smaller precision + apply_rounding_to_ascii_digits(&mut digits, &mut exp, total_prec, sign); + } + Ordering::Greater => { + // increase number of zeros to add to end of digits + extra_trailing_zero_count = total_prec - digit_count; + } } } + let needs_decimal_point = digits.len() > 1 || extra_trailing_zero_count > 0; + let mut abs_int = String::from_utf8(digits).unwrap(); // Determine the exponent value based on the scale @@ -232,16 +284,19 @@ fn format_exponential( // come up with an example let exponent = abs_int.len() as i128 + exp - 1; - if abs_int.len() > 1 { + if needs_decimal_point { // only add decimal point if there is more than 1 decimal digit abs_int.insert(1, '.'); } - if exponent != 0 { - write!(abs_int, "{}{:+}", e_symbol, exponent)?; + if extra_trailing_zero_count > 0 { + abs_int.extend(stdlib::iter::repeat('0').take(extra_trailing_zero_count)); } - let non_negative = matches!(this.sign(), Sign::Plus | Sign::NoSign); + // always print exponent in exponential mode + write!(abs_int, "{}{:+}", e_symbol, exponent)?; + + let non_negative = matches!(sign, Sign::Plus | Sign::NoSign); //pad_integral does the right thing although we have a decimal f.pad_integral(non_negative, "", &abs_int) } From 2bb507335e08c66932adb4d15f57c8331f34b929 Mon Sep 17 00:00:00 2001 From: Nikola Ilo Date: Thu, 24 May 2018 03:20:52 +0200 Subject: [PATCH 19/50] Restore expected results for test_fmt Reverts test contents to those originally added in commit cdb05cfb42998b07b --- src/impl_fmt.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index 9acd68e..a56c4ef 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -629,14 +629,14 @@ mod test { #[test] fn test_fmt() { let vals = vec![ - // b s ( {} {:.1} {:.4} {:4.1} {:+05.7} {:<6.4} - (1, 0, ( "1", "1", "1", " 1", "+0001", "1 " )), - (1, 1, ( "0.1", "0.1", "0.1", " 0.1", "+00.1", "0.1 " )), - (1, 2, ( "0.01", "0.01", "0.01", "0.01", "+0.01", "0.01 " )), - (1, -2, ( "1E+2", "1E+2", "1E+2", "1E+2", "+1E+2", "1E+2 " )), - (-1, 0, ( "-1", "-1", "-1", " -1", "-0001", "-1 " )), - (-1, 1, ( "-0.1", "-0.1", "-0.1", "-0.1", "-00.1", "-0.1 " )), - (-1, 2, ("-0.01", "-0.01", "-0.01", "-0.01", "-0.01", "-0.01 " )), + // b s ( {} {:.1} {:.4} {:4.1} {:+05.1} {:<4.1} + (1, 0, ( "1", "1.0", "1.0000", " 1.0", "+01.0", "1.0 " )), + (1, 1, ( "0.1", "0.1", "0.1000", " 0.1", "+00.1", "0.1 " )), + (1, 2, ( "0.01", "0.0", "0.0100", " 0.0", "+00.0", "0.0 " )), + (1, -2, ( "100", "100.0", "100.0000", "100.0", "+100.0", "100.0" )), + (-1, 0, ( "-1", "-1.0", "-1.0000", "-1.0", "-01.0", "-1.0" )), + (-1, 1, ( "-0.1", "-0.1", "-0.1000", "-0.1", "-00.1", "-0.1" )), + (-1, 2, ( "-0.01", "-0.0", "-0.0100", "-0.0", "-00.0", "-0.0" )), ]; for (i, scale, results) in vals { let x = BigDecimal::new(num_bigint::BigInt::from(i), scale); @@ -644,8 +644,8 @@ mod test { assert_eq!(format!("{:.1}", x), results.1); assert_eq!(format!("{:.4}", x), results.2); assert_eq!(format!("{:4.1}", x), results.3); - assert_eq!(format!("{:+05.7}", x), results.4); - assert_eq!(format!("{:<6.4}", x), results.5); + assert_eq!(format!("{:+05.1}", x), results.4); + assert_eq!(format!("{:<4.1}", x), results.5); } } From a6106a921f8263f8b5ddf2151556d60224b16ea5 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sun, 14 Apr 2024 14:51:09 -0400 Subject: [PATCH 20/50] Replace formatting tests --- src/impl_fmt.rs | 242 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 192 insertions(+), 50 deletions(-) diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index a56c4ef..bbbff41 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -433,6 +433,7 @@ fn apply_rounding_to_ascii_digits( #[cfg(test)] +#[allow(non_snake_case)] mod test { use super::*; use paste::*; @@ -516,12 +517,68 @@ mod test { } impl_case!(fmt_default: "{}" => "1"); - impl_case!(fmt_d1: "{:.1}" => "1"); - impl_case!(fmt_d4: "{:.4}" => "1"); - impl_case!(fmt_4d1: "{:4.1}" => " 1"); - impl_case!(fmt_r4d1: "{:>4.1}" => " 1"); - impl_case!(fmt_l4d1: "{:<4.1}" => "1 "); - impl_case!(fmt_p05d1: "{:+05.1}" => "+0001"); + impl_case!(fmt_d1: "{:.1}" => "1.0"); + impl_case!(fmt_d4: "{:.4}" => "1.0000"); + impl_case!(fmt_4d1: "{:4.1}" => " 1.0"); + impl_case!(fmt_r4d1: "{:>4.1}" => " 1.0"); + impl_case!(fmt_l4d1: "{:<4.1}" => "1.0 "); + impl_case!(fmt_p05d1: "{:+05.1}" => "+01.0"); + + impl_case!(fmt_e: "{:e}" => "1e+0"); + impl_case!(fmt_E: "{:E}" => "1E+0"); + } + + mod dec_1e1 { + use super::*; + + fn test_input() -> BigDecimal { + BigDecimal::new(1.into(), -1) + } + + impl_case!(fmt_default: "{}" => "10"); + impl_case!(fmt_d0: "{:.0}" => "1e1"); + impl_case!(fmt_d1: "{:.1}" => "1.0e1"); + impl_case!(fmt_d2: "{:.2}" => "1.00e1"); + } + + mod dec_1en1 { + use super::*; + + fn test_input() -> BigDecimal { + BigDecimal::new(1.into(), 1) + } + + impl_case!(fmt_default: "{}" => "0.1"); + impl_case!(fmt_d0: "{:.0}" => "0"); + impl_case!(fmt_d1: "{:.1}" => "0.1"); + impl_case!(fmt_d2: "{:.2}" => "0.10"); + } + + mod dec_9en1 { + use super::*; + + fn test_input() -> BigDecimal { + BigDecimal::new(9.into(), 1) + } + + impl_case!(fmt_default: "{}" => "0.9"); + impl_case!(fmt_d0: "{:.0}" => "1"); + impl_case!(fmt_d1: "{:.1}" => "0.9"); + impl_case!(fmt_d4: "{:.4}" => "0.9000"); + } + + mod dec_800en3 { + use super::*; + + fn test_input() -> BigDecimal { + BigDecimal::new(800.into(), 3) + } + + impl_case!(fmt_default: "{}" => "0.800"); + impl_case!(fmt_d0: "{:.0}" => "1"); + impl_case!(fmt_d1: "{:.1}" => "0.8"); + impl_case!(fmt_d3: "{:.3}" => "0.800"); + impl_case!(fmt_d9: "{:.9}" => "0.800000000"); } mod dec_123456 { @@ -532,13 +589,13 @@ mod test { } impl_case!(fmt_default: "{}" => "123456"); - impl_case!(fmt_p05d1: "{:+05.1}" => "+1E+5"); - impl_case!(fmt_d1: "{:.1}" => "1E+5"); - impl_case!(fmt_d4: "{:.4}" => "1.235E+5"); - impl_case!(fmt_4d1: "{:4.1}" => "1E+5"); - impl_case!(fmt_r4d3: "{:>4.3}" => "1.23E+5"); - impl_case!(fmt_r4d4: "{:>4.4}" => "1.235E+5"); - impl_case!(fmt_l4d1: "{:<4.1}" => "1E+5"); + impl_case!(fmt_d1: "{:.1}" => "123456.0"); + impl_case!(fmt_d4: "{:.4}" => "123456.0000"); + impl_case!(fmt_4d1: "{:4.1}" => "123456.0"); + impl_case!(fmt_15d2: "{:15.2}" => " 123456.00"); + impl_case!(fmt_r15d2: "{:>15.2}" => " 123456.00"); + impl_case!(fmt_l15d2: "{:<15.2}" => "123456.00 "); + impl_case!(fmt_p05d1: "{:+05.7}" => "+123456.0000000"); } mod dec_9999999 { @@ -548,8 +605,18 @@ mod test { "9999999".parse().unwrap() } - impl_case!(fmt_d4: "{:.4}" => "1.000E+7"); - impl_case!(fmt_d8: "{:.8}" => "9999999"); + impl_case!(fmt_default: "{}" => "9999999"); + impl_case!(fmt_d8: "{:.8}" => "9999999.00000000"); + + impl_case!(fmt_e: "{:e}" => "9.999999e+6"); + impl_case!(fmt_E: "{:E}" => "9.999999E+6"); + impl_case!(fmt_d0e: "{:.0e}" => "1e+7"); + impl_case!(fmt_d1e: "{:.1e}" => "1.0e+7"); + impl_case!(fmt_d2e: "{:.2e}" => "1.00e+7"); + impl_case!(fmt_d4e: "{:.4e}" => "1.0000e+7"); + impl_case!(fmt_d6e: "{:.6e}" => "9.999999e+6"); + impl_case!(fmt_d7e: "{:.7e}" => "9.9999990e+6"); + impl_case!(fmt_d10e: "{:.10e}" => "9.9999990000e+6"); } mod dec_19073d97235939614856 { @@ -560,13 +627,31 @@ mod test { } impl_case!(fmt_default: "{}" => "19073.97235939614856"); - impl_case!(fmt_p05d7: "{:+05.7}" => "+19073.97"); - impl_case!(fmt_d3: "{:.3}" => "1.91E+4"); - impl_case!(fmt_0d4: "{:0.4}" => "1.907E+4"); - impl_case!(fmt_4d1: "{:4.1}" => "2E+4"); - impl_case!(fmt_r8d3: "{:>8.3}" => " 1.91E+4"); - impl_case!(fmt_r8d4: "{:>8.4}" => "1.907E+4"); - impl_case!(fmt_l8d1: "{:<8.1}" => "2E+4 "); + impl_case!(fmt_pd7: "{:+.7}" => "+19073.9723594"); + impl_case!(fmt_d0: "{:.0}" => "19074"); + impl_case!(fmt_d1: "{:.1}" => "19074.0"); + impl_case!(fmt_d3: "{:.3}" => "19073.972"); + impl_case!(fmt_d4: "{:.4}" => "19073.9724"); + impl_case!(fmt_8d3: "{:8.3}" => "19073.972"); + impl_case!(fmt_10d3: "{:10.3}" => " 19073.972"); + impl_case!(fmt_010d3: "{:010.3}" => "019073.972"); + } + + mod dec_1764031078en13 { + use super::*; + + fn test_input() -> BigDecimal { + BigDecimal::new(1764031078.into(), 13) + } + + impl_case!(fmt_default: "{}" => "0.0001764031078"); + impl_case!(fmt_d1: "{:.1}" => "0.0"); + impl_case!(fmt_d3: "{:.3}" => "0.000"); + impl_case!(fmt_d4: "{:.4}" => "0.0002"); + impl_case!(fmt_d5: "{:.5}" => "0.00018"); + impl_case!(fmt_d13: "{:.13}" => "0.0001764031078"); + impl_case!(fmt_d20: "{:.20}" => "0.00017640310780000000"); + } mod dec_491326en12 { @@ -577,13 +662,91 @@ mod test { } impl_case!(fmt_default: "{}" => "4.91326E-7"); - impl_case!(fmt_p015d7: "{:+015.7}" => "+00004.91326E-7"); - impl_case!(fmt_d3: "{:.3}" => "4.91E-7"); - impl_case!(fmt_0d4: "{:0.4}" => "4.913E-7"); - impl_case!(fmt_4d1: "{:4.1}" => "5E-7"); - impl_case!(fmt_r8d3: "{:>8.3}" => " 4.91E-7"); - impl_case!(fmt_r8d4: "{:>8.4}" => "4.913E-7"); - impl_case!(fmt_l8d1: "{:<8.1}" => "5E-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"); + } + + mod dec_0d00003102564500 { + use super::*; + + fn test_input() -> BigDecimal { + "0.00003102564500".parse().unwrap() + } + + impl_case!(fmt_default: "{}" => "0.00003102564500"); + impl_case!(fmt_d0: "{:.0}" => "0"); + impl_case!(fmt_d4: "{:.4}" => "0.0000"); + impl_case!(fmt_d5: "{:.5}" => "0.00003"); + impl_case!(fmt_d10: "{:.10}" => "0.0000310256"); + impl_case!(fmt_d14: "{:.14}" => "0.00003102564500"); + impl_case!(fmt_d17: "{:.17}" => "0.00003102564500000"); + + impl_case!(fmt_e: "{:e}" => "3.102564500e-5"); + impl_case!(fmt_de: "{:.e}" => "3.102564500e-5"); + impl_case!(fmt_d0e: "{:.0e}" => "3e-5"); + impl_case!(fmt_d1e: "{:.1e}" => "3.1e-5"); + impl_case!(fmt_d4e: "{:.4e}" => "3.1026e-5"); + } + + mod dec_1en100000 { + use super::*; + + fn test_input() -> BigDecimal { + "1E-10000".parse().unwrap() + } + + impl_case!(fmt_default: "{}" => "1E-10000"); + impl_case!(fmt_d1: "{:.1}" => "1.0E-10000"); + impl_case!(fmt_d4: "{:.4}" => "1.0000E-10000"); + } + + mod dec_1e100000 { + use super::*; + + fn test_input() -> BigDecimal { + "1e10000".parse().unwrap() + } + + impl_case!(fmt_default: "{}" => "1E+10000"); + impl_case!(fmt_d1: "{:.1}" => "1.0E+10000"); + impl_case!(fmt_d4: "{:.4}" => "1.0000E+10000"); + } + + + mod dec_1234506789E5 { + use super::*; + + fn test_input() -> BigDecimal { + BigDecimal::new(1234506789.into(), -5) + } + + impl_case!(fmt_default: "{}" => "123450678900000"); + impl_case!(fmt_d1: "{:.1}" => "123450678900000.0"); + impl_case!(fmt_d3: "{:.3}" => "123450678900000.000"); + impl_case!(fmt_d4: "{:.4}" => "123450678900000.0000"); + impl_case!(fmt_l13d4: "{:<23.4}" => "123450678900000.0000 "); + impl_case!(fmt_r13d4: "{:>23.4}" => " 123450678900000.0000"); + } + + mod dec_1234506789E15 { + use super::*; + + fn test_input() -> BigDecimal { + BigDecimal::new(1234506789.into(), -15) + } + + impl_case!(fmt_default: "{}" => "1.234506789E+24"); + impl_case!(fmt_d1: "{:.1}" => "1.2E+24"); + impl_case!(fmt_d3: "{:.3}" => "1.235E+24"); + impl_case!(fmt_d4: "{:.4}" => "1.2345E+24"); + impl_case!(fmt_l13d4: "{:<13.4}" => "1.2345E+24 "); + impl_case!(fmt_r13d4: "{:>13.4}" => " 1.2345E+24"); } } @@ -649,27 +812,6 @@ mod test { } } - #[test] - fn test_fmt_with_large_values() { - let vals = vec![ - // b s ( {} {:.1} {:2.4} {:4.2} {:+05.7} {:<13.4} - // Numbers with large scales - (1, 10_000, ( "1E-10000", "1E-10000", "1E-10000", "1E-10000", "+1E-10000", "1E-10000 ")), - (1, -10_000, ( "1E+10000", "1E+10000", "1E+10000", "1E+10000", "+1E+10000", "1E+10000 ")), - // // Numbers with many digits - (1234506789, 5, ( "12345.06789", "1E+4", "1.235E+4", "1.2E+4", "+12345.07", "1.235E+4 ")), - (1234506789, -5, ( "1.234506789E+14", "1E+14", "1.235E+14", "1.2E+14", "+1.234507E+14", "1.235E+14 ")), - ]; - for (i, scale, results) in vals { - let x = BigDecimal::new(num_bigint::BigInt::from(i), scale); - assert_eq!(format!("{}", x), results.0, "digits={} scale={}", i, scale); - assert_eq!(format!("{:.1}", x), results.1, "digits={} scale={}", i, scale); - assert_eq!(format!("{:2.4}", x), results.2, "digits={} scale={}", i, scale); - assert_eq!(format!("{:4.2}", x), results.3, "digits={} scale={}", i, scale); - assert_eq!(format!("{:+05.7}", x), results.4, "digits={} scale={}", i, scale); - assert_eq!(format!("{:<13.4}", x), results.5, "digits={} scale={}", i, scale); - } - } mod fmt_debug { use super::*; From 5c91f49348fccc40400087d08c4e4594d1683f5c Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Wed, 17 Apr 2024 20:09:59 -0400 Subject: [PATCH 21/50] Add example script printing various decimal formats --- examples/formatting-examples.rs | 66 +++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 examples/formatting-examples.rs diff --git a/examples/formatting-examples.rs b/examples/formatting-examples.rs new file mode 100644 index 0000000..447dc51 --- /dev/null +++ b/examples/formatting-examples.rs @@ -0,0 +1,66 @@ + +extern crate bigdecimal; +extern crate num_traits; + +use bigdecimal::BigDecimal; + + +macro_rules! print_varying_values { + ($fmt:literal, $exp_range:expr) => { + print_varying_values!($fmt, $exp_range, 1); + }; + ($fmt:literal, $exp_range:expr, $value:literal) => { + println!("{}:", $fmt); + println!("{}×", $value); + for exp in $exp_range { + let n = BigDecimal::new($value.into(), -exp); + let dec_str = format!($fmt, n); + + let f = ($value as f64) * 10f64.powi(exp as i32); + let float_str = format!($fmt, f); + + let s = format!("10^{}", exp); + println!("{:>7} : {:25} {}", s, dec_str, float_str); + } + println!(""); + } +} + +macro_rules! print_varying_formats { + ($src:literal) => { + let src = $src; + let n: BigDecimal = src.parse().unwrap(); + let f: f64 = src.parse().unwrap(); + + println!("{}", src); + println!(" {{}} : {:<15} {}", n, f); + + for prec in 0..5 { + println!(" {{:.{prec}}} : {:<15.prec$} {:.prec$}", n, f, prec=prec); + } + println!(""); + }; +} + +fn main() { + println!(" | BigDecimal | f64"); + println!(" +------------ +-----"); + print_varying_formats!("1234"); + print_varying_formats!("1.234"); + print_varying_formats!(".1234"); + print_varying_formats!(".00001234"); + print_varying_formats!(".00000001234"); + print_varying_formats!("12340"); + print_varying_formats!("1234e1"); + print_varying_formats!("1234e10"); + + + print_varying_values!("{}", -20..=20); + print_varying_values!("{:e}", -20..=20); + print_varying_values!("{:.2e}", -20..=20); + + print_varying_values!("{}", -10..=10, 12345); + print_varying_values!("{:e}", -10..=10, 12345); + print_varying_values!("{:.2e}", -10..=10, 12345); + print_varying_values!("{:.3}", -10..=10, 12345); +} From d1419bc7a179046000966ade71d3a4f7a582ea9a Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sun, 21 Apr 2024 14:21:18 -0400 Subject: [PATCH 22/50] Clean build.rs environment variable loading --- build.rs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/build.rs b/build.rs index 0db0ca3..e6281cc 100644 --- a/build.rs +++ b/build.rs @@ -1,7 +1,11 @@ - use std::env; use std::path::{Path, PathBuf}; +// configuration defaults +const DEFAULT_PRECISION: &str = "100"; +const DEFAULT_ROUNDING_MODE: &str = "HalfEven"; +const EXPONENTIAL_FORMAT_THRESHOLD: &str = "5"; + fn main() { let ac = autocfg::new(); @@ -19,12 +23,17 @@ fn main() { write_exponential_format_threshold_file(&outdir); } +/// Loads the environment variable string or default +macro_rules! load_env { + ($env:ident, $name:literal, $default:ident) => {{ + println!("cargo:rerun-if-env-changed={}", $name); + $env::var($name).unwrap_or_else(|_| $default.to_owned()) + }}; +} /// Create default_precision.rs, containing definition of constant DEFAULT_PRECISION loaded in src/lib.rs fn write_default_precision_file(outdir: &Path) { - let env_var = env::var("RUST_BIGDECIMAL_DEFAULT_PRECISION").unwrap_or_else(|_| "100".to_owned()); - println!("cargo:rerun-if-env-changed=RUST_BIGDECIMAL_DEFAULT_PRECISION"); - + let env_var = load_env!(env, "RUST_BIGDECIMAL_DEFAULT_PRECISION", DEFAULT_PRECISION); let rust_file_path = outdir.join("default_precision.rs"); let default_prec: u32 = env_var @@ -39,8 +48,7 @@ fn write_default_precision_file(outdir: &Path) { /// Create default_rounding_mode.rs, using value of RUST_BIGDECIMAL_DEFAULT_ROUNDING_MODE environment variable fn write_default_rounding_mode(outdir: &Path) { - let rounding_mode_name = env::var("RUST_BIGDECIMAL_DEFAULT_ROUNDING_MODE").unwrap_or_else(|_| "HalfEven".to_owned()); - println!("cargo:rerun-if-env-changed=RUST_BIGDECIMAL_DEFAULT_ROUNDING_MODE"); + let rounding_mode_name = load_env!(env, "RUST_BIGDECIMAL_DEFAULT_ROUNDING_MODE", DEFAULT_ROUNDING_MODE); let rust_file_path = outdir.join("default_rounding_mode.rs"); let rust_file_contents = format!("const DEFAULT_ROUNDING_MODE: RoundingMode = RoundingMode::{};", rounding_mode_name); @@ -50,8 +58,7 @@ fn write_default_rounding_mode(outdir: &Path) { /// Create write_default_rounding_mode.rs, containing definition of constant EXPONENTIAL_FORMAT_THRESHOLD loaded in src/impl_fmt.rs fn write_exponential_format_threshold_file(outdir: &Path) { - let env_var = env::var("RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD").unwrap_or_else(|_| "5".to_owned()); - println!("cargo:rerun-if-env-changed=RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD"); + let env_var = load_env!(env, "RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD", EXPONENTIAL_FORMAT_THRESHOLD); let rust_file_path = outdir.join("exponential_format_threshold.rs"); From 5b653af0fb4c1a52d2fc5d346b217d82870a1599 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sun, 21 Apr 2024 20:25:05 -0400 Subject: [PATCH 23/50] Add EXPONENTIAL_FORMAT_UPPER_THRESHOLD variable in build.rs --- build.rs | 21 +++++++++++++++------ src/impl_fmt.rs | 1 + 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/build.rs b/build.rs index e6281cc..6485030 100644 --- a/build.rs +++ b/build.rs @@ -5,6 +5,7 @@ use std::path::{Path, PathBuf}; const DEFAULT_PRECISION: &str = "100"; const DEFAULT_ROUNDING_MODE: &str = "HalfEven"; const EXPONENTIAL_FORMAT_THRESHOLD: &str = "5"; +const EXPONENTIAL_FORMAT_UPPER_THRESHOLD: &str = "9"; fn main() { @@ -58,16 +59,24 @@ fn write_default_rounding_mode(outdir: &Path) { /// Create write_default_rounding_mode.rs, containing definition of constant EXPONENTIAL_FORMAT_THRESHOLD loaded in src/impl_fmt.rs fn write_exponential_format_threshold_file(outdir: &Path) { - let env_var = load_env!(env, "RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD", EXPONENTIAL_FORMAT_THRESHOLD); + let low_value = load_env!(env, "RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD", EXPONENTIAL_FORMAT_THRESHOLD); + let high_value = load_env!(env, "RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_UPPER_THRESHOLD", EXPONENTIAL_FORMAT_UPPER_THRESHOLD); - let rust_file_path = outdir.join("exponential_format_threshold.rs"); - - let value: u32 = env_var + let low_value: u32 = low_value .parse::() .expect("$RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD must be an integer > 0") .into(); - let rust_file_contents = format!("const EXPONENTIAL_FORMAT_THRESHOLD: i64 = {};", value); + let high_value: u32 = high_value + .parse::() + .expect("$RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_UPPER_THRESHOLD must be valid u32"); - std::fs::write(rust_file_path, rust_file_contents).unwrap(); + let rust_file_path = outdir.join("exponential_format_threshold.rs"); + + let rust_file_contents = [ + format!("const EXPONENTIAL_FORMAT_THRESHOLD: i64 = {};", low_value), + format!("const EXPONENTIAL_FORMAT_UPPER_THRESHOLD: i64 = {};", high_value), + ]; + + std::fs::write(rust_file_path, rust_file_contents.join("\n")).unwrap(); } diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index bbbff41..19ea9c1 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -6,6 +6,7 @@ use stdlib::fmt::Write; // const EXPONENTIAL_FORMAT_THRESHOLD: i64 = ${RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD} or 5; +// const EXPONENTIAL_FORMAT_UPPER_THRESHOLD: i64 = ${RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_UPPER_THRESHOLD} or 5; include!(concat!(env!("OUT_DIR"), "/exponential_format_threshold.rs")); From 5465b3892520d7cc0400ce9525d553bf52b7f9e3 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Mon, 22 Apr 2024 20:28:26 -0400 Subject: [PATCH 24/50] Double fmt threshold --- build.rs | 6 +++--- src/impl_fmt.rs | 42 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/build.rs b/build.rs index 6485030..6f4fd88 100644 --- a/build.rs +++ b/build.rs @@ -5,7 +5,7 @@ use std::path::{Path, PathBuf}; const DEFAULT_PRECISION: &str = "100"; const DEFAULT_ROUNDING_MODE: &str = "HalfEven"; const EXPONENTIAL_FORMAT_THRESHOLD: &str = "5"; -const EXPONENTIAL_FORMAT_UPPER_THRESHOLD: &str = "9"; +const EXPONENTIAL_FORMAT_UPPER_THRESHOLD: &str = "12"; fn main() { @@ -74,8 +74,8 @@ fn write_exponential_format_threshold_file(outdir: &Path) { let rust_file_path = outdir.join("exponential_format_threshold.rs"); let rust_file_contents = [ - format!("const EXPONENTIAL_FORMAT_THRESHOLD: i64 = {};", low_value), - format!("const EXPONENTIAL_FORMAT_UPPER_THRESHOLD: i64 = {};", high_value), + format!("const EXPONENTIAL_FORMAT_LEADING_ZERO_THRESHOLD: usize = {};", low_value), + format!("const EXPONENTIAL_FORMAT_TRAILING_ZERO_THRESHOLD: usize = {};", high_value), ]; std::fs::write(rust_file_path, rust_file_contents.join("\n")).unwrap(); diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index 19ea9c1..4b856b6 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -12,13 +12,23 @@ include!(concat!(env!("OUT_DIR"), "/exponential_format_threshold.rs")); impl fmt::Display for BigDecimal { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - dynamically_format_decimal(self.to_ref(), f, EXPONENTIAL_FORMAT_THRESHOLD) + dynamically_format_decimal( + self.to_ref(), + f, + EXPONENTIAL_FORMAT_LEADING_ZERO_THRESHOLD, + EXPONENTIAL_FORMAT_TRAILING_ZERO_THRESHOLD, + ) } } impl fmt::Display for BigDecimalRef<'_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - dynamically_format_decimal(*self, f, EXPONENTIAL_FORMAT_THRESHOLD) + dynamically_format_decimal( + *self, + f, + EXPONENTIAL_FORMAT_LEADING_ZERO_THRESHOLD, + EXPONENTIAL_FORMAT_TRAILING_ZERO_THRESHOLD, + ) } } @@ -68,15 +78,27 @@ impl fmt::Debug for BigDecimal { fn dynamically_format_decimal( this: BigDecimalRef, f: &mut fmt::Formatter, - threshold: i64, + leading_zero_threshold: usize, + trailing_zero_threshold: usize, ) -> fmt::Result { // Acquire the absolute integer as a decimal string let abs_int = this.digits.to_str_radix(10); - // use exponential form if decimal point is not "within" the number. - // "threshold" is max number of leading zeros before being considered - // "outside" the number - if this.scale < 0 || (this.scale > abs_int.len() as i64 + threshold) { + // number of zeros between most significant digit and decimal point + let leading_zero_count = this.scale + .to_usize() + .and_then(|scale| scale.checked_sub(abs_int.len())) + .unwrap_or(0); + + // number of zeros between least significant digit and decimal point + let trailing_zero_count = this.scale + .checked_neg() + .and_then(|d| d.to_usize()) + .unwrap_or(0); + + // use exponential form if decimal point is outside + // the upper and lower thresholds of the decimal + if leading_zero_threshold < leading_zero_count || trailing_zero_threshold < trailing_zero_count { format_exponential(this, f, abs_int, "E") } else { format_full_scale(this, f, abs_int) @@ -105,6 +127,10 @@ fn format_full_scale( digits.resize(digits.len() + prec as usize, b'0'); } exp = 0; + // } else if -(EXPONENTIAL_FORMAT_UPPER_THRESHOLD as i64) < this.scale { + // // dbg!(this.scale); + // digits.resize(digits.len() + this.scale.neg() as usize, b'0'); + // exp = 0; } } else { let scale = this.scale as usize; @@ -483,7 +509,7 @@ mod test { macro_rules! test_fmt_function { ($n:ident) => {{ - format!("{}", Fmt(|f| dynamically_format_decimal($n.to_ref(), f, 2))) + format!("{}", Fmt(|f| dynamically_format_decimal($n.to_ref(), f, 2, 9))) }}; } From 6386ff6a20799c1d477ff937f60a9e1cf63538ec Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sun, 12 May 2024 15:56:29 -0400 Subject: [PATCH 25/50] Reimplement tests for BigDecimal::normalized --- src/lib.rs | 46 +++++++++++++++++++++------------------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index a1b1a30..f692b90 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1968,33 +1968,29 @@ mod bigdecimal_tests { assert!((-BigDecimal::one()).abs().is_positive()); } - #[test] - fn test_normalize() { - use num_bigint::BigInt; - - let vals = vec![ - (BigDecimal::new(BigInt::from(10), 2), - BigDecimal::new(BigInt::from(1), 1), - "0.1"), - (BigDecimal::new(BigInt::from(132400), -4), - BigDecimal::new(BigInt::from(1324), -6), - "1.324E+9"), - (BigDecimal::new(BigInt::from(1_900_000), 3), - BigDecimal::new(BigInt::from(19), -2), - "1.9E+3"), - (BigDecimal::new(BigInt::from(0), -3), - BigDecimal::zero(), - "0"), - (BigDecimal::new(BigInt::from(0), 5), - BigDecimal::zero(), - "0"), - ]; + mod normalize { + use super::*; - for (not_normalized, normalized, string) in vals { - assert_eq!(not_normalized.normalized(), normalized); - assert_eq!(not_normalized.normalized().to_string(), string); - assert_eq!(normalized.to_string(), string); + macro_rules! impl_case { + ( $name:ident: ($i:literal, $s:literal) => ($e_int_val:literal, $e_scale:literal) ) => { + #[test] + fn $name() { + let d = BigDecimal::new($i.into(), $s); + let n = d.normalized(); + assert_eq!(n.int_val, $e_int_val.into()); + assert_eq!(n.scale, $e_scale); + } + } } + + impl_case!(case_0e3: (0, -3) => (0, 0)); + impl_case!(case_0en50: (0, 50) => (0, 0)); + impl_case!(case_10en2: (10, 2) => (1, 1)); + impl_case!(case_11en2: (11, 2) => (11, 2)); + impl_case!(case_132400en4: (132400, 4) => (1324, 2)); + impl_case!(case_1_900_000en3: (1_900_000, 3) => (19, -2)); + impl_case!(case_834700e4: (834700, -4) => (8347, -6)); + impl_case!(case_n834700e4: (-9900, 2) => (-99, 0)); } #[test] From 79f513e77f296eefccb277fcd6c9e1326de5da1b Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sun, 12 May 2024 16:07:54 -0400 Subject: [PATCH 26/50] Refactor format_full_scale into smaller implementation functions --- src/impl_fmt.rs | 193 ++++++++++++++++++++++++++++++------------------ 1 file changed, 122 insertions(+), 71 deletions(-) diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index 4b856b6..fe7f68b 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -5,8 +5,8 @@ use crate::*; use stdlib::fmt::Write; -// const EXPONENTIAL_FORMAT_THRESHOLD: i64 = ${RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD} or 5; -// const EXPONENTIAL_FORMAT_UPPER_THRESHOLD: i64 = ${RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_UPPER_THRESHOLD} or 5; +// const EXPONENTIAL_FORMAT_LEADING_ZERO_THRESHOLD: i64 = ${RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD} or 5; +// const EXPONENTIAL_FORMAT_TRAILING_ZERO_THRESHOLD: i64 = ${RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_UPPER_THRESHOLD} or 5; include!(concat!(env!("OUT_DIR"), "/exponential_format_threshold.rs")); @@ -120,81 +120,19 @@ fn format_full_scale( debug_assert_ne!(digits.len(), 0); if this.scale <= 0 { - if let Some(prec) = f.precision() { - digits.resize(digits.len() + this.scale.neg() as usize, b'0'); - if prec > 0 { - digits.push(b'.'); - digits.resize(digits.len() + prec as usize, b'0'); - } - exp = 0; - // } else if -(EXPONENTIAL_FORMAT_UPPER_THRESHOLD as i64) < this.scale { - // // dbg!(this.scale); - // digits.resize(digits.len() + this.scale.neg() as usize, b'0'); - // exp = 0; - } + // formating an integer value (add trailing zeros to the right) + zero_right_pad_integer_ascii_digits(&mut digits, &mut exp, f.precision()); } else { let scale = this.scale as usize; + // no-precision behaves the same as precision matching scale (i.e. no padding or rounding) let prec = f.precision().unwrap_or(scale); if scale < digits.len() { - // there are both integer and fractional digits - let integer_digit_count = digits.len() - scale; - - if prec < scale { - apply_rounding_to_ascii_digits( - &mut digits, &mut exp, integer_digit_count + prec, this.sign - ); - } - - if prec != 0 { - digits.insert(integer_digit_count, b'.'); - } - - if scale < prec { - // precision required beyond scale - digits.resize(digits.len() + (prec - scale), b'0'); - } - + // format both integer and fractional digits (always just 'trim') + trim_ascii_digits(&mut digits, scale, prec, &mut exp, this.sign); } else { - // there are no integer digits - let leading_zeros = scale - digits.len(); - - match prec.checked_sub(leading_zeros) { - None => { - digits.clear(); - digits.push(b'0'); - if prec > 0 { - digits.push(b'.'); - digits.resize(2 + prec, b'0'); - } - } - Some(0) => { - // precision is at the decimal digit boundary, round one value - let insig_digit = digits[0] - b'0'; - let trailing_zeros = digits[1..].iter().all(|&d| d == b'0'); - let rounded_value = Context::default().round_pair(this.sign, 0, insig_digit, trailing_zeros); - - digits.clear(); - if leading_zeros != 0 { - digits.push(b'0'); - digits.push(b'.'); - digits.resize(1 + leading_zeros, b'0'); - } - digits.push(rounded_value + b'0'); - } - Some(digit_prec) => { - let trailing_zeros = digit_prec.saturating_sub(digits.len()); - if digit_prec < digits.len() { - apply_rounding_to_ascii_digits(&mut digits, &mut exp, digit_prec, this.sign); - } - digits.extend_from_slice(b"0."); - digits.resize(digits.len() + leading_zeros, b'0'); - digits.rotate_right(leading_zeros + 2); - - // add any extra trailing zeros - digits.resize(digits.len() + trailing_zeros, b'0'); - } - } + // format only fractional digits + shift_or_trim_fractional_digits(&mut digits, scale, prec, &mut exp, this.sign); } // never print exp when in this branch exp = 0; @@ -212,6 +150,119 @@ fn format_full_scale( f.pad_integral(non_negative, "", &buf) } +/// Fill appropriate number of zeros and decimal point into Vec of (ascii/utf-8) digits +/// +/// Exponent is set to zero if zeros were added +/// +fn zero_right_pad_integer_ascii_digits( + digits: &mut Vec, + exp: &mut i128, + precision: Option, +) { + debug_assert!(*exp >= 0); + + let trailing_zero_count = exp.to_usize().unwrap(); + let total_additional_zeros = trailing_zero_count.saturating_add(precision.unwrap_or(0)); + if total_additional_zeros > 4096 { + return; + } + + // requested 'prec' digits of precision after decimal point + match precision { + None if trailing_zero_count > 20 => { + } + None | Some(0) => { + digits.resize(digits.len() + trailing_zero_count, b'0'); + *exp = 0; + } + Some(prec) => { + digits.resize(digits.len() + trailing_zero_count, b'0'); + digits.push(b'.'); + digits.resize(digits.len() + prec, b'0'); + *exp = 0; + } + } +} + +/// Fill zeros into utf-8 digits +fn trim_ascii_digits( + digits: &mut Vec, + scale: usize, + prec: usize, + exp: &mut i128, + sign: Sign, +) { + debug_assert!(scale < digits.len()); + // there are both integer and fractional digits + let integer_digit_count = digits.len() - scale; + + if prec < scale { + apply_rounding_to_ascii_digits( + digits, exp, integer_digit_count + prec, sign + ); + } + + if prec != 0 { + digits.insert(integer_digit_count, b'.'); + } + + if scale < prec { + // precision required beyond scale + digits.resize(digits.len() + (prec - scale), b'0'); + } +} + + +fn shift_or_trim_fractional_digits( + digits: &mut Vec, + scale: usize, + prec: usize, + exp: &mut i128, + sign: Sign, +) { + debug_assert!(scale >= digits.len()); + // there are no integer digits + let leading_zeros = scale - digits.len(); + + match prec.checked_sub(leading_zeros) { + None => { + digits.clear(); + digits.push(b'0'); + if prec > 0 { + digits.push(b'.'); + digits.resize(2 + prec, b'0'); + } + } + Some(0) => { + // precision is at the decimal digit boundary, round one value + let insig_digit = digits[0] - b'0'; + let trailing_zeros = digits[1..].iter().all(|&d| d == b'0'); + let rounded_value = Context::default().round_pair(sign, 0, insig_digit, trailing_zeros); + + digits.clear(); + if leading_zeros != 0 { + digits.push(b'0'); + digits.push(b'.'); + digits.resize(1 + leading_zeros, b'0'); + } + digits.push(rounded_value + b'0'); + } + Some(digit_prec) => { + let trailing_zeros = digit_prec.saturating_sub(digits.len()); + if digit_prec < digits.len() { + apply_rounding_to_ascii_digits(digits, exp, digit_prec, sign); + } + digits.extend_from_slice(b"0."); + digits.resize(digits.len() + leading_zeros, b'0'); + digits.rotate_right(leading_zeros + 2); + + // add any extra trailing zeros + digits.resize(digits.len() + trailing_zeros, b'0'); + } + } +} + + fn format_exponential( this: BigDecimalRef, From f3969e3c24611d6417dc923f67ed7dc06497fdee Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sun, 12 May 2024 19:54:05 -0400 Subject: [PATCH 27/50] Add test dec_n90037659d6902 --- src/impl_fmt.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index fe7f68b..c1cefb6 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -715,6 +715,25 @@ mod test { impl_case!(fmt_010d3: "{:010.3}" => "019073.972"); } + mod dec_n90037659d6902 { + use super::*; + + fn test_input() -> BigDecimal { + "-90037659.6905".parse().unwrap() + } + + impl_case!(fmt_default: "{}" => "-90037659.6905"); + impl_case!(fmt_debug: "{:?}" => "BigDecimal(sign=Minus, scale=4, digits=[900376596905])"); + impl_case!(fmt_debug_alt: "{:#?}" => "BigDecimal(\"-900376596905e-4\")"); + impl_case!(fmt_pd7: "{:+.7}" => "-90037659.6905000"); + impl_case!(fmt_d0: "{:.0}" => "-90037660"); + impl_case!(fmt_d3: "{:.3}" => "-90037659.690"); + impl_case!(fmt_d4: "{:.4}" => "-90037659.6905"); + impl_case!(fmt_14d4: "{:14.4}" => "-90037659.6905"); + impl_case!(fmt_15d4: "{:15.4}" => " -90037659.6905"); + impl_case!(fmt_l17d5: "{:<17.5}" => "-90037659.69050 "); + } + mod dec_1764031078en13 { use super::*; From f0c5fecbca726eb1e813b2c4ab1730bc7da799ff Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Mon, 13 May 2024 20:58:43 -0400 Subject: [PATCH 28/50] Add Formatting section to README --- README.md | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++---- build.rs | 2 +- 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 90477b8..2b3e13c 100644 --- a/README.md +++ b/README.md @@ -50,16 +50,86 @@ this code will print sqrt(2) = 1.414213562373095048801688724209698078569671875376948073176679737990732478462107038850387534327641573 ``` +### Formatting + +Until a more sophisticated formatting solution is implemented (currently work in progress), +we are restricted to Rust's `fmt::Display` formatting options. +This is how this crate formats BigDecimals: + +- `{}` - Default Display + - Format as "human readable" number + - "Small" fractional numbers (close to zero) are printed in scientific notation + - Number is considered "small" by number of leading zeros exceeding a threshold + - Configurable by the compile-time environment variable: + `RUST_BIGDECIMAL_FMT_EXPONENTIAL_LOWER_THRESHOLD` + - Default 5 + - Example: `1.23e-3` will print as `0.00123` but `1.23e-10` will be `1.23E-10` + - Trailing zeros will be added to "small" integers, avoiding scientific notation + - May appear to have more precision than they do + - Example: decimal `1e1` would be rendered as `10` + - The threshold for "small" is configured by compile-time environment variable: + `RUST_BIGDECIMAL_FMT_EXPONENTIAL_UPPER_THRESHOLD` + - Default 15 + - `1e15` => `1000000000000000` + - Large integers (e.g. `1e50000000`) will print in scientific notation, not + a 1 followed by fifty million zeros + - All other numbers are printed in standard decimal notation + +- `{:.}` - Display with precision + - Format number with exactly `PREC` digits after the decimal place + - Numbers with fractional components will be rounded at precision point, or have + zeros padded to precision point + - Integers will have zeros padded to the precision point + - To prevent unreasonably sized output, a threshold limits the number + of padded zeros + - Greater than the default case, since specific precision was requested + - Configurable by the compile-time environment variable: + `RUST_BIGDECIMAL_FMT_MAX_INTEGER_PADDING` + - Default 1000 + - If digits exceed this threshold, they are printed without decimal-point, + suffixed with scale of the big decimal + +- `{:e}` / `{:E}` - Exponential format + - Formats in scientific notation with either `e` or `E` as exponent delimiter + - Precision is kept exactly + +- `{:.e}` - formats in scientific notation, keeping number + - Number is rounded / zero padded until + +- `{:?}` - Debug + - Shows internal representation of BigDecimal + - `123.456` => `BigDecimal(sign=Plus, scale=3, digits=[123456])` + - `-1e10000` => `BigDecimal(sign=Minus, scale=-10000, digits=[1])` + +- `{:#?}` - Alternate Debug (used by `dbg!()`) + - Shows simple int+exponent string representation of BigDecimal + - `123.456` => `BigDecimal("123456e-3")` + - `-1e10000` => `BigDecimal("-1e10000")` + +There is a [formatting-example](examples/formatting-example.rs) script in the +`examples/` directory that demonstrates the formatting options and comparison +with Rust's standard floating point Display. + +It is recommended you include unit tests in your code to guarantee that future +versions of BigDecimal continue to format numbers the way you expect. +The rules above are not likely to change, but they are probably the only part +of this library that are relatively subjective, and could change behavior +without indication from the compiler. + +Also, check for changes to the configuration environment variables, those +may change name until 1.0. ### Compile-Time Configuration You can set a few default parameters at _compile-time_ via environment variables: -| Environment Variable | Default | -|-------------------------------------------------|------------| -| `RUST_BIGDECIMAL_DEFAULT_PRECISION` | 100 | -| `RUST_BIGDECIMAL_DEFAULT_ROUNDING_MODE` | `HalfEven` | -| `RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD` | 5 | +| Environment Variable | Default | +|----------------------------------------------------|------------| +| `RUST_BIGDECIMAL_DEFAULT_PRECISION` | 100 | +| `RUST_BIGDECIMAL_DEFAULT_ROUNDING_MODE` | `HalfEven` | +| `RUST_BIGDECIMAL_FMT_EXPONENTIAL_LOWER_THRESHOLD` | 5 | +| `RUST_BIGDECIMAL_FMT_EXPONENTIAL_UPPER_THRESHOLD` | 15 | +| `RUST_BIGDECIMAL_FMT_MAX_INTEGER_PADDING` | 1000 | These allow setting the default [Context] fields globally without incurring a runtime lookup, or having to pass Context parameters through all calculations. diff --git a/build.rs b/build.rs index 6f4fd88..30b6301 100644 --- a/build.rs +++ b/build.rs @@ -5,7 +5,7 @@ use std::path::{Path, PathBuf}; const DEFAULT_PRECISION: &str = "100"; const DEFAULT_ROUNDING_MODE: &str = "HalfEven"; const EXPONENTIAL_FORMAT_THRESHOLD: &str = "5"; -const EXPONENTIAL_FORMAT_UPPER_THRESHOLD: &str = "12"; +const EXPONENTIAL_FORMAT_UPPER_THRESHOLD: &str = "15"; fn main() { From cca95ec93ea1b1c62cac14556587c297df04ffd7 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Mon, 13 May 2024 22:51:56 -0400 Subject: [PATCH 29/50] Change the context variables --- build.rs | 19 +++++++++++++------ src/impl_fmt.rs | 9 +++++---- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/build.rs b/build.rs index 30b6301..8321164 100644 --- a/build.rs +++ b/build.rs @@ -4,8 +4,9 @@ use std::path::{Path, PathBuf}; // configuration defaults const DEFAULT_PRECISION: &str = "100"; const DEFAULT_ROUNDING_MODE: &str = "HalfEven"; -const EXPONENTIAL_FORMAT_THRESHOLD: &str = "5"; -const EXPONENTIAL_FORMAT_UPPER_THRESHOLD: &str = "15"; +const FMT_EXPONENTIAL_LOWER_THRESHOLD: &str = "5"; +const FMT_EXPONENTIAL_UPPER_THRESHOLD: &str = "15"; +const FMT_MAX_INTEGER_PADDING: &str = "1000"; fn main() { @@ -59,23 +60,29 @@ fn write_default_rounding_mode(outdir: &Path) { /// Create write_default_rounding_mode.rs, containing definition of constant EXPONENTIAL_FORMAT_THRESHOLD loaded in src/impl_fmt.rs fn write_exponential_format_threshold_file(outdir: &Path) { - let low_value = load_env!(env, "RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD", EXPONENTIAL_FORMAT_THRESHOLD); - let high_value = load_env!(env, "RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_UPPER_THRESHOLD", EXPONENTIAL_FORMAT_UPPER_THRESHOLD); + let low_value = load_env!(env, "RUST_BIGDECIMAL_FMT_EXPONENTIAL_LOWER_THRESHOLD", FMT_EXPONENTIAL_LOWER_THRESHOLD); + let high_value = load_env!(env, "RUST_BIGDECIMAL_FMT_EXPONENTIAL_UPPER_THRESHOLD", FMT_EXPONENTIAL_UPPER_THRESHOLD); + let max_padding = load_env!(env, "RUST_BIGDECIMAL_FMT_MAX_INTEGER_PADDING", FMT_MAX_INTEGER_PADDING); let low_value: u32 = low_value .parse::() - .expect("$RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD must be an integer > 0") + .expect("$RUST_BIGDECIMAL_FMT_EXPONENTIAL_LOWER_THRESHOLD must be an integer > 0") .into(); let high_value: u32 = high_value .parse::() - .expect("$RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_UPPER_THRESHOLD must be valid u32"); + .expect("$RUST_BIGDECIMAL_FMT_EXPONENTIAL_UPPER_THRESHOLD must be valid u32"); + + let max_padding: u32 = max_padding + .parse::() + .expect("$RUST_BIGDECIMAL_FMT_MAX_INTEGER_PADDING must be valid u32"); let rust_file_path = outdir.join("exponential_format_threshold.rs"); let rust_file_contents = [ format!("const EXPONENTIAL_FORMAT_LEADING_ZERO_THRESHOLD: usize = {};", low_value), format!("const EXPONENTIAL_FORMAT_TRAILING_ZERO_THRESHOLD: usize = {};", high_value), + format!("const FMT_MAX_INTEGER_PADDING: usize = {};", max_padding), ]; std::fs::write(rust_file_path, rust_file_contents.join("\n")).unwrap(); diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index c1cefb6..9e91536 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -5,8 +5,9 @@ use crate::*; use stdlib::fmt::Write; -// const EXPONENTIAL_FORMAT_LEADING_ZERO_THRESHOLD: i64 = ${RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD} or 5; -// const EXPONENTIAL_FORMAT_TRAILING_ZERO_THRESHOLD: i64 = ${RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_UPPER_THRESHOLD} or 5; +// const EXPONENTIAL_FORMAT_LEADING_ZERO_THRESHOLD: usize = ${RUST_BIGDECIMAL_FMT_EXPONENTIAL_LOWER_THRESHOLD} or 5; +// const EXPONENTIAL_FORMAT_TRAILING_ZERO_THRESHOLD: usize = ${RUST_BIGDECIMAL_FMT_EXPONENTIAL_UPPER_THRESHOLD} or 15; +// const FMT_MAX_INTEGER_PADDING: usize = = ${RUST_BIGDECIMAL_FMT_MAX_INTEGER_PADDING} or 1000; include!(concat!(env!("OUT_DIR"), "/exponential_format_threshold.rs")); @@ -128,7 +129,7 @@ fn format_full_scale( let prec = f.precision().unwrap_or(scale); if scale < digits.len() { - // format both integer and fractional digits (always just 'trim') + // format both integer and fractional digits (always 'trim' to precision) trim_ascii_digits(&mut digits, scale, prec, &mut exp, this.sign); } else { // format only fractional digits @@ -163,7 +164,7 @@ fn zero_right_pad_integer_ascii_digits( let trailing_zero_count = exp.to_usize().unwrap(); let total_additional_zeros = trailing_zero_count.saturating_add(precision.unwrap_or(0)); - if total_additional_zeros > 4096 { + if total_additional_zeros > FMT_MAX_INTEGER_PADDING { return; } From 0bfe54d705c32132b6d507befee1e5a3da9e11e5 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Tue, 14 May 2024 00:43:13 -0400 Subject: [PATCH 30/50] Add more formatting tests --- src/impl_fmt.rs | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index 9e91536..688f7ec 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -571,7 +571,7 @@ mod test { impl_case!(case_0d000123: "0.000123" => "1.23E-4"); impl_case!(case_123d: "123." => "123"); - impl_case!(case_123de1: "123.e1" => "1.23E+3"); + impl_case!(case_123de1: "123.e1" => "1230"); } mod fmt_options { @@ -615,9 +615,11 @@ mod test { } impl_case!(fmt_default: "{}" => "10"); - impl_case!(fmt_d0: "{:.0}" => "1e1"); - impl_case!(fmt_d1: "{:.1}" => "1.0e1"); - impl_case!(fmt_d2: "{:.2}" => "1.00e1"); + impl_case!(fmt_debug: "{:?}" => "BigDecimal(sign=Plus, scale=-1, digits=[1])"); + impl_case!(fmt_debug_alt: "{:#?}" => "BigDecimal(\"1e1\")"); + impl_case!(fmt_d0: "{:.0}" => "10"); + impl_case!(fmt_d1: "{:.1}" => "10.0"); + impl_case!(fmt_d2: "{:.2}" => "10.00"); } mod dec_1en1 { @@ -752,6 +754,31 @@ mod test { } + mod dec_1e15 { + use super::*; + + fn test_input() -> BigDecimal { + "1e15".parse().unwrap() + } + + impl_case!(fmt_default: "{}" => "1000000000000000"); + impl_case!(fmt_d0: "{:.0}" => "1000000000000000"); + impl_case!(fmt_d1: "{:.1}" => "1000000000000000.0"); + } + + mod dec_1e16 { + use super::*; + + fn test_input() -> BigDecimal { + "1e16".parse().unwrap() + } + + impl_case!(fmt_default: "{}" => "1e16"); + impl_case!(fmt_d0: "{:.0}" => "10000000000000000"); + impl_case!(fmt_d1: "{:.1}" => "10000000000000000.0"); + } + + mod dec_491326en12 { use super::*; From 9d58d8aebdb9ea0ec16773ced20a0e90cff31baf Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Mon, 27 May 2024 17:33:23 -0400 Subject: [PATCH 31/50] Add function format_dotless_exponential --- src/impl_fmt.rs | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index 688f7ec..3ea5494 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -94,13 +94,20 @@ fn dynamically_format_decimal( // number of zeros between least significant digit and decimal point let trailing_zero_count = this.scale .checked_neg() - .and_then(|d| d.to_usize()) - .unwrap_or(0); + .and_then(|d| d.to_usize()); + + // this ignores scientific-formatting if precision is requested + let trailing_zeros = f.precision().map(|_| 0) + .or(trailing_zero_count) + .unwrap_or(0); // use exponential form if decimal point is outside // the upper and lower thresholds of the decimal - if leading_zero_threshold < leading_zero_count || trailing_zero_threshold < trailing_zero_count { + if leading_zero_threshold < leading_zero_count { format_exponential(this, f, abs_int, "E") + } else if trailing_zero_threshold < trailing_zeros { + // non-scientific notation + format_dotless_exponential(f, abs_int, this.sign, this.scale, "e") } else { format_full_scale(this, f, abs_int) } @@ -264,6 +271,23 @@ fn shift_or_trim_fractional_digits( } +/// Format integer as {int}e+{exp} +/// +/// Slightly different than scientific notation, +/// +fn format_dotless_exponential( + f: &mut fmt::Formatter, + mut abs_int: String, + sign: Sign, + scale: i64, + e_symbol: &str, +) -> fmt::Result { + debug_assert!(scale <= 0); + + write!(abs_int, "{}{:+}", e_symbol, -scale).unwrap(); + let non_negative = matches!(sign, Sign::Plus | Sign::NoSign); + f.pad_integral(non_negative, "", &abs_int) +} fn format_exponential( this: BigDecimalRef, From 69a1ab33b4cc21b8607e02e4090ffb37456bd3d7 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Mon, 27 May 2024 18:43:28 -0400 Subject: [PATCH 32/50] Add test cases --- src/impl_fmt.rs | 55 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index 3ea5494..f81f2ee 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -797,9 +797,9 @@ mod test { "1e16".parse().unwrap() } - impl_case!(fmt_default: "{}" => "1e16"); + impl_case!(fmt_default: "{}" => "1e+16"); impl_case!(fmt_d0: "{:.0}" => "10000000000000000"); - impl_case!(fmt_d1: "{:.1}" => "10000000000000000.0"); + impl_case!(fmt_d2: "{:.2}" => "10000000000000000.00"); } @@ -859,12 +859,12 @@ mod test { use super::*; fn test_input() -> BigDecimal { - "1e10000".parse().unwrap() + "1e100000".parse().unwrap() } - 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_default: "{}" => "1e+100000"); + impl_case!(fmt_d1: "{:.1}" => "1E+100000"); + impl_case!(fmt_d4: "{:.4}" => "1E+100000"); } @@ -890,12 +890,22 @@ mod test { BigDecimal::new(1234506789.into(), -15) } - impl_case!(fmt_default: "{}" => "1.234506789E+24"); - impl_case!(fmt_d1: "{:.1}" => "1.2E+24"); - impl_case!(fmt_d3: "{:.3}" => "1.235E+24"); - impl_case!(fmt_d4: "{:.4}" => "1.2345E+24"); - impl_case!(fmt_l13d4: "{:<13.4}" => "1.2345E+24 "); - impl_case!(fmt_r13d4: "{:>13.4}" => " 1.2345E+24"); + impl_case!(fmt_default: "{}" => "1234506789000000000000000"); + impl_case!(fmt_d1: "{:.1}" => "1234506789000000000000000.0"); + impl_case!(fmt_d3: "{:.3}" => "1234506789000000000000000.000"); + impl_case!(fmt_l13d4: "{:<+32.2}" => "+1234506789000000000000000.00 "); + impl_case!(fmt_r13d4: "{:>+32.2}" => " +1234506789000000000000000.00"); + } + + mod dec_13400476439814628800E2502 { + use super::*; + + fn test_input() -> BigDecimal { + BigDecimal::new(13400476439814628800u64.into(), -2502) + } + + impl_case!(fmt_default: "{}" => "13400476439814628800e+2502"); + impl_case!(fmt_d1: "{:.1}" => "13400476439814628800E+2502"); } } @@ -911,11 +921,16 @@ mod test { let result = bd.to_string(); assert_eq!(result, $expected); - bd.to_scientific_notation(); - bd.to_engineering_notation(); - let round_trip = BigDecimal::from_str(&result).unwrap(); assert_eq!(round_trip, bd); + + let sci = bd.to_scientific_notation(); + let sci_round_trip = BigDecimal::from_str(&sci).unwrap(); + assert_eq!(sci_round_trip, bd); + + let eng = bd.to_engineering_notation(); + let eng_round_trip = BigDecimal::from_str(&eng).unwrap(); + assert_eq!(eng_round_trip, bd); } }; ( (panics) $name:ident: $src:expr ) => { @@ -928,10 +943,12 @@ mod test { }; } - impl_case!(test_max: format!("1E{}", i64::MAX) => "1E+9223372036854775807"); - impl_case!(test_max_multiple_digits: format!("314156E{}", i64::MAX) => "3.14156E+9223372036854775812"); - impl_case!(test_min_scale: "1E9223372036854775808" => "1E+9223372036854775808"); - impl_case!(test_max_scale: "1E-9223372036854775807" => "1E-9223372036854775807"); + + impl_case!(test_max: format!("1E{}", i64::MAX) => "1e+9223372036854775807"); + impl_case!(test_max_multiple_digits: format!("314156E{}", i64::MAX) => "314156e+9223372036854775807"); + impl_case!(test_min_scale: "1E9223372036854775807" => "1e+9223372036854775807"); + + // impl_case!(test_max_scale: "1E-9223372036854775807" => "1E-9223372036854775807"); impl_case!(test_min_multiple_digits: format!("271828182E-{}", i64::MAX) => "2.71828182E-9223372036854775799"); impl_case!((panics) test_max_exp_overflow: "1E9223372036854775809"); From 8a423f24d4e32b7e1e7c071abf21edae8d78ecad Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sat, 1 Jun 2024 13:50:13 -0400 Subject: [PATCH 33/50] Add test-case for engineering_notation producing out-of-bounds exponent --- src/impl_fmt.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index f81f2ee..9668896 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -933,6 +933,26 @@ mod test { assert_eq!(eng_round_trip, bd); } }; + ( (eng-check-invalid) $name:ident: $src:expr => $expected:literal ) => { + #[test] + fn $name() { + let src = $src; + let bd: BigDecimal = src.parse().unwrap(); + let result = bd.to_string(); + assert_eq!(result, $expected); + + let round_trip = BigDecimal::from_str(&result).unwrap(); + assert_eq!(round_trip, bd); + + let sci = bd.to_scientific_notation(); + let sci_round_trip = BigDecimal::from_str(&sci).unwrap(); + assert_eq!(sci_round_trip, bd); + + let eng = bd.to_engineering_notation(); + let eng_round_trip = BigDecimal::from_str(&eng); + assert!(eng_round_trip.is_err()); + } + }; ( (panics) $name:ident: $src:expr ) => { #[test] #[should_panic] @@ -948,7 +968,7 @@ mod test { impl_case!(test_max_multiple_digits: format!("314156E{}", i64::MAX) => "314156e+9223372036854775807"); impl_case!(test_min_scale: "1E9223372036854775807" => "1e+9223372036854775807"); - // impl_case!(test_max_scale: "1E-9223372036854775807" => "1E-9223372036854775807"); + impl_case!((eng-check-invalid) test_max_scale: "1E-9223372036854775807" => "1E-9223372036854775807"); impl_case!(test_min_multiple_digits: format!("271828182E-{}", i64::MAX) => "2.71828182E-9223372036854775799"); impl_case!((panics) test_max_exp_overflow: "1E9223372036854775809"); From d2779525ca0f0a1ad7cf239cc5e153d15af30c76 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sat, 1 Jun 2024 14:31:54 -0400 Subject: [PATCH 34/50] Restrict versions of num-* dependencies --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 543d4a7..a8ff71e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,9 +21,9 @@ bench = false [dependencies] libm = "0.2.6" -num-bigint = { version = "0.4", default-features = false } +num-bigint = { version = "<0.4.5", default-features = false } num-integer = { version = "0.1", default-features = false } -num-traits = { version = "0.2", default-features = false } +num-traits = { version = "<0.2.19", default-features = false } serde = { version = "1.0", optional = true, default-features = false } [dev-dependencies] From 21033d20d67aee408db96af4fa24ff9bc378b4c9 Mon Sep 17 00:00:00 2001 From: tenuous-guidance <105654822+tenuous-guidance@users.noreply.github.com> Date: Fri, 3 Nov 2023 19:12:57 +0000 Subject: [PATCH 35/50] Add support for serde_json's arbitrary precision --- Cargo.toml | 5 ++ src/impl_serde.rs | 168 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index a8ff71e..acbe295 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,10 @@ num-bigint = { version = "<0.4.5", default-features = false } num-integer = { version = "0.1", default-features = false } num-traits = { version = "<0.2.19", default-features = false } serde = { version = "1.0", optional = true, default-features = false } +# `arbitrary-precision` is specifically an interaction with the similarly named feature in `serde_json`. We need it when we want to support it. +# +# The version is set to the minimum needed for the `arbitrary_precision` feature +serde_json = { version = "1.0.108", optional = true, default-features = false, features = ["arbitrary_precision", "std"] } [dev-dependencies] paste = "1" @@ -44,6 +48,7 @@ autocfg = "1" [features] default = ["std"] +arbitrary-precision = ["serde", "serde_json", "std", "serde/derive"] string-only = [] std = ["num-bigint/std", "num-integer/std", "num-traits/std"] diff --git a/src/impl_serde.rs b/src/impl_serde.rs index 6ba601d..2adb5f8 100644 --- a/src/impl_serde.rs +++ b/src/impl_serde.rs @@ -192,3 +192,171 @@ mod test { } } } + + +/// Serialize/deserialize [`BigDecimal`] as arbitrary precision numbers in JSON using the `arbitrary_precision` feature within `serde_json`. +/// +/// ``` +/// # extern crate serde; +/// # use serde::{Serialize, Deserialize}; +/// # use bigdecimal::BigDecimal; +/// # use std::str::FromStr; +/// +/// #[derive(Serialize, Deserialize)] +/// pub struct ArbitraryExample { +/// #[serde(with = "bigdecimal::impl_serde::arbitrary_precision")] +/// value: BigDecimal, +/// } +/// +/// let value = ArbitraryExample { value: BigDecimal::from_str("123.400").unwrap() }; +/// assert_eq!( +/// &serde_json::to_string(&value).unwrap(), +/// r#"{"value":123.400}"# +/// ); +/// ``` +#[cfg(feature = "arbitrary-precision")] +pub mod arbitrary_precision { + use crate::{BigDecimal, FromStr, stdlib::string::ToString}; + use serde::{Serialize, Deserialize}; + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + serde_json::Number::deserialize(deserializer)?.to_string().parse().map_err(serde::de::Error::custom) + + } + + pub fn serialize(value: &BigDecimal, serializer: S) -> Result + where + S: serde::Serializer, + { + serde_json::Number::from_str(&value.to_string()) + .map_err(serde::ser::Error::custom)? + .serialize(serializer) + } +} + + +/// Serialize/deserialize [`Option`] as arbitrary precision numbers in JSON using the `arbitrary_precision` feature within `serde_json`. +/// +/// ``` +/// # extern crate serde; +/// # use serde::{Serialize, Deserialize}; +/// # use bigdecimal::BigDecimal; +/// # use std::str::FromStr; +/// +/// #[derive(Serialize, Deserialize)] +/// pub struct ArbitraryExample { +/// #[serde(with = "bigdecimal::impl_serde::arbitrary_precision_option")] +/// value: Option, +/// } +/// +/// let value = ArbitraryExample { value: Some(BigDecimal::from_str("123.400").unwrap()) }; +/// assert_eq!( +/// &serde_json::to_string(&value).unwrap(), +/// r#"{"value":123.400}"# +/// ); +/// +/// let value = ArbitraryExample { value: None }; +/// assert_eq!( +/// &serde_json::to_string(&value).unwrap(), +/// r#"{"value":null}"# +/// ); +/// ``` +#[cfg(feature = "arbitrary-precision")] +pub mod arbitrary_precision_option { + use crate::{BigDecimal, FromStr, stdlib::string::ToString}; + use serde::{Serialize, Deserialize}; + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: serde::de::Deserializer<'de>, + { + Option::::deserialize(deserializer)?.map(|num| num.to_string().parse().map_err(serde::de::Error::custom)).transpose() + + } + + pub fn serialize(value: &Option, serializer: S) -> Result + where + S: serde::Serializer, + { + match *value { + Some(ref decimal) => serde_json::Number::from_str(&decimal.to_string()) + .map_err(serde::ser::Error::custom)? + .serialize(serializer), + None => serializer.serialize_none(), + } + } +} + + + + +#[cfg(all(test, feature = "arbitrary-precision"))] +mod test_arbitrary_precision { + extern crate serde_json; + + use crate::{BigDecimal, FromStr}; + use serde::Deserialize; + + #[test] + #[cfg(not(any(feature = "string-only", feature = "arbitrary-precision")))] + fn test_serde_deserialize_f64() { + use crate::{FromPrimitive,stdlib::f64::consts::PI}; + + let vals = vec![ + 1.0, + 0.5, + 0.25, + 50.0, + 50000., + 0.001, + 12.34, + 5.0 * 0.03125, + PI, + PI * 10000.0, + PI * 30000.0, + ]; + for n in vals { + let expected = BigDecimal::from_f64(n).unwrap(); + let value: BigDecimal = serde_json::from_str(&serde_json::to_string(&n).unwrap()).unwrap(); + assert_eq!(expected, value); + } + } + + /// Not a great test but demonstrates why `arbitrary-precision` exists. + #[test] + #[cfg(not(feature = "arbitrary-precision"))] + fn test_normal_precision() { + #[derive(Deserialize, Debug, PartialEq, Eq)] + struct ViaF64 { + n: BigDecimal, + } + + let json = r#"{ "n": 0.1 }"#; + let expected = BigDecimal::from_str("0.1").expect("should parse 0.1 as BigDecimal"); + let deser: ViaF64 = serde_json::from_str(json).expect("should parse JSON"); + + // 0.1 is directly representable in `BigDecimal`, but not `f64` so the default deserialization fails. + assert_ne!(expected, deser.n); + } + + #[test] + #[cfg(feature = "arbitrary-precision")] + fn test_arbitrary_precision() { + use serde::Deserialize; + + #[derive(Deserialize, Debug, PartialEq, Eq)] + struct ArbitraryPrec { + #[serde(with = "crate::impl_serde::arbitrary_precision")] + n: BigDecimal + } + + let json = r#"{ "n": 0.1 }"#; + let expected = BigDecimal::from_str("0.1").expect("should parse 0.1 as BigDecimal"); + let deser: ArbitraryPrec = serde_json::from_str(json).expect("should parse JSON"); + + assert_eq!(expected, deser.n); + } +} From 2abd7bb83458f13f98400e936cb051493d194d76 Mon Sep 17 00:00:00 2001 From: tenuous-guidance <105654822+tenuous-guidance@users.noreply.github.com> Date: Fri, 3 Nov 2023 20:21:05 +0000 Subject: [PATCH 36/50] Support running the tests on an old version of rust --- src/impl_serde.rs | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/src/impl_serde.rs b/src/impl_serde.rs index 2adb5f8..45e4c86 100644 --- a/src/impl_serde.rs +++ b/src/impl_serde.rs @@ -196,7 +196,9 @@ mod test { /// Serialize/deserialize [`BigDecimal`] as arbitrary precision numbers in JSON using the `arbitrary_precision` feature within `serde_json`. /// -/// ``` +// The following example is ignored as it requires derives which we don't import and aren't compatible +// with our locked versions of rust due to proc_macro2. +/// ```ignore /// # extern crate serde; /// # use serde::{Serialize, Deserialize}; /// # use bigdecimal::BigDecimal; @@ -240,7 +242,9 @@ pub mod arbitrary_precision { /// Serialize/deserialize [`Option`] as arbitrary precision numbers in JSON using the `arbitrary_precision` feature within `serde_json`. /// -/// ``` +// The following example is ignored as it requires derives which we don't import and aren't compatible +// with our locked versions of rust due to proc_macro2. +/// ```ignore /// # extern crate serde; /// # use serde::{Serialize, Deserialize}; /// # use bigdecimal::BigDecimal; @@ -291,8 +295,6 @@ pub mod arbitrary_precision_option { } - - #[cfg(all(test, feature = "arbitrary-precision"))] mod test_arbitrary_precision { extern crate serde_json; @@ -329,34 +331,23 @@ mod test_arbitrary_precision { #[test] #[cfg(not(feature = "arbitrary-precision"))] fn test_normal_precision() { - #[derive(Deserialize, Debug, PartialEq, Eq)] - struct ViaF64 { - n: BigDecimal, - } - - let json = r#"{ "n": 0.1 }"#; + let json = r#"0.1"#; let expected = BigDecimal::from_str("0.1").expect("should parse 0.1 as BigDecimal"); - let deser: ViaF64 = serde_json::from_str(json).expect("should parse JSON"); + let deser: BigDecimal = serde_json::from_str(json).expect("should parse JSON"); // 0.1 is directly representable in `BigDecimal`, but not `f64` so the default deserialization fails. - assert_ne!(expected, deser.n); + assert_ne!(expected, deser); } #[test] #[cfg(feature = "arbitrary-precision")] fn test_arbitrary_precision() { - use serde::Deserialize; - - #[derive(Deserialize, Debug, PartialEq, Eq)] - struct ArbitraryPrec { - #[serde(with = "crate::impl_serde::arbitrary_precision")] - n: BigDecimal - } + use crate::impl_serde::arbitrary_precision; - let json = r#"{ "n": 0.1 }"#; + let json = r#"0.1"#; let expected = BigDecimal::from_str("0.1").expect("should parse 0.1 as BigDecimal"); - let deser: ArbitraryPrec = serde_json::from_str(json).expect("should parse JSON"); + let deser = arbitrary_precision::deserialize(&mut serde_json::Deserializer::from_str(json)).expect("should parse JSON"); - assert_eq!(expected, deser.n); + assert_eq!(expected, deser); } } From 48e1aac64d908dd015de24b7922d8b53ae5d6b42 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Wed, 6 Mar 2024 12:21:56 -0500 Subject: [PATCH 37/50] Simplify serde dependency requirements --- Cargo.toml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index acbe295..9d98248 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,10 +25,8 @@ num-bigint = { version = "<0.4.5", default-features = false } num-integer = { version = "0.1", default-features = false } num-traits = { version = "<0.2.19", default-features = false } serde = { version = "1.0", optional = true, default-features = false } -# `arbitrary-precision` is specifically an interaction with the similarly named feature in `serde_json`. We need it when we want to support it. -# -# The version is set to the minimum needed for the `arbitrary_precision` feature -serde_json = { version = "1.0.108", optional = true, default-features = false, features = ["arbitrary_precision", "std"] } +# Allow direct parsing of JSON floats, for full arbitrary precision +serde_json = { version = "1.0", optional = true, default-features = false, features = ["alloc", "arbitrary_precision"]} [dev-dependencies] paste = "1" @@ -48,7 +46,7 @@ autocfg = "1" [features] default = ["std"] -arbitrary-precision = ["serde", "serde_json", "std", "serde/derive"] +serde-json = ["serde/derive", "serde_json"] string-only = [] std = ["num-bigint/std", "num-integer/std", "num-traits/std"] From 467ec3e468a1573ccb89f84745ae578ce01115c8 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Wed, 6 Mar 2024 12:56:29 -0500 Subject: [PATCH 38/50] Replace arbitrary-precision with serde_json --- src/impl_serde.rs | 11 ++++++----- src/lib.rs | 7 +++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/impl_serde.rs b/src/impl_serde.rs index 45e4c86..4437908 100644 --- a/src/impl_serde.rs +++ b/src/impl_serde.rs @@ -206,7 +206,7 @@ mod test { /// /// #[derive(Serialize, Deserialize)] /// pub struct ArbitraryExample { -/// #[serde(with = "bigdecimal::impl_serde::arbitrary_precision")] +/// #[serde(with = "bigdecimal::serde_json_float")] /// value: BigDecimal, /// } /// @@ -216,7 +216,7 @@ mod test { /// r#"{"value":123.400}"# /// ); /// ``` -#[cfg(feature = "arbitrary-precision")] +#[cfg(feature = "serde_json")] pub mod arbitrary_precision { use crate::{BigDecimal, FromStr, stdlib::string::ToString}; use serde::{Serialize, Deserialize}; @@ -268,7 +268,7 @@ pub mod arbitrary_precision { /// r#"{"value":null}"# /// ); /// ``` -#[cfg(feature = "arbitrary-precision")] +#[cfg(feature = "serde_json")] pub mod arbitrary_precision_option { use crate::{BigDecimal, FromStr, stdlib::string::ToString}; use serde::{Serialize, Deserialize}; @@ -295,8 +295,9 @@ pub mod arbitrary_precision_option { } -#[cfg(all(test, feature = "arbitrary-precision"))] -mod test_arbitrary_precision { +#[cfg(all(test, feature = "serde_json"))] +mod test_jsonification { + use super::*; extern crate serde_json; use crate::{BigDecimal, FromStr}; diff --git a/src/lib.rs b/src/lib.rs index f692b90..49761e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,7 +62,7 @@ extern crate paste; #[cfg(feature = "serde")] extern crate serde; -#[cfg(all(test, feature = "serde"))] +#[cfg(all(test, any(feature = "serde", feature = "serde_json")))] extern crate serde_test; #[cfg(feature = "std")] @@ -124,9 +124,12 @@ mod impl_num; mod impl_fmt; // Implementations for deserializations and serializations -#[cfg(feature = "serde")] +#[cfg(any(feature = "serde", feature="serde_json"))] pub mod impl_serde; +#[cfg(feature = "serde_json")] +pub use impl_serde::arbitrary_precision as serde_json_float; + // construct BigDecimals from strings and floats mod parsing; From 27192f56a740464cbd8a9a3efa3ebb53ed9a3b31 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Wed, 6 Mar 2024 13:33:23 -0500 Subject: [PATCH 39/50] Export serde_json derive-modules via bigdecimal::serde --- src/impl_serde.rs | 112 ++++++++++++++++++++++++++-------------------- src/lib.rs | 11 ++++- 2 files changed, 72 insertions(+), 51 deletions(-) diff --git a/src/impl_serde.rs b/src/impl_serde.rs index 4437908..f912e83 100644 --- a/src/impl_serde.rs +++ b/src/impl_serde.rs @@ -2,7 +2,7 @@ //! Support for serde implementations //! use crate::*; -use serde::{de, ser}; +use serde_crate::{self as serde, de, ser, Serialize, Deserialize}; impl ser::Serialize for BigDecimal { @@ -206,7 +206,7 @@ mod test { /// /// #[derive(Serialize, Deserialize)] /// pub struct ArbitraryExample { -/// #[serde(with = "bigdecimal::serde_json_float")] +/// #[serde(with = "bigdecimal::serde::json_num")] /// value: BigDecimal, /// } /// @@ -218,8 +218,7 @@ mod test { /// ``` #[cfg(feature = "serde_json")] pub mod arbitrary_precision { - use crate::{BigDecimal, FromStr, stdlib::string::ToString}; - use serde::{Serialize, Deserialize}; + use super::*; pub fn deserialize<'de, D>(deserializer: D) -> Result where @@ -270,8 +269,7 @@ pub mod arbitrary_precision { /// ``` #[cfg(feature = "serde_json")] pub mod arbitrary_precision_option { - use crate::{BigDecimal, FromStr, stdlib::string::ToString}; - use serde::{Serialize, Deserialize}; + use super::*; pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> where @@ -300,55 +298,71 @@ mod test_jsonification { use super::*; extern crate serde_json; - use crate::{BigDecimal, FromStr}; - use serde::Deserialize; - - #[test] - #[cfg(not(any(feature = "string-only", feature = "arbitrary-precision")))] - fn test_serde_deserialize_f64() { - use crate::{FromPrimitive,stdlib::f64::consts::PI}; - - let vals = vec![ - 1.0, - 0.5, - 0.25, - 50.0, - 50000., - 0.001, - 12.34, - 5.0 * 0.03125, - PI, - PI * 10000.0, - PI * 30000.0, - ]; - for n in vals { - let expected = BigDecimal::from_f64(n).unwrap(); - let value: BigDecimal = serde_json::from_str(&serde_json::to_string(&n).unwrap()).unwrap(); - assert_eq!(expected, value); + mod deserialize_f64 { + use super::*; + + macro_rules! impl_case { + ($name:ident : $input:expr) => { + impl_case!($name: $input => $input); + }; + ($name:ident : $input:expr => $expected:literal) => { + #[test] + fn $name() { + let mut json_deserialize = serde_json::Deserializer::from_str($input); + let value = arbitrary_precision::deserialize(&mut json_deserialize) + .expect("should parse JSON"); + let expected: BigDecimal = $expected.parse().unwrap(); + + assert_eq!(expected, value); + } + }; } + + impl_case!(case_1d0: "1.0" => "1.0"); + impl_case!(case_0d001: "0.001" => "0.001"); + impl_case!(case_41009d2207etc: "41009.22075436852032769878903135430037010"); } - /// Not a great test but demonstrates why `arbitrary-precision` exists. - #[test] - #[cfg(not(feature = "arbitrary-precision"))] - fn test_normal_precision() { - let json = r#"0.1"#; - let expected = BigDecimal::from_str("0.1").expect("should parse 0.1 as BigDecimal"); - let deser: BigDecimal = serde_json::from_str(json).expect("should parse JSON"); + mod serde_struct_decimals { + use super::*; + use crate as bigdecimal; - // 0.1 is directly representable in `BigDecimal`, but not `f64` so the default deserialization fails. - assert_ne!(expected, deser); - } + #[derive(Serialize, Deserialize)] + pub struct TestExample { + #[serde(with = "bigdecimal::serde::json_num")] + value: BigDecimal, + } + + macro_rules! impl_case { + ($name:ident : $input:expr => (error)) => { + #[test] + fn $name() { + let res = serde_json::from_str::($input); + assert!(res.is_err()); + } + }; + ($name:ident : $input:expr => $expected:expr) => { + #[test] + fn $name() { + let obj: TestExample = serde_json::from_str($input).unwrap(); + let expected: BigDecimal = $expected.parse().unwrap(); + + assert_eq!(expected, obj.value); + + // expect output to be input with all spaces removed + let s = serde_json::to_string(&obj).unwrap(); + let input_stripped = $input.replace(" ", ""); - #[test] - #[cfg(feature = "arbitrary-precision")] - fn test_arbitrary_precision() { - use crate::impl_serde::arbitrary_precision; + assert_eq!(&s, &input_stripped); + } + }; + } - let json = r#"0.1"#; - let expected = BigDecimal::from_str("0.1").expect("should parse 0.1 as BigDecimal"); - let deser = arbitrary_precision::deserialize(&mut serde_json::Deserializer::from_str(json)).expect("should parse JSON"); + impl_case!(case_1d0: r#"{ "value": 1.0 }"# => "1.0" ); + impl_case!(case_2d01: r#"{ "value": 2.01 }"# => "2.01" ); + impl_case!(case_50: r#"{ "value": 50 }"# => "50" ); + impl_case!(case_high_prec: r#"{ "value": 64771126779.35857825871133263810255301911 }"# => "64771126779.35857825871133263810255301911" ); - assert_eq!(expected, deser); + impl_case!(case_nan: r#"{ "value": nan }"# => (error) ); } } diff --git a/src/lib.rs b/src/lib.rs index 49761e2..dae0a2e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,7 +60,7 @@ extern crate num_integer; extern crate paste; #[cfg(feature = "serde")] -extern crate serde; +extern crate serde as serde_crate; #[cfg(all(test, any(feature = "serde", feature = "serde_json")))] extern crate serde_test; @@ -127,8 +127,15 @@ mod impl_fmt; #[cfg(any(feature = "serde", feature="serde_json"))] pub mod impl_serde; +/// re-export serde-json derive modules #[cfg(feature = "serde_json")] -pub use impl_serde::arbitrary_precision as serde_json_float; +pub mod serde { + /// Parse JSON number directly to BigDecimal + pub use impl_serde::arbitrary_precision as json_num; + /// Parse JSON (number | null) directly to Option + pub use impl_serde::arbitrary_precision_option as json_num_option; +} + // construct BigDecimals from strings and floats mod parsing; From 4c9739dc1b174cc97e1009784836c5f510f2e09f Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Wed, 6 Mar 2024 14:10:43 -0500 Subject: [PATCH 40/50] Modify formatting in impl_serde --- src/impl_serde.rs | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/impl_serde.rs b/src/impl_serde.rs index f912e83..7d872bb 100644 --- a/src/impl_serde.rs +++ b/src/impl_serde.rs @@ -224,8 +224,10 @@ pub mod arbitrary_precision { where D: serde::de::Deserializer<'de>, { - serde_json::Number::deserialize(deserializer)?.to_string().parse().map_err(serde::de::Error::custom) - + serde_json::Number::deserialize(deserializer)? + .as_str() + .parse() + .map_err(de::Error::custom) } pub fn serialize(value: &BigDecimal, serializer: S) -> Result @@ -233,8 +235,8 @@ pub mod arbitrary_precision { S: serde::Serializer, { serde_json::Number::from_str(&value.to_string()) - .map_err(serde::ser::Error::custom)? - .serialize(serializer) + .map_err(ser::Error::custom)? + .serialize(serializer) } } @@ -275,8 +277,9 @@ pub mod arbitrary_precision_option { where D: serde::de::Deserializer<'de>, { - Option::::deserialize(deserializer)?.map(|num| num.to_string().parse().map_err(serde::de::Error::custom)).transpose() - + Option::::deserialize(deserializer)? + .map(|num| num.as_str().parse().map_err(serde::de::Error::custom)) + .transpose() } pub fn serialize(value: &Option, serializer: S) -> Result @@ -284,9 +287,11 @@ pub mod arbitrary_precision_option { S: serde::Serializer, { match *value { - Some(ref decimal) => serde_json::Number::from_str(&decimal.to_string()) - .map_err(serde::ser::Error::custom)? - .serialize(serializer), + Some(ref decimal) => { + serde_json::Number::from_str(&decimal.to_string()) + .map_err(serde::ser::Error::custom)? + .serialize(serializer) + } None => serializer.serialize_none(), } } From 2d5c2d1c0882731c55c592bbb1df2f68e5ce23b3 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Wed, 6 Mar 2024 14:12:23 -0500 Subject: [PATCH 41/50] Add more serde test-cases to ci --- .circleci/config.yml | 8 ++++++++ .gitlab-ci.yml | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4274ccf..6615f67 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -163,6 +163,14 @@ workflows: name: build-and-test:serde+no_std rust-features: "--no-default-features --features='serde'" + - build-and-test: + name: build-and-test:serde_json + rust-features: "--features='serde-json'" + + - build-and-test: + name: build-and-test:serde_json+no_std + rust-features: "--features='serde-json' --no-default-features" + - cargo-semver-check: requires: - build-and-test:latest:serde diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d67a2f9..63adcca 100755 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -218,6 +218,29 @@ cargo:test:serde: <<: *script-cargo-test script: - cargo test --features=serde --all-targets + - cargo test --features=serde --no-default-features + +cargo:build:serde-json: + stage: build + image: akubera/rust:stable + needs: + - cargo:check + variables: + RUST_CACHE_KEY: "stable+serde-json" + <<: *script-cargo-build + script: + - cargo build --features=serde-json --all-targets + +cargo:test:serde-json: + stage: test + image: akubera/rust:stable + needs: + - "cargo:build:serde-json" + variables: + RUST_CACHE_KEY: "stable+serde-json" + <<: *script-cargo-test + script: + - cargo test --features=serde-json --all-targets cargo:build-nightly: @@ -265,6 +288,18 @@ cargo:test-1.43: RUST_CACHE_KEY: "1.43" <<: *script-cargo-test +cargo:test-1.43:serde: + stage: test + needs: + - "cargo:build-1.43" + image: "akubera/rust-kcov:1.43.1-buster" + variables: + RUST_CACHE_KEY: "1.43-serde" + <<: *script-cargo-test + script: + - cargo test --features=serde --all-targets + - cargo test --features=serde --no-default-features + cargo:check-1.70: stage: check From 09d634b107f3beb1315dad23287a62ded114406f Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sun, 2 Jun 2024 14:01:24 -0400 Subject: [PATCH 42/50] Add more methods to BigDecimalVisitor impl --- src/impl_serde.rs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/impl_serde.rs b/src/impl_serde.rs index 7d872bb..089e7b9 100644 --- a/src/impl_serde.rs +++ b/src/impl_serde.rs @@ -45,12 +45,47 @@ impl<'de> de::Visitor<'de> for BigDecimalVisitor { Ok(BigDecimal::from(value)) } + fn visit_u128(self, value: u128) -> Result + where + E: de::Error, + { + Ok(BigDecimal::from(value)) + } + + fn visit_i128(self, value: i128) -> Result + where + E: de::Error, + { + Ok(BigDecimal::from(value)) + } + + fn visit_f32(self, value: f32) -> Result + where + E: de::Error, + { + BigDecimal::try_from(value).map_err(|err| E::custom(format!("{}", err))) + } + fn visit_f64(self, value: f64) -> Result where E: de::Error, { BigDecimal::try_from(value).map_err(|err| E::custom(format!("{}", err))) } + + fn visit_map(self, mut map: A) -> Result + where + A: de::MapAccess<'de>, + { + match map.next_key::<&str>() { + Ok(Some("$serde_json::private::Number")) => { + map.next_value::() + } + _ => { + Err(de::Error::invalid_type(de::Unexpected::Map, &self)) + } + } + } } #[cfg(not(feature = "string-only"))] From 654bed16a73b59ddc0394e729f0531ec30363074 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sun, 2 Jun 2024 14:01:47 -0400 Subject: [PATCH 43/50] Update formatting in serde test --- src/impl_serde.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/impl_serde.rs b/src/impl_serde.rs index 089e7b9..a23b762 100644 --- a/src/impl_serde.rs +++ b/src/impl_serde.rs @@ -136,7 +136,7 @@ mod test { impl_case!(case_50: "50" => "50"); impl_case!(case_50000: "50000" => "50000"); impl_case!(case_1en3: "1e-3" => "0.001"); - impl_case!(case_10e11: "10e11" => "1.0E+12"); + impl_case!(case_10e11: "10e11" => "1000000000000"); impl_case!(case_d25: ".25" => "0.25"); impl_case!(case_12d34e1: "12.34e1" => "123.4"); impl_case!(case_40d0010: "40.0010" => "40.0010"); From fb2db646f0505b90c6ca5acdc05e53432fb9d6f3 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Wed, 5 Jun 2024 01:21:36 -0400 Subject: [PATCH 44/50] Add test for json number serialization --- src/impl_serde.rs | 32 ++++++++++++++++++++++++++++++++ src/lib.rs | 3 +++ 2 files changed, 35 insertions(+) diff --git a/src/impl_serde.rs b/src/impl_serde.rs index a23b762..a7a2e61 100644 --- a/src/impl_serde.rs +++ b/src/impl_serde.rs @@ -226,6 +226,38 @@ mod test { assert_de_tokens_error::(&tokens, "NAN"); } } + + + #[cfg(feature = "serde_json")] + mod json_support { + use super::*; + use impl_serde::{Serialize, Deserialize}; + use serde_json; + + #[derive(Serialize,Deserialize)] + struct TestStruct { + name: String, + value: BigDecimal, + #[serde(with = "crate::serde::json_num")] + number: BigDecimal, + } + + + #[test] + fn test_struct_parsing() { + let json_src = r#" + { "name": "foo", "value": 0.0008741329382918, "number": "12.34" } + "#; + let my_struct: TestStruct = serde_json::from_str(&json_src).unwrap(); + assert_eq!(&my_struct.name, "foo"); + assert_eq!(&my_struct.value, &"0.0008741329382918".parse().unwrap()); + assert_eq!(&my_struct.number, &"12.34".parse().unwrap()); + + let s = serde_json::to_string(&my_struct).unwrap(); + assert_eq!(s, r#"{"name":"foo","value":"0.0008741329382918","number":12.34}"#); + } + + } } diff --git a/src/lib.rs b/src/lib.rs index dae0a2e..e2a3568 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,6 +65,9 @@ extern crate serde as serde_crate; #[cfg(all(test, any(feature = "serde", feature = "serde_json")))] extern crate serde_test; +#[cfg(all(test, feature = "serde_json"))] +extern crate serde_json; + #[cfg(feature = "std")] include!("./with_std.rs"); From 376da3252e1f816c4319b29fd8b26ff32f49c910 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Wed, 17 Apr 2024 01:44:22 -0400 Subject: [PATCH 45/50] Add Serialization section to README --- README.md | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/README.md b/README.md index 2b3e13c..d832674 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,67 @@ this code will print sqrt(2) = 1.414213562373095048801688724209698078569671875376948073176679737990732478462107038850387534327641573 ``` +### Serialization + +If you are passing BigDecimals between systems, be sure to use a serialization format +which explicitly supports decimal numbers and does not require transformations to +floating-point binary numbers, or there will be information loss. + +Text formats like JSON should work ok as long as the receiver will also parse +numbers as decimals so complete precision is kept accurate. +Typically JSON-parsing implementations do not do this by default, and need special +configuration. + +Binary formats like msgpack may expect/require representing numbers as 64-bit IEEE-754 +floating-point, and will likely lose precision by default unless you explicitly format +the decimal as a string, bytes, or some custom structure. + +By default, this will serialize the decimal _as a string_. + +To use `serde_json` with this crate it is recommended to enable the `serde-json` feature +(note `serde -dash- json` , not `serde_json`) and that will add support for +serializing & deserializing values to BigDecimal. +By default it will parse numbers and strings using normal conventions. +If you want to serialize to a number, rather than a string, you can use the +`serde(with)` annotation as shown in the following example: + +```toml +[dependencies] +bigdecimal = { version = "0.4", features = [ "serde-json" ] } # '-' not '_' +``` + +```rust +use bigdecimal::BigDecimal; +use serde::*; +use serde_json; + +#[derive(Debug,Serialize,Deserialize)] +struct MyStruct { + name: String, + // this will be witten to json as string + value: BigDecimal, + // this will be witten to json as number + #[serde(with = "bigdecimal::serde::json_num")] + number: BigDecimal, +} + +fn main() { + let json_src = r#" + { "name": "foo", "value": 1234567e-3, "number": 3.14159 } + "#; + + let my_struct: MyStruct = serde_json::from_str(&json_src).unwrap(); + dbg!(my_struct); + // MyStruct { name: "foo", value: BigDecimal("1234.567"), BigDecimal("3.1459") } + + println!("{}", serde_json::to_string(&my_struct)); + // {"name":"foo","value":"1234.567","number":3.1459} +} +``` + +If you have suggestions for improving serialization, please bring them +to the Zulip chat. + ### Formatting Until a more sophisticated formatting solution is implemented (currently work in progress), @@ -207,6 +268,7 @@ $ cargo run ``` > [!NOTE] +> > These are **compile time** environment variables, and the BigDecimal > library is not configurable at **runtime** via environment variable, or > any kind of global variables, by default. From 03d7241821c3ad9b68a7e119cfb0046d97503201 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Fri, 7 Jun 2024 23:31:14 -0400 Subject: [PATCH 46/50] Add compile-time variable safely limit when parsing json --- build.rs | 22 ++++++++++++++++++++++ src/impl_serde.rs | 2 ++ 2 files changed, 24 insertions(+) diff --git a/build.rs b/build.rs index 8321164..414eba9 100644 --- a/build.rs +++ b/build.rs @@ -8,6 +8,8 @@ const FMT_EXPONENTIAL_LOWER_THRESHOLD: &str = "5"; const FMT_EXPONENTIAL_UPPER_THRESHOLD: &str = "15"; const FMT_MAX_INTEGER_PADDING: &str = "1000"; +const SERDE_MAX_SCALE: &str = "150000"; + fn main() { let ac = autocfg::new(); @@ -23,6 +25,7 @@ fn main() { write_default_precision_file(&outdir); write_default_rounding_mode(&outdir); write_exponential_format_threshold_file(&outdir); + write_max_serde_parsing_scale_limit(&outdir); } /// Loads the environment variable string or default @@ -87,3 +90,22 @@ fn write_exponential_format_threshold_file(outdir: &Path) { std::fs::write(rust_file_path, rust_file_contents.join("\n")).unwrap(); } + + +/// Create write_default_rounding_mode.rs, containing definition of constant EXPONENTIAL_FORMAT_THRESHOLD loaded in src/impl_fmt.rs +fn write_max_serde_parsing_scale_limit(outdir: &Path) { + let scale_limit = load_env!(env, "RUST_BIGDECIMAL_SERDE_SCALE_LIMIT", SERDE_MAX_SCALE); + + let scale_limit: u32 = scale_limit + .parse::() + .or_else(|e| if scale_limit.to_lowercase() == "none" { Ok(0) } else { Err(e) }) + .expect("$RUST_BIGDECIMAL_SERDE_SCALE_LIMIT must be an integer"); + + let rust_file_path = outdir.join("serde_scale_limit.rs"); + + let rust_file_contents = [ + format!("const SERDE_SCALE_LIMIT: i64 = {};", scale_limit), + ]; + + std::fs::write(rust_file_path, rust_file_contents.join("\n")).unwrap(); +} diff --git a/src/impl_serde.rs b/src/impl_serde.rs index a7a2e61..989f982 100644 --- a/src/impl_serde.rs +++ b/src/impl_serde.rs @@ -4,6 +4,8 @@ use crate::*; use serde_crate::{self as serde, de, ser, Serialize, Deserialize}; +// const SERDE_SCALE_LIMIT: usize = = ${RUST_BIGDECIMAL_SERDE_SCALE_LIMIT} or 150_000; +include!(concat!(env!("OUT_DIR"), "/serde_scale_limit.rs")); impl ser::Serialize for BigDecimal { fn serialize(&self, serializer: S) -> Result From 6e457b31e2e42a4c7b22b11bf9a5e971b55afbbe Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sat, 8 Jun 2024 22:30:57 -0400 Subject: [PATCH 47/50] Use the scale safety-limit when parsing json --- src/impl_serde.rs | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/impl_serde.rs b/src/impl_serde.rs index 989f982..1b25fd5 100644 --- a/src/impl_serde.rs +++ b/src/impl_serde.rs @@ -293,10 +293,14 @@ pub mod arbitrary_precision { where D: serde::de::Deserializer<'de>, { - serde_json::Number::deserialize(deserializer)? - .as_str() - .parse() - .map_err(de::Error::custom) + let n = BigDecimal::deserialize(deserializer)?; + + if n.scale.abs() > SERDE_SCALE_LIMIT && SERDE_SCALE_LIMIT > 0 { + let msg = format!("Calculated exponent '{}' out of bounds", -n.scale); + Err(serde::de::Error::custom(msg)) + } else { + Ok(n) + } } pub fn serialize(value: &BigDecimal, serializer: S) -> Result @@ -438,5 +442,18 @@ mod test_jsonification { impl_case!(case_high_prec: r#"{ "value": 64771126779.35857825871133263810255301911 }"# => "64771126779.35857825871133263810255301911" ); impl_case!(case_nan: r#"{ "value": nan }"# => (error) ); + + + #[test] + fn scale_out_of_bounds() { + use serde_crate::de::Error; + + let src = r#"{ "value": 1e92233720392233 }"#; + + let parse_err = serde_json::from_str::(&src).err().unwrap(); + let err_str = parse_err.to_string(); + + assert!(err_str.starts_with("Calculated exponent '92233720392233' out of bounds"), "{}", err_str); + } } } From 5c8be4221beaf60bf0221570815a2ba25be3934b Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Wed, 12 Jun 2024 01:22:51 -0400 Subject: [PATCH 48/50] Add missing conditional --- src/impl_serde.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/impl_serde.rs b/src/impl_serde.rs index 1b25fd5..d299a5f 100644 --- a/src/impl_serde.rs +++ b/src/impl_serde.rs @@ -5,6 +5,7 @@ use crate::*; use serde_crate::{self as serde, de, ser, Serialize, Deserialize}; // const SERDE_SCALE_LIMIT: usize = = ${RUST_BIGDECIMAL_SERDE_SCALE_LIMIT} or 150_000; +#[cfg(feature = "serde_json")] include!(concat!(env!("OUT_DIR"), "/serde_scale_limit.rs")); impl ser::Serialize for BigDecimal { From 2633ef3d529c1fd2b46c41a16224d39828ff686a Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Thu, 13 Jun 2024 00:25:42 -0400 Subject: [PATCH 49/50] Further restrict versions of num-* dependencies --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9d98248..1a9e125 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,9 +21,9 @@ bench = false [dependencies] libm = "0.2.6" -num-bigint = { version = "<0.4.5", default-features = false } -num-integer = { version = "0.1", default-features = false } -num-traits = { version = "<0.2.19", default-features = false } +num-bigint = { version = ">=0.3.0,<0.4.5", default-features = false } +num-integer = { version = "^0.1", default-features = false } +num-traits = { version = ">0.2.0,<0.2.19", default-features = false } serde = { version = "1.0", optional = true, default-features = false } # Allow direct parsing of JSON floats, for full arbitrary precision serde_json = { version = "1.0", optional = true, default-features = false, features = ["alloc", "arbitrary_precision"]} From 86cecb4e0f8d1c0e94cafd8650928ec63f834942 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sat, 15 Jun 2024 09:00:18 -0400 Subject: [PATCH 50/50] Version 0.4.4 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 1a9e125..ad1ee1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bigdecimal" -version = "0.4.4+dev" +version = "0.4.4" authors = ["Andrew Kubera"] description = "Arbitrary precision decimal numbers" documentation = "https://docs.rs/bigdecimal"