From 351411addce5018f6808e85ad7a3460993e81340 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Mon, 16 Oct 2023 02:55:55 -0400 Subject: [PATCH 01/54] Begin v0.4.3 development --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 49bd37e..6fdfbea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bigdecimal" -version = "0.4.2" +version = "0.4.3+dev" authors = ["Andrew Kubera"] description = "Arbitrary precision decimal numbers" documentation = "https://docs.rs/bigdecimal" From 8ebccee0a0e1bbb2c7b5b0bb25c6ae829036d25c Mon Sep 17 00:00:00 2001 From: LinFeng Date: Mon, 15 Jan 2024 06:39:27 +0800 Subject: [PATCH 02/54] Remove unnecessary copy in add_refs_into --- src/context.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/context.rs b/src/context.rs index c48cc84..32beda9 100644 --- a/src/context.rs +++ b/src/context.rs @@ -128,9 +128,7 @@ impl Context { A: Into>, B: Into>, { - let a = a.into(); - let b = b.into(); - let sum = a.to_owned() + b.to_owned(); + let sum = a.into() + b.into(); *dest = sum.with_precision_round(self.precision, self.rounding) } } From 064e880e86d183a43f9e91ab2280fd29ea74f544 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sun, 14 Jan 2024 16:35:44 -0500 Subject: [PATCH 03/54] Move FromStr impl to separate file --- src/impl_trait_from_str.rs | 11 +++++++++++ src/lib.rs | 10 +--------- 2 files changed, 12 insertions(+), 9 deletions(-) create mode 100644 src/impl_trait_from_str.rs diff --git a/src/impl_trait_from_str.rs b/src/impl_trait_from_str.rs new file mode 100644 index 0000000..7f8538b --- /dev/null +++ b/src/impl_trait_from_str.rs @@ -0,0 +1,11 @@ +use crate::*; +use stdlib::str::FromStr; + +impl FromStr for BigDecimal { + type Err = ParseBigDecimalError; + + #[inline] + fn from_str(s: &str) -> Result { + BigDecimal::from_str_radix(s, 10) + } +} diff --git a/src/lib.rs b/src/lib.rs index e461a6a..0287363 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -97,6 +97,7 @@ mod arithmetic; // From, To, TryFrom impls mod impl_convert; +mod impl_trait_from_str; // Add, Sub, etc... mod impl_ops; @@ -918,15 +919,6 @@ impl From for ParseBigDecimalError { } } -impl FromStr for BigDecimal { - type Err = ParseBigDecimalError; - - #[inline] - fn from_str(s: &str) -> Result { - BigDecimal::from_str_radix(s, 10) - } -} - #[allow(deprecated)] // trim_right_match -> trim_end_match impl Hash for BigDecimal { fn hash(&self, state: &mut H) { From 7b926e484b5cbcc99dc478a9437b40a8ee2300a4 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sun, 14 Jan 2024 16:48:35 -0500 Subject: [PATCH 04/54] Move tests to impl_trait_from_str --- src/impl_trait_from_str.rs | 36 ++++++++++++++++++++++++++++++++++++ src/lib.rs | 30 ------------------------------ 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/src/impl_trait_from_str.rs b/src/impl_trait_from_str.rs index 7f8538b..112f2f5 100644 --- a/src/impl_trait_from_str.rs +++ b/src/impl_trait_from_str.rs @@ -9,3 +9,39 @@ impl FromStr for BigDecimal { BigDecimal::from_str_radix(s, 10) } } + + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! impl_case { + ($name:ident: $input:literal => $int:literal E $exp:literal) => { + #[test] + fn $name() { + let dec = BigDecimal::from_str($input).unwrap(); + assert_eq!(dec.int_val, $int.into()); + assert_eq!(dec.scale, -$exp); + } + }; + } + + impl_case!(case_1331d107: "1331.107" => 1331107 E -3 ); + impl_case!(case_1d0: "1.0" => 10 E -1 ); + impl_case!(case_2e1: "2e1" => 2 E 1 ); + impl_case!(case_0d00123: "0.00123" => 123 E -5); + impl_case!(case_n123: "-123" => -123 E -0); + impl_case!(case_n1230: "-1230" => -1230 E -0); + impl_case!(case_12d3: "12.3" => 123 E -1); + impl_case!(case_123en1: "123e-1" => 123 E -1); + impl_case!(case_1d23ep1: "1.23e+1" => 123 E -1); + impl_case!(case_1d23ep3: "1.23E+3" => 123 E 1); + impl_case!(case_1d23en8: "1.23E-8" => 123 E -10); + impl_case!(case_n1d23en10: "-1.23E-10" => -123 E -12); + impl_case!(case_123_: "123_" => 123 E -0); + impl_case!(case_31_862_140d830_686_979: "31_862_140.830_686_979" => 31862140830686979i128 E -9); + impl_case!(case_n1_1d2_2: "-1_1.2_2" => -1122 E -2); + impl_case!(case_999d521_939: "999.521_939" => 999521939 E -6); + impl_case!(case_679d35_84_03en2: "679.35_84_03E-2" => 679358403 E -8); + impl_case!(case_271576662d_e4: "271576662.__E4" => 271576662 E 4); +} diff --git a/src/lib.rs b/src/lib.rs index 0287363..74f816c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2150,36 +2150,6 @@ mod bigdecimal_tests { } } - #[test] - fn test_from_str() { - let vals = vec![ - ("1331.107", 1331107, 3), - ("1.0", 10, 1), - ("2e1", 2, -1), - ("0.00123", 123, 5), - ("-123", -123, 0), - ("-1230", -1230, 0), - ("12.3", 123, 1), - ("123e-1", 123, 1), - ("1.23e+1", 123, 1), - ("1.23E+3", 123, -1), - ("1.23E-8", 123, 10), - ("-1.23E-10", -123, 12), - ("123_", 123, 0), - ("31_862_140.830_686_979", 31862140830686979, 9), - ("-1_1.2_2", -1122, 2), - ("999.521_939", 999521939, 6), - ("679.35_84_03E-2", 679358403, 8), - ("271576662.__E4", 271576662, -4), - ]; - - for &(source, val, scale) in vals.iter() { - let x = BigDecimal::from_str(source).unwrap(); - assert_eq!(x.int_val.to_i64().unwrap(), val); - assert_eq!(x.scale, scale); - } - } - #[test] fn test_fmt() { let vals = vec![ From d749306315f26f38d891fb15669e3e7aad92fb69 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sun, 14 Jan 2024 17:02:11 -0500 Subject: [PATCH 05/54] Move invalid-str tests to impl_trait_from_str --- src/impl_trait_from_str.rs | 32 +++++++++++++++++++++++++++++ src/lib.rs | 41 -------------------------------------- 2 files changed, 32 insertions(+), 41 deletions(-) diff --git a/src/impl_trait_from_str.rs b/src/impl_trait_from_str.rs index 112f2f5..7e8de01 100644 --- a/src/impl_trait_from_str.rs +++ b/src/impl_trait_from_str.rs @@ -44,4 +44,36 @@ mod tests { impl_case!(case_999d521_939: "999.521_939" => 999521939 E -6); impl_case!(case_679d35_84_03en2: "679.35_84_03E-2" => 679358403 E -8); impl_case!(case_271576662d_e4: "271576662.__E4" => 271576662 E 4); + + impl_case!(case_1_d_2: "1_._2" => 12 E -1); +} + + +#[cfg(test)] +mod test_invalid { + use super::*; + + macro_rules! impl_case { + ($name:ident: $input:literal => $exp:literal) => { + #[test] + #[should_panic(expected = $exp)] + fn $name() { + BigDecimal::from_str($input).unwrap(); + } + }; + } + + impl_case!(case_bad_string_empty : "" => "Empty"); + impl_case!(case_bad_string_empty_exponent : "123.123E" => "Empty"); + impl_case!(case_bad_string_only_decimal_point : "." => "Empty"); + impl_case!(test_bad_string_only_decimal_and_exponent : ".e4" => "Empty"); + + impl_case!(test_bad_string_only_decimal_and_underscore : "_._" => "InvalidDigit"); + + impl_case!(case_bad_string_hello : "hello" => "InvalidDigit"); + impl_case!(case_bad_string_nan : "nan" => "InvalidDigit"); + impl_case!(case_bad_string_invalid_char : "12z3.12" => "InvalidDigit"); + impl_case!(case_bad_string_nan_exponent : "123.123eg" => "InvalidDigit"); + impl_case!(case_bad_string_multiple_decimal_points : "123.12.45" => "InvalidDigit"); + impl_case!(case_bad_string_hex : "0xCafeBeef" => "InvalidDigit"); } diff --git a/src/lib.rs b/src/lib.rs index 74f816c..eba35ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2228,47 +2228,6 @@ mod bigdecimal_tests { } } - #[test] - #[should_panic(expected = "InvalidDigit")] - fn test_bad_string_nan() { - BigDecimal::from_str("hello").unwrap(); - } - #[test] - #[should_panic(expected = "Empty")] - fn test_bad_string_empty() { - BigDecimal::from_str("").unwrap(); - } - #[test] - #[should_panic(expected = "InvalidDigit")] - fn test_bad_string_invalid_char() { - BigDecimal::from_str("12z3.12").unwrap(); - } - #[test] - #[should_panic(expected = "InvalidDigit")] - fn test_bad_string_nan_exponent() { - BigDecimal::from_str("123.123eg").unwrap(); - } - #[test] - #[should_panic(expected = "Empty")] - fn test_bad_string_empty_exponent() { - BigDecimal::from_str("123.123E").unwrap(); - } - #[test] - #[should_panic(expected = "InvalidDigit")] - fn test_bad_string_multiple_decimal_points() { - BigDecimal::from_str("123.12.45").unwrap(); - } - #[test] - #[should_panic(expected = "Empty")] - fn test_bad_string_only_decimal() { - BigDecimal::from_str(".").unwrap(); - } - #[test] - #[should_panic(expected = "Empty")] - fn test_bad_string_only_decimal_and_exponent() { - BigDecimal::from_str(".e4").unwrap(); - } - #[test] fn test_from_i128() { let value = BigDecimal::from_i128(-368934881474191032320).unwrap(); From f3406a7cb9cec69e7e231bcaa4d142215247aab1 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sun, 14 Jan 2024 17:05:14 -0500 Subject: [PATCH 06/54] Add comment on where to find real implementation --- src/impl_trait_from_str.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/impl_trait_from_str.rs b/src/impl_trait_from_str.rs index 7e8de01..4dd760f 100644 --- a/src/impl_trait_from_str.rs +++ b/src/impl_trait_from_str.rs @@ -6,6 +6,7 @@ impl FromStr for BigDecimal { #[inline] fn from_str(s: &str) -> Result { + // implemented in impl_num.rs BigDecimal::from_str_radix(s, 10) } } From b86dfc3fa189951cd2b9fdacc4521530bc5dd6a1 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 07/54] Move serde implementation into impl_serde.rs --- src/impl_serde.rs | 164 ++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 169 ++-------------------------------------------- 2 files changed, 168 insertions(+), 165 deletions(-) create mode 100644 src/impl_serde.rs diff --git a/src/impl_serde.rs b/src/impl_serde.rs new file mode 100644 index 0000000..58aabb4 --- /dev/null +++ b/src/impl_serde.rs @@ -0,0 +1,164 @@ +//! +//! Support for serde implementations +//! +use crate::*; +use serde::{de, ser}; + +#[allow(unused_imports)] +use num_traits::FromPrimitive; + +impl ser::Serialize for BigDecimal { + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + serializer.collect_str(&self) + } +} + +struct BigDecimalVisitor; + +impl<'de> de::Visitor<'de> for BigDecimalVisitor { + type Value = BigDecimal; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "a number or formatted decimal string") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + BigDecimal::from_str(value).map_err(|err| E::custom(format!("{}", err))) + } + + fn visit_u64(self, value: u64) -> Result + where + E: de::Error, + { + Ok(BigDecimal::from(value)) + } + + fn visit_i64(self, value: i64) -> Result + where + E: de::Error, + { + Ok(BigDecimal::from(value)) + } + + fn visit_f64(self, value: f64) -> Result + where + E: de::Error, + { + BigDecimal::try_from(value).map_err(|err| E::custom(format!("{}", err))) + } +} + +#[cfg(not(feature = "string-only"))] +impl<'de> de::Deserialize<'de> for BigDecimal { + fn deserialize(d: D) -> Result + where + D: de::Deserializer<'de>, + { + d.deserialize_any(BigDecimalVisitor) + } +} + +#[cfg(feature = "string-only")] +impl<'de> de::Deserialize<'de> for BigDecimal { + fn deserialize(d: D) -> Result + where + D: de::Deserializer<'de>, + { + d.deserialize_str(BigDecimalVisitor) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_serde_serialize() { + let vals = vec![ + ("1.0", "1.0"), + ("0.5", "0.5"), + ("50", "50"), + ("50000", "50000"), + ("1e-3", "0.001"), + ("1e12", "1000000000000"), + ("0.25", "0.25"), + ("12.34", "12.34"), + ("0.15625", "0.15625"), + ("0.3333333333333333", "0.3333333333333333"), + ("3.141592653589793", "3.141592653589793"), + ("94247.77960769380", "94247.77960769380"), + ("10.99", "10.99"), + ("12.0010", "12.0010"), + ]; + for (s, v) in vals { + let expected = format!("\"{}\"", v); + let value = serde_json::to_string(&BigDecimal::from_str(s).unwrap()).unwrap(); + assert_eq!(expected, value); + } + } + + #[test] + fn test_serde_deserialize_str() { + let vals = vec![ + ("1.0", "1.0"), + ("0.5", "0.5"), + ("50", "50"), + ("50000", "50000"), + ("1e-3", "0.001"), + ("1e12", "1000000000000"), + ("0.25", "0.25"), + ("12.34", "12.34"), + ("0.15625", "0.15625"), + ("0.3333333333333333", "0.3333333333333333"), + ("3.141592653589793", "3.141592653589793"), + ("94247.77960769380", "94247.77960769380"), + ("10.99", "10.99"), + ("12.0010", "12.0010"), + ]; + for (s, v) in vals { + let expected = BigDecimal::from_str(v).unwrap(); + let value: BigDecimal = serde_json::from_str(&format!("\"{}\"", s)).unwrap(); + assert_eq!(expected, value); + } + } + + #[test] + #[cfg(not(feature = "string-only"))] + fn test_serde_deserialize_int() { + let vals = vec![0, 1, 81516161, -370, -8, -99999999999]; + for n in vals { + let expected = BigDecimal::from_i64(n).unwrap(); + let value: BigDecimal = serde_json::from_str(&serde_json::to_string(&n).unwrap()).unwrap(); + assert_eq!(expected, value); + } + } + + #[test] + #[cfg(not(feature = "string-only"))] + fn test_serde_deserialize_f64() { + let vals = vec![ + 1.0, + 0.5, + 0.25, + 50.0, + 50000., + 0.001, + 12.34, + 5.0 * 0.03125, + stdlib::f64::consts::PI, + stdlib::f64::consts::PI * 10000.0, + stdlib::f64::consts::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); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index eba35ae..a90c824 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -113,6 +113,10 @@ mod impl_cmp; // Implementations of num_traits mod impl_num; +// Implementations for deserializations and serializations +#[cfg(feature = "serde")] +pub mod impl_serde; + // construct BigDecimals from strings and floats mod parsing; @@ -1344,171 +1348,6 @@ impl<'a> From<&'a BigInt> for BigDecimalRef<'a> { } } - -/// Tools to help serializing/deserializing `BigDecimal`s -#[cfg(feature = "serde")] -mod bigdecimal_serde { - use super::*; - use serde::{de, ser}; - - #[allow(unused_imports)] - use num_traits::FromPrimitive; - - impl ser::Serialize for BigDecimal { - fn serialize(&self, serializer: S) -> Result - where - S: ser::Serializer, - { - serializer.collect_str(&self) - } - } - - struct BigDecimalVisitor; - - impl<'de> de::Visitor<'de> for BigDecimalVisitor { - type Value = BigDecimal; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - write!(formatter, "a number or formatted decimal string") - } - - fn visit_str(self, value: &str) -> Result - where - E: de::Error, - { - BigDecimal::from_str(value).map_err(|err| E::custom(format!("{}", err))) - } - - fn visit_u64(self, value: u64) -> Result - where - E: de::Error, - { - Ok(BigDecimal::from(value)) - } - - fn visit_i64(self, value: i64) -> Result - where - E: de::Error, - { - Ok(BigDecimal::from(value)) - } - - fn visit_f64(self, value: f64) -> Result - where - E: de::Error, - { - BigDecimal::try_from(value).map_err(|err| E::custom(format!("{}", err))) - } - } - - #[cfg(not(feature = "string-only"))] - impl<'de> de::Deserialize<'de> for BigDecimal { - fn deserialize(d: D) -> Result - where - D: de::Deserializer<'de>, - { - d.deserialize_any(BigDecimalVisitor) - } - } - - #[cfg(feature = "string-only")] - impl<'de> de::Deserialize<'de> for BigDecimal { - fn deserialize(d: D) -> Result - where - D: de::Deserializer<'de>, - { - d.deserialize_str(BigDecimalVisitor) - } - } - - #[cfg(test)] - extern crate serde_json; - - #[test] - fn test_serde_serialize() { - let vals = vec![ - ("1.0", "1.0"), - ("0.5", "0.5"), - ("50", "50"), - ("50000", "50000"), - ("1e-3", "0.001"), - ("1e12", "1000000000000"), - ("0.25", "0.25"), - ("12.34", "12.34"), - ("0.15625", "0.15625"), - ("0.3333333333333333", "0.3333333333333333"), - ("3.141592653589793", "3.141592653589793"), - ("94247.77960769380", "94247.77960769380"), - ("10.99", "10.99"), - ("12.0010", "12.0010"), - ]; - for (s, v) in vals { - let expected = format!("\"{}\"", v); - let value = serde_json::to_string(&BigDecimal::from_str(s).unwrap()).unwrap(); - assert_eq!(expected, value); - } - } - - #[test] - fn test_serde_deserialize_str() { - let vals = vec![ - ("1.0", "1.0"), - ("0.5", "0.5"), - ("50", "50"), - ("50000", "50000"), - ("1e-3", "0.001"), - ("1e12", "1000000000000"), - ("0.25", "0.25"), - ("12.34", "12.34"), - ("0.15625", "0.15625"), - ("0.3333333333333333", "0.3333333333333333"), - ("3.141592653589793", "3.141592653589793"), - ("94247.77960769380", "94247.77960769380"), - ("10.99", "10.99"), - ("12.0010", "12.0010"), - ]; - for (s, v) in vals { - let expected = BigDecimal::from_str(v).unwrap(); - let value: BigDecimal = serde_json::from_str(&format!("\"{}\"", s)).unwrap(); - assert_eq!(expected, value); - } - } - - #[test] - #[cfg(not(feature = "string-only"))] - fn test_serde_deserialize_int() { - let vals = vec![0, 1, 81516161, -370, -8, -99999999999]; - for n in vals { - let expected = BigDecimal::from_i64(n).unwrap(); - let value: BigDecimal = serde_json::from_str(&serde_json::to_string(&n).unwrap()).unwrap(); - assert_eq!(expected, value); - } - } - - #[test] - #[cfg(not(feature = "string-only"))] - fn test_serde_deserialize_f64() { - let vals = vec![ - 1.0, - 0.5, - 0.25, - 50.0, - 50000., - 0.001, - 12.34, - 5.0 * 0.03125, - stdlib::f64::consts::PI, - stdlib::f64::consts::PI * 10000.0, - stdlib::f64::consts::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); - } - } -} - #[rustfmt::skip] #[cfg(test)] #[allow(non_snake_case)] From 909062bb8b9338e26d3a9b7b911c551dda93f100 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sun, 14 Jan 2024 19:55:46 -0500 Subject: [PATCH 08/54] Rewrite serde tests using macros --- src/impl_serde.rs | 174 +++++++++++++++++++++++++++------------------- 1 file changed, 101 insertions(+), 73 deletions(-) diff --git a/src/impl_serde.rs b/src/impl_serde.rs index 58aabb4..ed5a179 100644 --- a/src/impl_serde.rs +++ b/src/impl_serde.rs @@ -4,8 +4,6 @@ use crate::*; use serde::{de, ser}; -#[allow(unused_imports)] -use num_traits::FromPrimitive; impl ser::Serialize for BigDecimal { fn serialize(&self, serializer: S) -> Result @@ -16,6 +14,7 @@ impl ser::Serialize for BigDecimal { } } +/// Used by SerDe to construct a BigDecimal struct BigDecimalVisitor; impl<'de> de::Visitor<'de> for BigDecimalVisitor { @@ -77,88 +76,117 @@ impl<'de> de::Deserialize<'de> for BigDecimal { #[cfg(test)] mod test { use super::*; - - #[test] - fn test_serde_serialize() { - let vals = vec![ - ("1.0", "1.0"), - ("0.5", "0.5"), - ("50", "50"), - ("50000", "50000"), - ("1e-3", "0.001"), - ("1e12", "1000000000000"), - ("0.25", "0.25"), - ("12.34", "12.34"), - ("0.15625", "0.15625"), - ("0.3333333333333333", "0.3333333333333333"), - ("3.141592653589793", "3.141592653589793"), - ("94247.77960769380", "94247.77960769380"), - ("10.99", "10.99"), - ("12.0010", "12.0010"), - ]; - for (s, v) in vals { - let expected = format!("\"{}\"", v); - let value = serde_json::to_string(&BigDecimal::from_str(s).unwrap()).unwrap(); - assert_eq!(expected, value); + use paste::paste; + + mod serde_serialize { + use super::*; + + macro_rules! impl_case { + ($name:ident : $input:literal => $output:literal) => { + #[test] + fn $name() { + let expected = format!("\"{}\"", $output); + let decimal: BigDecimal = $input.parse().unwrap(); + let value = serde_json::to_string(&decimal).unwrap(); + assert_eq!(expected, value); + } + } } + + impl_case!(case_1d0: "1.0" => "1.0"); + impl_case!(case_0d5: "0.5" => "0.5"); + impl_case!(case_50: "50" => "50"); + impl_case!(case_50000: "50000" => "50000"); + impl_case!(case_1en3: "1e-3" => "0.001"); + impl_case!(case_1e12: "1e12" => "1000000000000"); + impl_case!(case_d25: ".25" => "0.25"); + impl_case!(case_12d34e1: "12.34e1" => "123.4"); + impl_case!(case_40d0010: "40.0010" => "40.0010"); } - #[test] - fn test_serde_deserialize_str() { - let vals = vec![ - ("1.0", "1.0"), - ("0.5", "0.5"), - ("50", "50"), - ("50000", "50000"), - ("1e-3", "0.001"), - ("1e12", "1000000000000"), - ("0.25", "0.25"), - ("12.34", "12.34"), - ("0.15625", "0.15625"), - ("0.3333333333333333", "0.3333333333333333"), - ("3.141592653589793", "3.141592653589793"), - ("94247.77960769380", "94247.77960769380"), - ("10.99", "10.99"), - ("12.0010", "12.0010"), - ]; - for (s, v) in vals { - let expected = BigDecimal::from_str(v).unwrap(); - let value: BigDecimal = serde_json::from_str(&format!("\"{}\"", s)).unwrap(); - assert_eq!(expected, value); + mod serde_deserialize_str { + use super::*; + + macro_rules! impl_case { + ($name:ident : $input:literal => $output:literal) => { + #[test] + fn $name() { + let expected: BigDecimal = $output.parse().unwrap(); + + let s = $input; + let value: BigDecimal = serde_json::from_str(&format!("\"{}\"", s)).unwrap(); + assert_eq!(expected, value); + } + } } + + impl_case!(case_1d0: "1.0" => "1.0"); + impl_case!(case_0d5: "0.5" => "0.5"); + impl_case!(case_50: "50" => "50"); + impl_case!(case_50000: "50000" => "50000"); + impl_case!(case_1en3: "1e-3" => "0.001"); + impl_case!(case_1e12: "1e12" => "1000000000000"); + impl_case!(case_d25: ".25" => "0.25"); + impl_case!(case_12d34e1: "12.34e1" => "123.4"); + impl_case!(case_40d0010: "40.0010" => "40.0010"); } - #[test] + #[cfg(not(feature = "string-only"))] - fn test_serde_deserialize_int() { - let vals = vec![0, 1, 81516161, -370, -8, -99999999999]; - for n in vals { - let expected = BigDecimal::from_i64(n).unwrap(); - let value: BigDecimal = serde_json::from_str(&serde_json::to_string(&n).unwrap()).unwrap(); - assert_eq!(expected, value); + mod serde_deserialize_int { + use super::*; + + macro_rules! impl_case { + (-$input:literal) => { + paste! { impl_case!([< case_n $input >] : -$input); } + }; + ($input:literal) => { + paste! { impl_case!([< case_ $input >] : $input); } + }; + ($name:ident : $input:literal) => { + #[test] + fn $name() { + let n = $input; + let expected = BigDecimal::from_i64(n).unwrap(); + let value: BigDecimal = serde_json::from_str(&serde_json::to_string(&n).unwrap()).unwrap(); + assert_eq!(expected, value); + } + } } + + impl_case!(0); + impl_case!(1); + impl_case!(81516161); + impl_case!(-370); + impl_case!(-8); + impl_case!(-99999999999); } - #[test] + #[cfg(not(feature = "string-only"))] - fn test_serde_deserialize_f64() { - let vals = vec![ - 1.0, - 0.5, - 0.25, - 50.0, - 50000., - 0.001, - 12.34, - 5.0 * 0.03125, - stdlib::f64::consts::PI, - stdlib::f64::consts::PI * 10000.0, - stdlib::f64::consts::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 serde_deserialize_f64 { + use super::*; + use stdlib::f64::consts; + + macro_rules! impl_case { + ($name:ident : $input:expr) => { + #[test] + fn $name() { + let n = $input; + 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); + } + } } + + impl_case!(case_1d0: 1.0); + impl_case!(case_0d1: 0.1); + impl_case!(case_0d5: 0.5); + impl_case!(case_50d0: 50.0); + impl_case!(case_pi: consts::PI); + impl_case!(case_pi_times_100: consts::PI * 100.0); + impl_case!(case_pi_times_30000: consts::PI * 30000.0); } + } From 8355022a303a552568899d62cb70b8eef434b303 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Mon, 15 Jan 2024 20:51:43 -0500 Subject: [PATCH 09/54] Replace serde_json with serde_test --- Cargo.toml | 2 +- src/impl_serde.rs | 128 +++++++++++++++++++++++----------------------- src/lib.rs | 3 ++ 3 files changed, 69 insertions(+), 64 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6fdfbea..50c9dfd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ serde = { version = "1.0", optional = true, default-features = false } [dev-dependencies] paste = "1" -serde_json = "<1.0.101" +serde_test = "<1.0.176" siphasher = { version = "0.3.10", default-features = false } # The following dev-dependencies are only required for benchmarking # (use the `benchmark-bigdecimal` script to uncomment these and run benchmarks) diff --git a/src/impl_serde.rs b/src/impl_serde.rs index ed5a179..e8bdac7 100644 --- a/src/impl_serde.rs +++ b/src/impl_serde.rs @@ -78,17 +78,20 @@ mod test { use super::*; use paste::paste; - mod serde_serialize { + use serde_test::{ + Token, assert_tokens, assert_de_tokens, assert_de_tokens_error + }; + + mod serde_serialize_deserialize_str { use super::*; macro_rules! impl_case { ($name:ident : $input:literal => $output:literal) => { #[test] fn $name() { - let expected = format!("\"{}\"", $output); + let expected = Token::Str($output); let decimal: BigDecimal = $input.parse().unwrap(); - let value = serde_json::to_string(&decimal).unwrap(); - assert_eq!(expected, value); + assert_tokens(&decimal, &[expected]); } } } @@ -104,89 +107,88 @@ mod test { impl_case!(case_40d0010: "40.0010" => "40.0010"); } - mod serde_deserialize_str { + #[cfg(not(feature = "string-only"))] + mod serde_deserialize_int { use super::*; macro_rules! impl_case { - ($name:ident : $input:literal => $output:literal) => { + ( $( $ttype:ident ),+ : -$input:literal ) => { + $( paste! { impl_case!([< case_n $input _ $ttype:lower >] : $ttype : -$input); } )* + }; + ( $( $ttype:ident ),+ : $input:literal ) => { + $( paste! { impl_case!([< case_ $input _ $ttype:lower >] : $ttype : $input); } )* + }; + ($name:ident : $type:ident : $input:literal) => { #[test] fn $name() { - let expected: BigDecimal = $output.parse().unwrap(); - - let s = $input; - let value: BigDecimal = serde_json::from_str(&format!("\"{}\"", s)).unwrap(); - assert_eq!(expected, value); + let expected = BigDecimal::from($input); + let token = Token::$type($input); + assert_de_tokens(&expected, &[token]); } - } + }; } - impl_case!(case_1d0: "1.0" => "1.0"); - impl_case!(case_0d5: "0.5" => "0.5"); - impl_case!(case_50: "50" => "50"); - impl_case!(case_50000: "50000" => "50000"); - impl_case!(case_1en3: "1e-3" => "0.001"); - impl_case!(case_1e12: "1e12" => "1000000000000"); - impl_case!(case_d25: ".25" => "0.25"); - impl_case!(case_12d34e1: "12.34e1" => "123.4"); - impl_case!(case_40d0010: "40.0010" => "40.0010"); + impl_case!(I8, I16, I32, I64, U8, U16, U32, U64 : 0); + impl_case!(I8, I16, I32, I64, U8, U16, U32, U64 : 1); + impl_case!(I8, I16, I32, I64 : -1); + impl_case!(I64: -99999999999i64); + impl_case!(I64: -9_223_372_036_854_775_808i64); } - #[cfg(not(feature = "string-only"))] - mod serde_deserialize_int { + mod serde_deserialize_float { use super::*; macro_rules! impl_case { - (-$input:literal) => { - paste! { impl_case!([< case_n $input >] : -$input); } + ( $name:ident : $input:literal => $ttype:ident : $expected:literal ) => { + paste! { + #[test] + fn [< $name _ $ttype:lower >]() { + let expected: BigDecimal = $expected.parse().unwrap(); + let token = Token::$ttype($input); + assert_de_tokens(&expected, &[token]); + } + } }; - ($input:literal) => { - paste! { impl_case!([< case_ $input >] : $input); } + ( $name:ident : $input:literal => $( $ttype:ident : $expected:literal )+ ) => { + $( impl_case!($name : $input => $ttype : $expected); )* + }; + ( $name:ident : $input:literal => $( $ttype:ident ),+ : $expected:literal ) => { + $( impl_case!($name : $input => $ttype : $expected); )* }; - ($name:ident : $input:literal) => { - #[test] - fn $name() { - let n = $input; - let expected = BigDecimal::from_i64(n).unwrap(); - let value: BigDecimal = serde_json::from_str(&serde_json::to_string(&n).unwrap()).unwrap(); - assert_eq!(expected, value); - } - } } - impl_case!(0); - impl_case!(1); - impl_case!(81516161); - impl_case!(-370); - impl_case!(-8); - impl_case!(-99999999999); - } + impl_case!(case_1d0 : 1.0 => F32, F64 : "1"); + impl_case!(case_1d1 : 1.1 => F32 : "1.10000002384185791015625" + F64 : "1.100000000000000088817841970012523233890533447265625"); + + impl_case!(case_0d001834988943300: + 0.001834988943300 => F32 : "0.001834988943301141262054443359375" + F64 : "0.00183498894330000003084768511740776375518180429935455322265625"); + impl_case!(case_n869651d9131236838: + -869651.9131236838 => F32 : "-869651.9375" + F64 : "-869651.91312368377111852169036865234375"); + + impl_case!(case_n1en20: + -1e-20 => F32 : "-9.999999682655225388967887463487205224055287544615566730499267578125E-21" + F64 : "-999999999999999945153271454209571651729503702787392447107715776066783064379706047475337982177734375e-119"); + } #[cfg(not(feature = "string-only"))] - mod serde_deserialize_f64 { + mod serde_deserialize_nan { use super::*; - use stdlib::f64::consts; - macro_rules! impl_case { - ($name:ident : $input:expr) => { - #[test] - fn $name() { - let n = $input; - 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); - } - } + #[test] + fn case_f32() { + let tokens = [ Token::F32(f32::NAN) ]; + assert_de_tokens_error::(&tokens, "NAN"); } - impl_case!(case_1d0: 1.0); - impl_case!(case_0d1: 0.1); - impl_case!(case_0d5: 0.5); - impl_case!(case_50d0: 50.0); - impl_case!(case_pi: consts::PI); - impl_case!(case_pi_times_100: consts::PI * 100.0); - impl_case!(case_pi_times_30000: consts::PI * 30000.0); + #[test] + fn case_f64() { + let tokens = [ Token::F64(f64::NAN) ]; + assert_de_tokens_error::(&tokens, "NAN"); + } } - } diff --git a/src/lib.rs b/src/lib.rs index a90c824..46a3a38 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,6 +59,9 @@ extern crate paste; #[cfg(feature = "serde")] extern crate serde; +#[cfg(all(test, feature = "serde"))] +extern crate serde_test; + #[cfg(feature = "std")] include!("./with_std.rs"); From 332db69163261cdb8b3eab49c349b2065e21ab5e Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Mon, 15 Jan 2024 22:50:57 -0500 Subject: [PATCH 10/54] Fix clippy warnings in examples --- examples/floating-precision.rs | 2 +- examples/simple-math.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/floating-precision.rs b/examples/floating-precision.rs index 2813a1e..14f4e8f 100644 --- a/examples/floating-precision.rs +++ b/examples/floating-precision.rs @@ -4,7 +4,7 @@ use bigdecimal::BigDecimal; use std::str::FromStr; fn main() { - let input = std::env::args().skip(1).next().unwrap_or("0.7".to_string()); + let input = std::env::args().nth(1).unwrap_or("0.7".to_string()); let decimal = BigDecimal::from_str(&input).expect("invalid decimal"); let floating = f32::from_str(&input).expect("invalid float"); diff --git a/examples/simple-math.rs b/examples/simple-math.rs index c4cab2d..097cde0 100644 --- a/examples/simple-math.rs +++ b/examples/simple-math.rs @@ -21,8 +21,8 @@ sum mut: 48.00000000000000 fn main() { println!("Hello, Big Decimals!"); let input = "0.8"; - let dec = BigDecimal::from_str(&input).unwrap(); - let float = f32::from_str(&input).unwrap(); + let dec = BigDecimal::from_str(input).unwrap(); + let float = f32::from_str(input).unwrap(); println!("Input ({}) with 10 decimals: {} vs {})", input, dec, float); let bd_square = dec.square(); From fbb947b435df0519da9fe3d9fb2f4569db492b80 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Mon, 15 Jan 2024 23:00:38 -0500 Subject: [PATCH 11/54] Allow some clippy violations --- src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 46a3a38..f0779f3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,11 +41,14 @@ //! ``` #![cfg_attr(not(feature = "std"), no_std)] #![allow(clippy::style)] +#![allow(clippy::excessive_precision)] #![allow(clippy::unreadable_literal)] #![allow(clippy::needless_return)] #![allow(clippy::suspicious_arithmetic_impl)] #![allow(clippy::suspicious_op_assign_impl)] #![allow(clippy::redundant_field_names)] +#![allow(clippy::approx_constant)] +#![cfg_attr(test, allow(clippy::useless_vec))] #![allow(unused_imports)] From 66e97b7f1961acf424e63d8ef1bfcc78f2a7557f Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Mon, 15 Jan 2024 21:34:41 -0500 Subject: [PATCH 12/54] Update circle-ci config --- .circleci/config.yml | 94 +++++++++++++++++++++++++++++++------------- 1 file changed, 66 insertions(+), 28 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index bf4eb2b..fba6b07 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,17 +1,17 @@ version: 2.1 orbs: -# codecov: codecov/codecov@3.2.4 - rust: circleci/rust@1.6.0 + # codecov: codecov/codecov@3.3.0 + rust: circleci/rust@1.6.1 jobs: build-and-test: parameters: rust-version: type: string - default: "1.69.0" + default: "1.75.0" debian-version: type: string - default: "buster" + default: "bookworm" rust-features: type: string default: "--all-targets" @@ -49,7 +49,7 @@ jobs: type: string debian-version: type: string - default: "bullseye" + default: "bookworm" machine: true steps: - checkout @@ -65,54 +65,92 @@ jobs: sh -c 'cargo test -q --no-run && kcov-rust && upload-kcov-results-to-codecov' - store_artifacts: path: target/cov - # - store_test_results: - # path: target lint-check: docker: - - image: cimg/rust:1.69 + - image: cimg/rust:1.75 steps: - checkout + - run: + name: Generate cargo.lock + command: cargo generate-lockfile - rust/build: with_cache: false # - rust/format - # - rust/clippy + - rust/clippy - rust/test - run: name: Build examples command: cargo build --examples + cargo-semver-check: + docker: + - image: "akubera/rust:stable" + steps: + - checkout + - run: + name: Tool Versions + command: > + rustc --version + && cargo --version + && cargo semver-checks --version + - run: + name: cargo semver-checks + command: cargo semver-checks --verbose + - run: + name: cargo semver-checks (no-std) + command: cargo semver-checks --verbose --only-explicit-features + workflows: version: 2 cargo:build-and-test: jobs: + - rust/lint-test-build: + name: "lint-test-build:stable" + release: true + version: "1.75" + pre-steps: + - checkout + - run: + command: cargo generate-lockfile + - rust/lint-test-build: + name: "lint-test-build:1.56" + release: true + version: "1.56" + - lint-check + + - build-and-test: + name: build-and-test:MSRV + rust-version: "1.43.1" + debian-version: "buster" + - build-and-test: - matrix: - parameters: - rust-version: - - "1.43.1" - - "1.54.0" + name: build-and-test:MSRV:serde + rust-version: "1.43.1" + debian-version: "buster" + rust-features: "--all-targets --features='serde'" - build-and-test: - name: build-and-test:latest - debian-version: "bullseye" + name: build-and-test:latest - build-and-test: - matrix: - parameters: - rust-version: - - "1.43.1" - - "1.69.0" - rust-features: - - "--features='serde'" - - "--features='serde,string-only'" + name: build-and-test:latest:serde + rust-features: "--all-targets --features='serde'" - build-and-test: - name: build-and-test:no-default-features - rust-features: "--no-default-features" + name: build-and-test:no_std + rust-features: "--no-default-features" + + - build-and-test: + name: build-and-test:serde+no_std + rust-features: "--no-default-features --features='serde'" + + - cargo-semver-check: + requires: + - build-and-test:latest:serde - upload-coverage: - rust-version: "1.69.0" + rust-version: "1.75.0" requires: - - build-and-test:latest + - build-and-test:latest:serde From 246b91abbc9b263e172e3100f3bc816d9c0186b6 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Mon, 15 Jan 2024 17:27:44 -0500 Subject: [PATCH 13/54] Allow clippy float-comparisons in division impl --- src/impl_ops.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/impl_ops.rs b/src/impl_ops.rs index 565d94b..b7afec1 100644 --- a/src/impl_ops.rs +++ b/src/impl_ops.rs @@ -356,6 +356,7 @@ macro_rules! impl_div_for_primitive { impl Div<$t> for BigDecimal { type Output = BigDecimal; + #[allow(clippy::float_cmp)] fn div(self, denom: $t) -> BigDecimal { if !denom.is_normal() { BigDecimal::zero() From bceaa0960ae98aeb824864bcdd72d92837ed15a9 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Mon, 15 Jan 2024 17:29:49 -0500 Subject: [PATCH 14/54] Improve multiplication implementation --- src/impl_ops_mul.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/impl_ops_mul.rs b/src/impl_ops_mul.rs index 2b1df37..181ce9d 100644 --- a/src/impl_ops_mul.rs +++ b/src/impl_ops_mul.rs @@ -30,15 +30,15 @@ impl<'a> Mul<&'a BigDecimal> for BigDecimal { if self.is_one() { self.scale = rhs.scale; self.int_val.set_zero(); - self.int_val.add_assign(&rhs.int_val); - self - } else if rhs.is_one() { - self - } else { + self.int_val += &rhs.int_val; + } else if rhs.is_zero() { + self.scale = 0; + self.int_val.set_zero(); + } else if !self.is_zero() && !rhs.is_one() { self.scale += rhs.scale; - MulAssign::mul_assign(&mut self.int_val, &rhs.int_val); - self + self.int_val *= &rhs.int_val; } + self } } From 8b614b315eb7d77f59a4b2a0df02604a61fc1109 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Tue, 16 Jan 2024 21:36:16 -0500 Subject: [PATCH 15/54] Create impl_fmt.rs (history preserving) --- src/{lib.rs => impl_fmt.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{lib.rs => impl_fmt.rs} (100%) diff --git a/src/lib.rs b/src/impl_fmt.rs similarity index 100% rename from src/lib.rs rename to src/impl_fmt.rs From c7262705df2afef04bb39a596adc3aa211da2b8b Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Tue, 16 Jan 2024 21:40:06 -0500 Subject: [PATCH 16/54] Isolate fmt::{Display,Debug} and tests in impl_fmt.rs --- src/impl_fmt.rs | 2063 +---------------------------------------------- 1 file changed, 32 insertions(+), 2031 deletions(-) diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index f0779f3..cdc02bb 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -1,1139 +1,8 @@ -// Copyright 2016 Adam Sunderland -// 2016-2023 Andrew Kubera -// 2017 Ruben De Smet -// See the COPYRIGHT file at the top-level directory of this -// distribution. -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -//! A Big Decimal -//! -//! `BigDecimal` allows storing any real number to arbitrary precision; which -//! avoids common floating point errors (such as 0.1 + 0.2 ≠ 0.3) at the -//! cost of complexity. -//! -//! Internally, `BigDecimal` uses a `BigInt` object, paired with a 64-bit -//! integer which determines the position of the decimal point. Therefore, -//! the precision *is not* actually arbitrary, but limited to 263 -//! decimal places. -//! -//! Common numerical operations are overloaded, so we can treat them -//! the same way we treat other numbers. -//! -//! It is not recommended to convert a floating point number to a decimal -//! directly, as the floating point representation may be unexpected. -//! -//! # Example +//! fmt implementations and stringification routines //! -//! ``` -//! use bigdecimal::BigDecimal; -//! use std::str::FromStr; -//! -//! let input = "0.8"; -//! let dec = BigDecimal::from_str(&input).unwrap(); -//! let float = f32::from_str(&input).unwrap(); -//! -//! println!("Input ({}) with 10 decimals: {} vs {})", input, dec, float); -//! ``` -#![cfg_attr(not(feature = "std"), no_std)] -#![allow(clippy::style)] -#![allow(clippy::excessive_precision)] -#![allow(clippy::unreadable_literal)] -#![allow(clippy::needless_return)] -#![allow(clippy::suspicious_arithmetic_impl)] -#![allow(clippy::suspicious_op_assign_impl)] -#![allow(clippy::redundant_field_names)] -#![allow(clippy::approx_constant)] -#![cfg_attr(test, allow(clippy::useless_vec))] -#![allow(unused_imports)] - - -pub extern crate num_bigint; -pub extern crate num_traits; -extern crate num_integer; - -#[cfg(test)] -extern crate paste; - -#[cfg(feature = "serde")] -extern crate serde; - -#[cfg(all(test, feature = "serde"))] -extern crate serde_test; - -#[cfg(feature = "std")] -include!("./with_std.rs"); - -#[cfg(not(feature = "std"))] -include!("./without_std.rs"); - -// make available some standard items -use self::stdlib::cmp::{self, Ordering}; -use self::stdlib::convert::TryFrom; -use self::stdlib::default::Default; -use self::stdlib::hash::{Hash, Hasher}; -use self::stdlib::num::{ParseFloatError, ParseIntError}; -use self::stdlib::ops::{ - Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign, Rem, RemAssign, -}; -use self::stdlib::iter::Sum; -use self::stdlib::str::FromStr; -use self::stdlib::string::{String, ToString}; -use self::stdlib::fmt; - -use num_bigint::{BigInt, BigUint, ParseBigIntError, Sign}; -use num_integer::Integer as IntegerTrait; -pub use num_traits::{FromPrimitive, Num, One, Signed, ToPrimitive, Zero}; - -use stdlib::f64::consts::LOG2_10; - - -// const DEFAULT_PRECISION: u64 = ${RUST_BIGDECIMAL_DEFAULT_PRECISION} or 100; -include!(concat!(env!("OUT_DIR"), "/default_precision.rs")); - -#[macro_use] -mod macros; - -// "low level" functions -mod arithmetic; - -// From, To, TryFrom impls -mod impl_convert; -mod impl_trait_from_str; - -// Add, Sub, etc... -mod impl_ops; -mod impl_ops_add; -mod impl_ops_sub; -mod impl_ops_mul; -mod impl_ops_div; -mod impl_ops_rem; - -// PartialEq -mod impl_cmp; - -// Implementations of num_traits -mod impl_num; - -// Implementations for deserializations and serializations -#[cfg(feature = "serde")] -pub mod impl_serde; - -// construct BigDecimals from strings and floats -mod parsing; - -// Routines for rounding -pub mod rounding; -pub use rounding::RoundingMode; - -// Mathematical context -mod context; -pub use context::Context; - -use arithmetic::{ - ten_to_the, - ten_to_the_uint, - count_decimal_digits, - count_decimal_digits_uint, -}; - -/// Internal function used for rounding -/// -/// returns 1 if most significant digit is >= 5, otherwise 0 -/// -/// This is used after dividing a number by a power of ten and -/// rounding the last digit. -/// -#[inline(always)] -fn get_rounding_term(num: &BigInt) -> u8 { - if num.is_zero() { - return 0; - } - - let digits = (num.bits() as f64 / LOG2_10) as u64; - let mut n = ten_to_the(digits); - - // loop-method - loop { - if *num < n { - return 1; - } - n *= 5; - if *num < n { - return 0; - } - n *= 2; - } - - // string-method - // let s = format!("{}", num); - // let high_digit = u8::from_str(&s[0..1]).unwrap(); - // if high_digit < 5 { 0 } else { 1 } -} - -/// A big decimal type. -/// -#[derive(Clone, Eq)] -pub struct BigDecimal { - int_val: BigInt, - // A positive scale means a negative power of 10 - scale: i64, -} - -#[cfg(not(feature = "std"))] -// f64::exp2 is only available in std, we have to use an external crate like libm -fn exp2(x: f64) -> f64 { - libm::exp2(x) -} -#[cfg(feature = "std")] -fn exp2(x: f64) -> f64 { - x.exp2() -} - -impl BigDecimal { - /// Creates and initializes a `BigDecimal`. - /// - #[inline] - pub fn new(digits: BigInt, scale: i64) -> BigDecimal { - BigDecimal { - int_val: digits, - scale: scale, - } - } - - /// Make a BigDecimalRef of this value - pub fn to_ref(&self) -> BigDecimalRef<'_> { - // search for "From<&'a BigDecimal> for BigDecimalRef<'a>" - self.into() - } - - /// Returns the scale of the BigDecimal, the total number of - /// digits to the right of the decimal point (including insignificant - /// leading zeros) - /// - /// # Examples - /// - /// ``` - /// use bigdecimal::BigDecimal; - /// use std::str::FromStr; - /// - /// let a = BigDecimal::from(12345); // No fractional part - /// let b = BigDecimal::from_str("123.45").unwrap(); // Fractional part - /// let c = BigDecimal::from_str("0.0000012345").unwrap(); // Completely fractional part - /// let d = BigDecimal::from_str("5e9").unwrap(); // Negative-fractional part - /// - /// assert_eq!(a.fractional_digit_count(), 0); - /// assert_eq!(b.fractional_digit_count(), 2); - /// assert_eq!(c.fractional_digit_count(), 10); - /// assert_eq!(d.fractional_digit_count(), -9); - /// ``` - #[inline] - pub fn fractional_digit_count(&self) -> i64 { - self.scale - } - - /// Creates and initializes a `BigDecimal`. - /// - /// Decodes using `str::from_utf8` and forwards to `BigDecimal::from_str_radix`. - /// Only base-10 is supported. - /// - /// # Examples - /// - /// ``` - /// use bigdecimal::{BigDecimal, Zero}; - /// - /// assert_eq!(BigDecimal::parse_bytes(b"0", 10).unwrap(), BigDecimal::zero()); - /// assert_eq!(BigDecimal::parse_bytes(b"13", 10).unwrap(), BigDecimal::from(13)); - /// ``` - #[inline] - pub fn parse_bytes(buf: &[u8], radix: u32) -> Option { - stdlib::str::from_utf8(buf) - .ok() - .and_then(|s| BigDecimal::from_str_radix(s, radix).ok()) - } - - /// Return a new BigDecimal object equivalent to self, with internal - /// scaling set to the number specified. - /// If the new_scale is lower than the current value (indicating a larger - /// power of 10), digits will be dropped (as precision is lower) - /// - #[inline] - pub fn with_scale(&self, new_scale: i64) -> BigDecimal { - if self.int_val.is_zero() { - return BigDecimal::new(BigInt::zero(), new_scale); - } - - match new_scale.cmp(&self.scale) { - Ordering::Greater => { - let scale_diff = new_scale - self.scale; - let int_val = &self.int_val * ten_to_the(scale_diff as u64); - BigDecimal::new(int_val, new_scale) - } - Ordering::Less => { - let scale_diff = self.scale - new_scale; - let int_val = &self.int_val / ten_to_the(scale_diff as u64); - BigDecimal::new(int_val, new_scale) - } - Ordering::Equal => self.clone(), - } - } - - /// Return a new BigDecimal after shortening the digits and rounding - /// - /// ``` - /// # use bigdecimal::*; - /// - /// let n: BigDecimal = "129.41675".parse().unwrap(); - /// - /// assert_eq!(n.with_scale_round(2, RoundingMode::Up), "129.42".parse().unwrap()); - /// assert_eq!(n.with_scale_round(-1, RoundingMode::Down), "120".parse().unwrap()); - /// assert_eq!(n.with_scale_round(4, RoundingMode::HalfEven), "129.4168".parse().unwrap()); - /// ``` - pub fn with_scale_round(&self, new_scale: i64, mode: RoundingMode) -> BigDecimal { - use stdlib::cmp::Ordering::*; - - if self.int_val.is_zero() { - return BigDecimal::new(BigInt::zero(), new_scale); - } - - match new_scale.cmp(&self.scale) { - Ordering::Equal => { - self.clone() - } - Ordering::Greater => { - // increase number of zeros - let scale_diff = new_scale - self.scale; - let int_val = &self.int_val * ten_to_the(scale_diff as u64); - BigDecimal::new(int_val, new_scale) - } - Ordering::Less => { - let (sign, mut digits) = self.int_val.to_radix_le(10); - - let digit_count = digits.len(); - let int_digit_count = digit_count as i64 - self.scale; - let rounded_int = match int_digit_count.cmp(&-new_scale) { - Equal => { - let (&last_digit, remaining) = digits.split_last().unwrap(); - let trailing_zeros = remaining.iter().all(Zero::is_zero); - let rounded_digit = mode.round_pair(sign, (0, last_digit), trailing_zeros); - BigInt::new(sign, vec![rounded_digit as u32]) - } - Less => { - debug_assert!(!digits.iter().all(Zero::is_zero)); - let rounded_digit = mode.round_pair(sign, (0, 0), false); - BigInt::new(sign, vec![rounded_digit as u32]) - } - Greater => { - // location of new rounding point - let scale_diff = (self.scale - new_scale) as usize; - - let low_digit = digits[scale_diff - 1]; - let high_digit = digits[scale_diff]; - let trailing_zeros = digits[0..scale_diff-1].iter().all(Zero::is_zero); - let rounded_digit = mode.round_pair(sign, (high_digit, low_digit), trailing_zeros); - - debug_assert!(rounded_digit <= 10); - - if rounded_digit < 10 { - digits[scale_diff] = rounded_digit; - } else { - digits[scale_diff] = 0; - let mut i = scale_diff + 1; - loop { - if i == digit_count { - digits.push(1); - break; - } - - if digits[i] < 9 { - digits[i] += 1; - break; - } - - digits[i] = 0; - i += 1; - } - } - - BigInt::from_radix_le(sign, &digits[scale_diff..], 10).unwrap() - } - }; - - BigDecimal::new(rounded_int, new_scale) - } - } - } - - /// Return a new BigDecimal object with same value and given scale, - /// padding with zeros or truncating digits as needed - /// - /// Useful for aligning decimals before adding/subtracting. - /// - fn take_and_scale(mut self, new_scale: i64) -> BigDecimal { - if self.int_val.is_zero() { - return BigDecimal::new(BigInt::zero(), new_scale); - } - - match new_scale.cmp(&self.scale) { - Ordering::Greater => { - self.int_val *= ten_to_the((new_scale - self.scale) as u64); - BigDecimal::new(self.int_val, new_scale) - } - Ordering::Less => { - self.int_val /= ten_to_the((self.scale - new_scale) as u64); - BigDecimal::new(self.int_val, new_scale) - } - Ordering::Equal => self, - } - } - - /// Take and return bigdecimal with the given sign - /// - /// The Sign value `NoSign` is ignored: only use Plus & Minus - /// - pub(crate) fn take_with_sign(self, sign: Sign) -> BigDecimal { - let BigDecimal { scale, mut int_val } = self; - if int_val.sign() != sign && sign != Sign::NoSign { - int_val = int_val.neg(); - } - BigDecimal { - int_val: int_val, - scale: scale, - } - } +use stdlib::fmt; - /// Return a new BigDecimal object with precision set to new value - /// - /// ``` - /// # use bigdecimal::*; - /// - /// let n: BigDecimal = "129.41675".parse().unwrap(); - /// - /// assert_eq!(n.with_prec(2), "130".parse().unwrap()); - /// - /// let n_p12 = n.with_prec(12); - /// let (i, scale) = n_p12.as_bigint_and_exponent(); - /// assert_eq!(n_p12, "129.416750000".parse().unwrap()); - /// assert_eq!(i, 129416750000_u64.into()); - /// assert_eq!(scale, 9); - /// ``` - pub fn with_prec(&self, prec: u64) -> BigDecimal { - let digits = self.digits(); - - match digits.cmp(&prec) { - Ordering::Greater => { - let diff = digits - prec; - let p = ten_to_the(diff); - let (mut q, r) = self.int_val.div_rem(&p); - - // check for "leading zero" in remainder term; otherwise round - if p < 10 * &r { - q += get_rounding_term(&r); - } - - BigDecimal { - int_val: q, - scale: self.scale - diff as i64, - } - } - Ordering::Less => { - let diff = prec - digits; - BigDecimal { - int_val: &self.int_val * ten_to_the(diff), - scale: self.scale + diff as i64, - } - } - Ordering::Equal => self.clone(), - } - } - - /// Return this BigDecimal with the given precision, rounding if needed - #[cfg(rustc_1_46)] // Option::zip - pub fn with_precision_round(&self, prec: stdlib::num::NonZeroU64, round: RoundingMode) -> BigDecimal { - let digit_count = self.digits(); - let new_prec = prec.get().to_i64(); - let new_scale = new_prec - .zip(digit_count.to_i64()) - .and_then(|(new_prec, old_prec)| new_prec.checked_sub(old_prec)) - .and_then(|prec_diff| self.scale.checked_add(prec_diff)) - .expect("precision overflow"); - - self.with_scale_round(new_scale, round) - } - - #[cfg(not(rustc_1_46))] - pub fn with_precision_round(&self, prec: stdlib::num::NonZeroU64, round: RoundingMode) -> BigDecimal { - let new_scale = self.digits().to_i64().and_then( - |old_prec| { - prec.get().to_i64().and_then( - |new_prec| { new_prec.checked_sub(old_prec) })}) - .and_then(|prec_diff| self.scale.checked_add(prec_diff)) - .expect("precision overflow"); - - self.with_scale_round(new_scale, round) - } - - /// Return the sign of the `BigDecimal` as `num::bigint::Sign`. - /// - /// ``` - /// # use bigdecimal::{BigDecimal, num_bigint::Sign}; - /// - /// fn sign_of(src: &str) -> Sign { - /// let n: BigDecimal = src.parse().unwrap(); - /// n.sign() - /// } - /// - /// assert_eq!(sign_of("-1"), Sign::Minus); - /// assert_eq!(sign_of("0"), Sign::NoSign); - /// assert_eq!(sign_of("1"), Sign::Plus); - /// ``` - #[inline] - pub fn sign(&self) -> num_bigint::Sign { - self.int_val.sign() - } - - /// Return the internal big integer value and an exponent. Note that a positive - /// exponent indicates a negative power of 10. - /// - /// # Examples - /// - /// ``` - /// use bigdecimal::{BigDecimal, num_bigint::BigInt}; - /// - /// let n: BigDecimal = "1.23456".parse().unwrap(); - /// let expected = ("123456".parse::().unwrap(), 5); - /// assert_eq!(n.as_bigint_and_exponent(), expected); - /// ``` - #[inline] - pub fn as_bigint_and_exponent(&self) -> (BigInt, i64) { - (self.int_val.clone(), self.scale) - } - - /// Convert into the internal big integer value and an exponent. Note that a positive - /// exponent indicates a negative power of 10. - /// - /// # Examples - /// - /// ``` - /// use bigdecimal::{BigDecimal, num_bigint::BigInt}; - /// - /// let n: BigDecimal = "1.23456".parse().unwrap(); - /// let expected = ("123456".parse::().unwrap(), 5); - /// assert_eq!(n.into_bigint_and_exponent(), expected); - /// ``` - #[inline] - pub fn into_bigint_and_exponent(self) -> (BigInt, i64) { - (self.int_val, self.scale) - } - - /// Number of digits in the non-scaled integer representation - /// - #[inline] - pub fn digits(&self) -> u64 { - count_decimal_digits(&self.int_val) - } - - /// Compute the absolute value of number - /// - /// ``` - /// # use bigdecimal::BigDecimal; - /// let n: BigDecimal = "123.45".parse().unwrap(); - /// assert_eq!(n.abs(), "123.45".parse().unwrap()); - /// - /// let n: BigDecimal = "-123.45".parse().unwrap(); - /// assert_eq!(n.abs(), "123.45".parse().unwrap()); - /// ``` - #[inline] - pub fn abs(&self) -> BigDecimal { - BigDecimal { - int_val: self.int_val.abs(), - scale: self.scale, - } - } - - /// Multiply decimal by 2 (efficiently) - /// - /// ``` - /// # use bigdecimal::BigDecimal; - /// let n: BigDecimal = "123.45".parse().unwrap(); - /// assert_eq!(n.double(), "246.90".parse().unwrap()); - /// ``` - pub fn double(&self) -> BigDecimal { - if self.is_zero() { - self.clone() - } else { - BigDecimal { - int_val: self.int_val.clone() * 2, - scale: self.scale, - } - } - } - - /// Divide decimal by 2 (efficiently) - /// - /// *Note*: If the last digit in the decimal is odd, the precision - /// will increase by 1 - /// - /// ``` - /// # use bigdecimal::BigDecimal; - /// let n: BigDecimal = "123.45".parse().unwrap(); - /// assert_eq!(n.half(), "61.725".parse().unwrap()); - /// ``` - #[inline] - pub fn half(&self) -> BigDecimal { - if self.is_zero() { - self.clone() - } else if self.int_val.is_even() { - BigDecimal { - int_val: self.int_val.clone().div(2u8), - scale: self.scale, - } - } else { - BigDecimal { - int_val: self.int_val.clone().mul(5u8), - scale: self.scale + 1, - } - } - } - - /// Square a decimal: *x²* - /// - /// No rounding or truncating of digits; this is the full result - /// of the squaring operation. - /// - /// *Note*: doubles the scale of bigdecimal, which might lead to - /// accidental exponential-complexity if used in a loop. - /// - /// ``` - /// # use bigdecimal::BigDecimal; - /// let n: BigDecimal = "1.1156024145937225657484".parse().unwrap(); - /// assert_eq!(n.square(), "1.24456874744734405154288399835406316085210256".parse().unwrap()); - /// - /// let n: BigDecimal = "-9.238597585E+84".parse().unwrap(); - /// assert_eq!(n.square(), "8.5351685337567832225E+169".parse().unwrap()); - /// ``` - pub fn square(&self) -> BigDecimal { - if self.is_zero() || self.is_one() { - self.clone() - } else { - BigDecimal { - int_val: self.int_val.clone() * &self.int_val, - scale: self.scale * 2, - } - } - } - - /// Cube a decimal: *x³* - /// - /// No rounding or truncating of digits; this is the full result - /// of the cubing operation. - /// - /// *Note*: triples the scale of bigdecimal, which might lead to - /// accidental exponential-complexity if used in a loop. - /// - /// ``` - /// # use bigdecimal::BigDecimal; - /// let n: BigDecimal = "1.1156024145937225657484".parse().unwrap(); - /// assert_eq!(n.cube(), "1.388443899780141911774491376394890472130000455312878627147979955904".parse().unwrap()); - /// - /// let n: BigDecimal = "-9.238597585E+84".parse().unwrap(); - /// assert_eq!(n.cube(), "-7.88529874035334084567570176625E+254".parse().unwrap()); - /// ``` - pub fn cube(&self) -> BigDecimal { - if self.is_zero() || self.is_one() { - self.clone() - } else { - BigDecimal { - int_val: self.int_val.clone() * &self.int_val * &self.int_val, - scale: self.scale * 3, - } - } - } - - /// Take the square root of the number - /// - /// Uses default-precision, set from build time environment variable - //// `RUST_BIGDECIMAL_DEFAULT_PRECISION` (defaults to 100) - /// - /// If the value is < 0, None is returned - /// - /// ``` - /// # use bigdecimal::BigDecimal; - /// let n: BigDecimal = "1.1156024145937225657484".parse().unwrap(); - /// assert_eq!(n.sqrt().unwrap(), "1.056220817156016181190291268045893004363809142172289919023269377496528394924695970851558013658193913".parse().unwrap()); - /// - /// let n: BigDecimal = "-9.238597585E+84".parse().unwrap(); - /// assert_eq!(n.sqrt(), None); - /// ``` - #[inline] - pub fn sqrt(&self) -> Option { - self.sqrt_with_context(&Context::default()) - } - - /// Take the square root of the number, using context for precision and rounding - /// - pub fn sqrt_with_context(&self, ctx: &Context) -> Option { - if self.is_zero() || self.is_one() { - return Some(self.clone()); - } - if self.is_negative() { - return None; - } - - let uint = self.int_val.magnitude(); - let result = arithmetic::sqrt::impl_sqrt(uint, self.scale, ctx); - - Some(result) - } - - /// Take the cube root of the number, using default context - /// - #[inline] - pub fn cbrt(&self) -> BigDecimal { - self.cbrt_with_context(&Context::default()) - } - - /// Take cube root of self, using properties of context - pub fn cbrt_with_context(&self, ctx: &Context) -> BigDecimal { - if self.is_zero() || self.is_one() { - return self.clone(); - } - - let uint = self.int_val.magnitude(); - let result = arithmetic::cbrt::impl_cbrt_uint_scale(uint, self.scale, ctx); - - // always copy sign - result.take_with_sign(self.sign()) - } - - /// Compute the reciprical of the number: x-1 - #[inline] - pub fn inverse(&self) -> BigDecimal { - self.inverse_with_context(&Context::default()) - } - - /// Return inverse of self, rounding with ctx - pub fn inverse_with_context(&self, ctx: &Context) -> BigDecimal { - if self.is_zero() || self.is_one() { - return self.clone(); - } - - let uint = self.int_val.magnitude(); - let result = arithmetic::inverse::impl_inverse_uint_scale(uint, self.scale, ctx); - - // always copy sign - result.take_with_sign(self.sign()) - } - - /// Return number rounded to round_digits precision after the decimal point - pub fn round(&self, round_digits: i64) -> BigDecimal { - // we have fewer digits than we need, no rounding - if round_digits >= self.scale { - return self.with_scale(round_digits); - } - - let (sign, double_digits) = self.int_val.to_radix_le(100); - - let last_is_double_digit = *double_digits.last().unwrap() >= 10; - let digit_count = (double_digits.len() - 1) * 2 + 1 + last_is_double_digit as usize; - - // relevant digit positions: each "pos" is position of 10^{pos} - let least_significant_pos = -self.scale; - let most_sig_digit_pos = digit_count as i64 + least_significant_pos - 1; - let rounding_pos = -round_digits; - - // digits are too small, round to zero - if rounding_pos > most_sig_digit_pos + 1 { - return BigDecimal::zero(); - } - - // highest digit is next to rounding point - if rounding_pos == most_sig_digit_pos + 1 { - let (&last_double_digit, remaining) = double_digits.split_last().unwrap(); - - let mut trailing_zeros = remaining.iter().all(|&d| d == 0); - - let last_digit = if last_is_double_digit { - let (high, low) = num_integer::div_rem(last_double_digit, 10); - trailing_zeros &= low == 0; - high - } else { - last_double_digit - }; - - if last_digit > 5 || (last_digit == 5 && !trailing_zeros) { - return BigDecimal::new(BigInt::one(), round_digits); - } - - return BigDecimal::zero(); - } - - let double_digits_to_remove = self.scale - round_digits; - debug_assert!(double_digits_to_remove > 0); - - let (rounding_idx, rounding_offset) = num_integer::div_rem(double_digits_to_remove as usize, 2); - debug_assert!(rounding_idx <= double_digits.len()); - - let (low_digits, high_digits) = double_digits.as_slice().split_at(rounding_idx); - debug_assert!(high_digits.len() > 0); - - let mut unrounded_uint = num_bigint::BigUint::from_radix_le(high_digits, 100).unwrap(); - - let rounded_uint; - if rounding_offset == 0 { - let high_digit = high_digits[0] % 10; - let (&top, rest) = low_digits.split_last().unwrap_or((&0u8, &[])); - let (low_digit, lower_digit) = num_integer::div_rem(top, 10); - let trailing_zeros = lower_digit == 0 && rest.iter().all(|&d| d == 0); - - let rounding = if low_digit < 5 { - 0 - } else if low_digit > 5 || !trailing_zeros { - 1 - } else { - high_digit % 2 - }; - - rounded_uint = unrounded_uint + rounding; - } else { - let (high_digit, low_digit) = num_integer::div_rem(high_digits[0], 10); - - let trailing_zeros = low_digits.iter().all(|&d| d == 0); - - let rounding = if low_digit < 5 { - 0 - } else if low_digit > 5 || !trailing_zeros { - 1 - } else { - high_digit % 2 - }; - - // shift unrounded_uint down, - unrounded_uint /= num_bigint::BigUint::from_u8(10).unwrap(); - rounded_uint = unrounded_uint + rounding; - } - - let rounded_int = num_bigint::BigInt::from_biguint(sign, rounded_uint); - BigDecimal::new(rounded_int, round_digits) - } - - /// Return true if this number has zero fractional part (is equal - /// to an integer) - /// - #[inline] - pub fn is_integer(&self) -> bool { - if self.scale <= 0 { - true - } else { - (self.int_val.clone() % ten_to_the(self.scale as u64)).is_zero() - } - } - - /// Evaluate the natural-exponential function ex - /// - #[inline] - pub fn exp(&self) -> BigDecimal { - if self.is_zero() { - return BigDecimal::one(); - } - - let target_precision = DEFAULT_PRECISION; - - let precision = self.digits(); - - let mut term = self.clone(); - let mut result = self.clone() + BigDecimal::one(); - let mut prev_result = result.clone(); - let mut factorial = BigInt::one(); - - for n in 2.. { - term *= self; - factorial *= n; - // ∑ term=x^n/n! - result += impl_division(term.int_val.clone(), &factorial, term.scale, 117 + precision); - - let trimmed_result = result.with_prec(target_precision + 5); - if prev_result == trimmed_result { - return trimmed_result.with_prec(target_precision); - } - prev_result = trimmed_result; - } - unreachable!("Loop did not converge") - } - - #[must_use] - pub fn normalized(&self) -> BigDecimal { - if self == &BigDecimal::zero() { - return BigDecimal::zero(); - } - let (sign, mut digits) = self.int_val.to_radix_be(10); - let trailing_count = digits.iter().rev().take_while(|i| **i == 0).count(); - let trunc_to = digits.len() - trailing_count; - digits.truncate(trunc_to); - let int_val = BigInt::from_radix_be(sign, &digits, 10).unwrap(); - let scale = self.scale - trailing_count as i64; - BigDecimal::new(int_val, scale) - } -} - -#[derive(Debug, PartialEq)] -pub enum ParseBigDecimalError { - ParseDecimal(ParseFloatError), - ParseInt(ParseIntError), - ParseBigInt(ParseBigIntError), - Empty, - Other(String), -} - -impl fmt::Display for ParseBigDecimalError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use ParseBigDecimalError::*; - - match *self { - ParseDecimal(ref e) => e.fmt(f), - ParseInt(ref e) => e.fmt(f), - ParseBigInt(ref e) => e.fmt(f), - Empty => "Failed to parse empty string".fmt(f), - Other(ref reason) => reason[..].fmt(f), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for ParseBigDecimalError { - fn description(&self) -> &str { - "failed to parse bigint/biguint" - } -} - -impl From for ParseBigDecimalError { - fn from(err: ParseFloatError) -> ParseBigDecimalError { - ParseBigDecimalError::ParseDecimal(err) - } -} - -impl From for ParseBigDecimalError { - fn from(err: ParseIntError) -> ParseBigDecimalError { - ParseBigDecimalError::ParseInt(err) - } -} - -impl From for ParseBigDecimalError { - fn from(err: ParseBigIntError) -> ParseBigDecimalError { - ParseBigDecimalError::ParseBigInt(err) - } -} - -#[allow(deprecated)] // trim_right_match -> trim_end_match -impl Hash for BigDecimal { - fn hash(&self, state: &mut H) { - let mut dec_str = self.int_val.to_str_radix(10); - let scale = self.scale; - let zero = self.int_val.is_zero(); - if scale > 0 && !zero { - let mut cnt = 0; - dec_str = dec_str - .trim_right_matches(|x| { - cnt += 1; - x == '0' && cnt <= scale - }) - .to_string(); - } else if scale < 0 && !zero { - dec_str.push_str(&"0".repeat(self.scale.abs() as usize)); - } - dec_str.hash(state); - } -} - -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] - fn default() -> BigDecimal { - Zero::zero() - } -} - -impl Zero for BigDecimal { - #[inline] - fn zero() -> BigDecimal { - BigDecimal::new(BigInt::zero(), 0) - } - - #[inline] - fn is_zero(&self) -> bool { - self.int_val.is_zero() - } -} - -impl One for BigDecimal { - #[inline] - fn one() -> BigDecimal { - BigDecimal::new(BigInt::one(), 0) - } -} - - -fn impl_division(mut num: BigInt, den: &BigInt, mut scale: i64, max_precision: u64) -> BigDecimal { - // quick zero check - if num.is_zero() { - return BigDecimal::new(num, 0); - } - - match (num.is_negative(), den.is_negative()) { - (true, true) => return impl_division(num.neg(), &den.neg(), scale, max_precision), - (true, false) => return -impl_division(num.neg(), den, scale, max_precision), - (false, true) => return -impl_division(num, &den.neg(), scale, max_precision), - (false, false) => (), - } - - // shift digits until numerator is larger than denominator (set scale appropriately) - while num < *den { - scale += 1; - num *= 10; - } - - // first division - let (mut quotient, mut remainder) = num.div_rem(den); - - // division complete - if remainder.is_zero() { - return BigDecimal { - int_val: quotient, - scale: scale, - }; - } - - let mut precision = count_decimal_digits("ient); - - // shift remainder by 1 decimal; - // quotient will be 1 digit upon next division - remainder *= 10; - - while !remainder.is_zero() && precision < max_precision { - let (q, r) = remainder.div_rem(den); - quotient = quotient * 10 + q; - remainder = r * 10; - - precision += 1; - scale += 1; - } - - if !remainder.is_zero() { - // round final number with remainder - quotient += get_rounding_term(&remainder.div(den)); - } - - let result = BigDecimal::new(quotient, scale); - // println!(" {} / {}\n = {}\n", self, other, result); - return result; -} - - - -impl Signed for BigDecimal { - #[inline] - fn abs(&self) -> BigDecimal { - match self.sign() { - Sign::Plus | Sign::NoSign => self.clone(), - Sign::Minus => -self, - } - } - - #[inline] - fn abs_sub(&self, other: &BigDecimal) -> BigDecimal { - if *self <= *other { - Zero::zero() - } else { - self - other - } - } - - #[inline] - fn signum(&self) -> BigDecimal { - match self.sign() { - Sign::Plus => One::one(), - Sign::NoSign => Zero::zero(), - Sign::Minus => -Self::one(), - } - } - - #[inline] - fn is_positive(&self) -> bool { - self.sign() == Sign::Plus - } - - #[inline] - fn is_negative(&self) -> bool { - self.sign() == Sign::Minus - } -} - -impl Sum for BigDecimal { - #[inline] - fn sum>(iter: I) -> BigDecimal { - iter.fold(Zero::zero(), |a, b| a + b) - } -} - -impl<'a> Sum<&'a BigDecimal> for BigDecimal { - #[inline] - fn sum>(iter: I) -> BigDecimal { - iter.fold(Zero::zero(), |a, b| a + b) - } -} impl fmt::Display for BigDecimal { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -1158,842 +27,50 @@ impl fmt::Display for BigDecimal { let abs_int = abs_int + "0".repeat(zeros).as_str(); (abs_int, "".to_string()) } else { - // Case 2.2, somewhere around the decimal point - // Just split it in two - let after = abs_int.split_off(location as usize); - (abs_int, after) - } - }; - - // Alter precision after the decimal point - let after = if let Some(precision) = f.precision() { - let len = after.len(); - if len < precision { - after + "0".repeat(precision - len).as_str() - } else { - // TODO: Should we round? - after[0..precision].to_string() - } - } else { - after - }; - - // Concatenate everything - let complete_without_sign = if !after.is_empty() { - before + "." + after.as_str() - } else { - before - }; - - let non_negative = matches!(self.int_val.sign(), Sign::Plus | Sign::NoSign); - //pad_integral does the right thing although we have a decimal - f.pad_integral(non_negative, "", &complete_without_sign) - } -} - -impl fmt::Debug for BigDecimal { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "BigDecimal(\"{}\")", self) - } -} - - -/// Immutable big-decimal, referencing a borrowed buffer of digits -/// -/// The non-digit information like `scale` and `sign` may be changed -/// on these objects, which otherwise would require cloning the full -/// digit buffer in the BigDecimal. -/// -/// Built from full `BigDecimal` object using the `to_ref()` method. -/// `BigDecimal` not implement `AsRef`, so we will reserve the method -/// `as_ref()` for a later time. -/// -/// May be transformed into full BigDecimal object using the `to_owned()` -/// method. -/// This clones the bigdecimal digits. -/// -/// BigDecimalRef (or `Into`) should be preferred over -/// using `&BigDecimal` for library functions that need an immutable -/// reference to a bigdecimal, as it may be much more efficient. -/// -/// NOTE: Using `&BigDecimalRef` is redundant, and not recommended. -/// -/// ## Examples -/// -/// ``` -/// # use bigdecimal::*; use std::ops::Neg; -/// fn add_one<'a, N: Into>>(n: N) -> BigDecimal { -/// n.into() + 1 -/// } -/// -/// let n: BigDecimal = "123.456".parse().unwrap(); -/// -/// // call via "standard" reference (implements Into) -/// let m = add_one(&n); -/// assert_eq!(m, "124.456".parse().unwrap()); -/// -/// // call by negating the reference (fast: no-digit cloning involved) -/// let m = add_one(n.to_ref().neg()); -/// assert_eq!(m, "-122.456".parse().unwrap()); -/// ``` -/// -#[derive(Clone, Copy, Debug)] -pub struct BigDecimalRef<'a> { - sign: Sign, - digits: &'a BigUint, - scale: i64, -} - -impl BigDecimalRef<'_> { - /// Clone digits to make this reference a full BigDecimal object - pub fn to_owned(&self) -> BigDecimal { - BigDecimal { - scale: self.scale, - int_val: BigInt::from_biguint(self.sign, self.digits.clone()), - } - } - - /// Clone digits, returning BigDecimal with given scale - /// - /// ``` - /// # use bigdecimal::*; - /// - /// let n: BigDecimal = "123.45678".parse().unwrap(); - /// let r = n.to_ref(); - /// assert_eq!(r.to_owned_with_scale(5), n.clone()); - /// assert_eq!(r.to_owned_with_scale(0), "123".parse().unwrap()); - /// assert_eq!(r.to_owned_with_scale(-1), "12e1".parse().unwrap()); - /// - /// let x = r.to_owned_with_scale(8); - /// assert_eq!(&x, &n); - /// assert_eq!(x.fractional_digit_count(), 8); - /// ``` - pub fn to_owned_with_scale(&self, scale: i64) -> BigDecimal { - use stdlib::cmp::Ordering::*; - - let digits = match scale.cmp(&self.scale) { - Equal => self.digits.clone(), - Greater => self.digits * ten_to_the_uint((scale - self.scale) as u64), - Less => self.digits / ten_to_the_uint((self.scale - scale) as u64) - }; - - BigDecimal { - scale: scale, - int_val: BigInt::from_biguint(self.sign, digits), - } - } - - /// Sign of decimal - pub fn sign(&self) -> Sign { - self.sign - } - - /// Return number of digits 'right' of the decimal point - /// (including leading zeros) - pub fn fractional_digit_count(&self) -> i64 { - self.scale - } - - /// Count total number of decimal digits - pub fn count_digits(&self) -> u64 { - count_decimal_digits_uint(self.digits) - } - - /// Split into components - pub(crate) fn as_parts(&self) -> (Sign, i64, &BigUint) { - (self.sign, self.scale, self.digits) - } - - /// Take absolute value of the decimal (non-negative sign) - pub fn abs(&self) -> Self { - Self { - sign: self.sign * self.sign, - digits: self.digits, - scale: self.scale, - } - } - - /// Take square root of this number - pub fn sqrt_with_context(&self, ctx: &Context) -> Option { - use Sign::*; + // Case 2.2, somewhere around the decimal point + // Just split it in two + let after = abs_int.split_off(location as usize); + (abs_int, after) + } + }; - let (sign, scale, uint) = self.as_parts(); + // Alter precision after the decimal point + let after = if let Some(precision) = f.precision() { + let len = after.len(); + if len < precision { + after + "0".repeat(precision - len).as_str() + } else { + // TODO: Should we round? + after[0..precision].to_string() + } + } else { + after + }; - match sign { - Minus => None, - NoSign => Some(Zero::zero()), - Plus => Some(arithmetic::sqrt::impl_sqrt(uint, scale, ctx)), - } - } + // Concatenate everything + let complete_without_sign = if !after.is_empty() { + before + "." + after.as_str() + } else { + before + }; - /// Return if the referenced decimal is zero - pub fn is_zero(&self) -> bool { - self.digits.is_zero() + let non_negative = matches!(self.int_val.sign(), Sign::Plus | Sign::NoSign); + //pad_integral does the right thing although we have a decimal + f.pad_integral(non_negative, "", &complete_without_sign) } } -impl<'a> From<&'a BigDecimal> for BigDecimalRef<'a> { - fn from(n: &'a BigDecimal) -> Self { - let sign = n.int_val.sign(); - let mag = n.int_val.magnitude(); - Self { - sign: sign, - digits: mag, - scale: n.scale, - } +impl fmt::Debug for BigDecimal { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "BigDecimal(\"{}\")", self) } } -impl<'a> From<&'a BigInt> for BigDecimalRef<'a> { - fn from(n: &'a BigInt) -> Self { - Self { - sign: n.sign(), - digits: n.magnitude(), - scale: 0, - } - } -} #[rustfmt::skip] #[cfg(test)] #[allow(non_snake_case)] mod bigdecimal_tests { - use crate::{stdlib, BigDecimal, ToString, FromStr, TryFrom}; - use num_traits::{ToPrimitive, FromPrimitive, Signed, Zero, One}; - use num_bigint; - use paste::paste; - - #[test] - fn test_fractional_digit_count() { - // Zero value - let vals = BigDecimal::from(0); - assert_eq!(vals.fractional_digit_count(), 0); - assert_eq!(vals.to_ref().fractional_digit_count(), 0); - - // Fractional part with trailing zeros - let vals = BigDecimal::from_str("1.0").unwrap(); - assert_eq!(vals.fractional_digit_count(), 1); - assert_eq!(vals.to_ref().fractional_digit_count(), 1); - - // Fractional part - let vals = BigDecimal::from_str("1.23").unwrap(); - assert_eq!(vals.fractional_digit_count(), 2); - assert_eq!(vals.to_ref().fractional_digit_count(), 2); - - // shifted to 'left' has negative scale - let vals = BigDecimal::from_str("123e5").unwrap(); - assert_eq!(vals.fractional_digit_count(), -5); - assert_eq!(vals.to_ref().fractional_digit_count(), -5); - } - - #[test] - fn test_sum() { - let vals = vec![ - BigDecimal::from_f32(2.5).unwrap(), - BigDecimal::from_f32(0.3).unwrap(), - BigDecimal::from_f32(0.001).unwrap(), - ]; - - let expected_sum = BigDecimal::from_str("2.801000011968426406383514404296875").unwrap(); - let sum = vals.iter().sum::(); - - assert_eq!(expected_sum, sum); - } - - #[test] - fn test_sum1() { - let vals = vec![ - BigDecimal::from_f32(0.1).unwrap(), - BigDecimal::from_f32(0.2).unwrap(), - ]; - - let expected_sum = BigDecimal::from_str("0.300000004470348358154296875").unwrap(); - let sum = vals.iter().sum::(); - - assert_eq!(expected_sum, sum); - } - - #[test] - fn test_to_i64() { - let vals = vec![ - ("12.34", 12), - ("3.14", 3), - ("50", 50), - ("50000", 50000), - ("0.001", 0), - // TODO: Is the desired behaviour to round? - //("0.56", 1), - ]; - for (s, ans) in vals { - let calculated = BigDecimal::from_str(s).unwrap().to_i64().unwrap(); - - assert_eq!(ans, calculated); - } - } - - #[test] - fn test_to_i128() { - let vals = vec![ - ("170141183460469231731687303715884105727", 170141183460469231731687303715884105727), - ("-170141183460469231731687303715884105728", -170141183460469231731687303715884105728), - ("12.34", 12), - ("3.14", 3), - ("50", 50), - ("0.001", 0), - ]; - for (s, ans) in vals { - let calculated = BigDecimal::from_str(s).unwrap().to_i128().unwrap(); - - assert_eq!(ans, calculated); - } - } - - #[test] - fn test_to_u128() { - let vals = vec![ - ("340282366920938463463374607431768211455", 340282366920938463463374607431768211455), - ("12.34", 12), - ("3.14", 3), - ("50", 50), - ("0.001", 0), - ]; - for (s, ans) in vals { - let calculated = BigDecimal::from_str(s).unwrap().to_u128().unwrap(); - - assert_eq!(ans, calculated); - } - } - - #[test] - fn test_to_f64() { - let vals = vec![ - ("12.34", 12.34), - ("3.14", 3.14), - ("50", 50.), - ("50000", 50000.), - ("0.001", 0.001), - ]; - for (s, ans) in vals { - let diff = BigDecimal::from_str(s).unwrap().to_f64().unwrap() - ans; - let diff = diff.abs(); - - assert!(diff < 1e-10); - } - } - - #[test] - fn test_from_i8() { - let vals = vec![ - ("0", 0), - ("1", 1), - ("12", 12), - ("-13", -13), - ("111", 111), - ("-128", i8::MIN), - ("127", i8::MAX), - ]; - for (s, n) in vals { - let expected = BigDecimal::from_str(s).unwrap(); - let value = BigDecimal::from_i8(n).unwrap(); - assert_eq!(expected, value); - } - } - - #[test] - fn test_from_f32() { - let vals = vec![ - ("0.0", 0.0), - ("1.0", 1.0), - ("0.5", 0.5), - ("0.25", 0.25), - ("50.", 50.0), - ("50000", 50000.), - ("0.001000000047497451305389404296875", 0.001), - ("12.340000152587890625", 12.34), - ("0.15625", 0.15625), - ("3.1415927410125732421875", stdlib::f32::consts::PI), - ("31415.927734375", stdlib::f32::consts::PI * 10000.0), - ("94247.78125", stdlib::f32::consts::PI * 30000.0), - ("1048576", 1048576.), - ]; - for (s, n) in vals { - let expected = BigDecimal::from_str(s).unwrap(); - let value = BigDecimal::from_f32(n).unwrap(); - assert_eq!(expected, value); - } - } - - #[test] - fn test_from_f64() { - let vals = vec![ - ("1.0", 1.0f64), - ("0.5", 0.5), - ("50", 50.), - ("50000", 50000.), - ("0.001000000000000000020816681711721685132943093776702880859375", 0.001), - ("0.25", 0.25), - ("12.339999999999999857891452847979962825775146484375", 12.34), - ("0.15625", 5.0 * 0.03125), - ("0.333333333333333314829616256247390992939472198486328125", 1.0 / 3.0), - ("3.141592653589793115997963468544185161590576171875", stdlib::f64::consts::PI), - ("31415.926535897931898944079875946044921875", stdlib::f64::consts::PI * 10000.0f64), - ("94247.779607693795696832239627838134765625", stdlib::f64::consts::PI * 30000.0f64), - ]; - for (s, n) in vals { - let expected = BigDecimal::from_str(s).unwrap(); - let value = BigDecimal::from_f64(n).unwrap(); - assert_eq!(expected, value); - // assert_eq!(expected, n); - } - } - - #[test] - fn test_nan_float() { - assert!(BigDecimal::try_from(f32::NAN).is_err()); - assert!(BigDecimal::try_from(f64::NAN).is_err()); - } - - #[test] - fn test_equal() { - let vals = vec![ - ("2", ".2e1"), - ("0e1", "0.0"), - ("0e0", "0.0"), - ("0e-0", "0.0"), - ("-0901300e-3", "-901.3"), - ("-0.901300e+3", "-901.3"), - ("-0e-1", "-0.0"), - ("2123121e1231", "212.3121e1235"), - ]; - for &(x, y) in vals.iter() { - let a = BigDecimal::from_str(x).unwrap(); - let b = BigDecimal::from_str(y).unwrap(); - assert_eq!(a, b); - } - } - - #[test] - fn test_not_equal() { - let vals = vec![ - ("2", ".2e2"), - ("1e45", "1e-900"), - ("1e+900", "1e-900"), - ]; - for &(x, y) in vals.iter() { - let a = BigDecimal::from_str(x).unwrap(); - let b = BigDecimal::from_str(y).unwrap(); - assert!(a != b, "{} == {}", a, b); - } - } - - #[test] - fn test_hash_equal() { - use stdlib::DefaultHasher; - use stdlib::hash::{Hash, Hasher}; - - fn hash(obj: &T) -> u64 - where T: Hash - { - let mut hasher = DefaultHasher::new(); - obj.hash(&mut hasher); - hasher.finish() - } - - let vals = vec![ - ("1.1234", "1.1234000"), - ("1.12340000", "1.1234"), - ("001.1234", "1.1234000"), - ("001.1234", "0001.1234"), - ("1.1234000000", "1.1234000"), - ("1.12340", "1.1234000000"), - ("-0901300e-3", "-901.3"), - ("-0.901300e+3", "-901.3"), - ("100", "100.00"), - ("100.00", "100"), - ("0.00", "0"), - ("0.00", "0.000"), - ("-0.00", "0.000"), - ("0.00", "-0.000"), - ]; - for &(x,y) in vals.iter() { - let a = BigDecimal::from_str(x).unwrap(); - let b = BigDecimal::from_str(y).unwrap(); - assert_eq!(a, b); - assert_eq!(hash(&a), hash(&b), "hash({}) != hash({})", a, b); - } - } - - #[test] - fn test_hash_not_equal() { - use stdlib::DefaultHasher; - use stdlib::hash::{Hash, Hasher}; - - fn hash(obj: &T) -> u64 - where T: Hash - { - let mut hasher = DefaultHasher::new(); - obj.hash(&mut hasher); - hasher.finish() - } - - let vals = vec![ - ("1.1234", "1.1234001"), - ("10000", "10"), - ("10", "10000"), - ("10.0", "100"), - ]; - for &(x,y) in vals.iter() { - let a = BigDecimal::from_str(x).unwrap(); - let b = BigDecimal::from_str(y).unwrap(); - assert!(a != b, "{} == {}", a, b); - assert!(hash(&a) != hash(&b), "hash({}) == hash({})", a, b); - } - } - - #[test] - fn test_hash_equal_scale() { - use stdlib::DefaultHasher; - use stdlib::hash::{Hash, Hasher}; - - fn hash(obj: &T) -> u64 - where T: Hash - { - let mut hasher = DefaultHasher::new(); - obj.hash(&mut hasher); - hasher.finish() - } - - let vals = vec![ - ("1234.5678", -2, "1200", 0), - ("1234.5678", -2, "1200", -2), - ("1234.5678", 0, "1234.1234", 0), - ("1234.5678", -3, "1200", -3), - ("-1234", -2, "-1200", 0), - ]; - for &(x,xs,y,ys) in vals.iter() { - let a = BigDecimal::from_str(x).unwrap().with_scale(xs); - let b = BigDecimal::from_str(y).unwrap().with_scale(ys); - assert_eq!(a, b); - assert_eq!(hash(&a), hash(&b), "hash({}) != hash({})", a, b); - } - } - - #[test] - fn test_with_prec() { - let vals = vec![ - ("7", 1, "7"), - ("7", 2, "7.0"), - ("895", 2, "900"), - ("8934", 2, "8900"), - ("8934", 1, "9000"), - ("1.0001", 5, "1.0001"), - ("1.0001", 4, "1"), - ("1.00009", 6, "1.00009"), - ("1.00009", 5, "1.0001"), - ("1.00009", 4, "1.000"), - ]; - for &(x, p, y) in vals.iter() { - let a = BigDecimal::from_str(x).unwrap().with_prec(p); - assert_eq!(a, BigDecimal::from_str(y).unwrap()); - } - } - - - #[test] - fn test_digits() { - let vals = vec![ - ("0", 1), - ("7", 1), - ("10", 2), - ("8934", 4), - ]; - for &(x, y) in vals.iter() { - let a = BigDecimal::from_str(x).unwrap(); - assert_eq!(a.digits(), y); - } - } - - #[test] - fn test_get_rounding_term() { - use num_bigint::BigInt; - use super::get_rounding_term; - let vals = vec![ - ("0", 0), - ("4", 0), - ("5", 1), - ("10", 0), - ("15", 0), - ("49", 0), - ("50", 1), - ("51", 1), - ("8934", 1), - ("9999", 1), - ("10000", 0), - ("50000", 1), - ("99999", 1), - ("100000", 0), - ("100001", 0), - ("10000000000", 0), - ("9999999999999999999999999999999999999999", 1), - ("10000000000000000000000000000000000000000", 0), - ]; - for &(x, y) in vals.iter() { - let a = BigInt::from_str(x).unwrap(); - assert_eq!(get_rounding_term(&a), y, "{}", x); - } - } - - #[test] - fn test_abs() { - let vals = vec![ - ("10", "10"), - ("-10", "10"), - ]; - for &(x, y) in vals.iter() { - let a = BigDecimal::from_str(x).unwrap().abs(); - let b = BigDecimal::from_str(y).unwrap(); - assert!(a == b, "{} == {}", a, b); - } - } - - #[test] - fn test_count_decimal_digits() { - use num_bigint::BigInt; - use super::count_decimal_digits; - let vals = vec![ - ("10", 2), - ("1", 1), - ("9", 1), - ("999", 3), - ("1000", 4), - ("9900", 4), - ("9999", 4), - ("10000", 5), - ("99999", 5), - ("100000", 6), - ("999999", 6), - ("1000000", 7), - ("9999999", 7), - ("999999999999", 12), - ("999999999999999999999999", 24), - ("999999999999999999999999999999999999999999999999", 48), - ("999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999", 96), - ("199999911199999999999999999999999999999999999999999999999999999999999999999999999999999999999000", 96), - ("999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999991", 192), - ("199999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999", 192), - ("-1", 1), - ("-6", 1), - ("-10", 2), - ("-999999999999999999999999", 24), - ]; - for &(x, y) in vals.iter() { - let a = BigInt::from_str(x).unwrap(); - let b = count_decimal_digits(&a); - assert_eq!(b, y); - } - } - - #[test] - fn test_half() { - let vals = vec![ - ("100", "50."), - ("2", "1"), - (".2", ".1"), - ("42", "21"), - ("3", "1.5"), - ("99", "49.5"), - ("3.141592653", "1.5707963265"), - ("3.1415926536", "1.5707963268"), - ]; - for &(x, y) in vals.iter() { - let a = BigDecimal::from_str(x).unwrap().half(); - let b = BigDecimal::from_str(y).unwrap(); - assert_eq!(a, b); - assert_eq!(a.scale, b.scale); - } - } - - #[test] - fn test_round() { - let test_cases = vec![ - ("1.45", 1, "1.4"), - ("1.444445", 1, "1.4"), - ("1.44", 1, "1.4"), - ("0.444", 2, "0.44"), - ("4.5", 0, "4"), - ("4.05", 1, "4.0"), - ("4.050", 1, "4.0"), - ("4.15", 1, "4.2"), - ("0.0045", 2, "0.00"), - ("5.5", -1, "10"), - ("-1.555", 2, "-1.56"), - ("-1.555", 99, "-1.555"), - ("5.5", 0, "6"), - ("-1", -1, "0"), - ("5", -1, "0"), - ("44", -1, "40"), - ("44", -99, "0"), - ("44", 99, "44"), - ("1.4499999999", -1, "0"), - ("1.4499999999", 0, "1"), - ("1.4499999999", 1, "1.4"), - ("1.4499999999", 2, "1.45"), - ("1.4499999999", 3, "1.450"), - ("1.4499999999", 4, "1.4500"), - ("1.4499999999", 10, "1.4499999999"), - ("1.4499999999", 15, "1.449999999900000"), - ("-1.4499999999", 1, "-1.4"), - ("1.449999999", 1, "1.4"), - ("-9999.444455556666", 10, "-9999.4444555567"), - ("-12345678987654321.123456789", 8, "-12345678987654321.12345679"), - ("0.33333333333333333333333333333333333333333333333333333333333333333333333333333333333333", 0, "0"), - ("0.1165085714285714285714285714285714285714", 0, "0"), - ("0.1165085714285714285714285714285714285714", 2, "0.12"), - ("0.1165085714285714285714285714285714285714", 5, "0.11651"), - ("0.1165085714285714285714285714285714285714", 8, "0.11650857"), - ]; - for &(x, digits, y) in test_cases.iter() { - let a = BigDecimal::from_str(x).unwrap(); - let b = BigDecimal::from_str(y).unwrap(); - let rounded = a.round(digits); - assert_eq!(rounded, b); - } - } - - #[test] - fn round_large_number() { - use super::BigDecimal; - - let z = BigDecimal::from_str("3.4613133327063255443352353815722045816611958409944513040035462804475524").unwrap(); - let expected = BigDecimal::from_str("11.9806899871705702711783103817684242408972124568942276285200973527647213").unwrap(); - let zsq = &z*&z; - let zsq = zsq.round(70); - debug_assert_eq!(zsq, expected); - } - - #[test] - fn test_is_integer() { - let true_vals = vec![ - "100", - "100.00", - "1724e4", - "31.47e8", - "-31.47e8", - "-0.0", - ]; - - let false_vals = vec![ - "100.1", - "0.001", - "3147e-3", - "3147e-8", - "-0.01", - "-1e-3", - ]; - - for s in true_vals { - let d = BigDecimal::from_str(&s).unwrap(); - assert!(d.is_integer()); - } - - for s in false_vals { - let d = BigDecimal::from_str(&s).unwrap(); - assert!(!d.is_integer()); - } - } - - #[test] - fn test_inverse() { - let vals = vec![ - ("100", "0.01"), - ("2", "0.5"), - (".2", "5"), - ("3.141592653", "0.3183098862435492205742690218851870990799646487459493049686604293188738877535183744268834079171116523"), - ]; - for &(x, y) in vals.iter() { - let a = BigDecimal::from_str(x).unwrap(); - let i = a.inverse(); - let b = BigDecimal::from_str(y).unwrap(); - assert_eq!(i, b); - assert_eq!(BigDecimal::from(1)/&a, b); - assert_eq!(i.inverse(), a); - // assert_eq!(a.scale, b.scale, "scale mismatch ({} != {}", a, b); - } - } - - mod double { - use super::*; - - include!("lib.tests.double.rs"); - } - - #[test] - fn test_square() { - let vals = vec![ - ("1.00", "1.00"), - ("1.5", "2.25"), - ("1.50", "2.2500"), - ("5", "25"), - ("5.0", "25.00"), - ("-5.0", "25.00"), - ("5.5", "30.25"), - ("0.80", "0.6400"), - ("0.01234", "0.0001522756"), - ("3.1415926", "9.86960406437476"), - ]; - for &(x, y) in vals.iter() { - let a = BigDecimal::from_str(x).unwrap().square(); - let b = BigDecimal::from_str(y).unwrap(); - assert_eq!(a, b); - assert_eq!(a.scale, b.scale); - } - } - - #[test] - fn test_cube() { - let vals = vec![ - ("1.00", "1.00"), - ("1.50", "3.375000"), - ("5", "125"), - ("5.0", "125.000"), - ("5.00", "125.000000"), - ("-5", "-125"), - ("-5.0", "-125.000"), - ("2.01", "8.120601"), - ("5.5", "166.375"), - ("0.01234", "0.000001879080904"), - ("3.1415926", "31.006275093569669642776"), - ]; - for &(x, y) in vals.iter() { - let a = BigDecimal::from_str(x).unwrap().cube(); - let b = BigDecimal::from_str(y).unwrap(); - assert_eq!(a, b); - assert_eq!(a.scale, b.scale); - } - } - - #[test] - fn test_exp() { - let vals = vec![ - ("0", "1"), - ("1", "2.718281828459045235360287471352662497757247093699959574966967627724076630353547594571382178525166427"), - ("1.01", "2.745601015016916493989776316660387624073750819595962291667398087987297168243899027802501018008905180"), - ("0.5", "1.648721270700128146848650787814163571653776100710148011575079311640661021194215608632776520056366643"), - ("-1", "0.3678794411714423215955237701614608674458111310317678345078368016974614957448998033571472743459196437"), - ("-0.01", "0.9900498337491680535739059771800365577720790812538374668838787452931477271687452950182155307793838110"), - ("-10.04", "0.00004361977305405268676261569570537884674661515701779752139657120453194647205771372804663141467275928595"), - //("-1000.04", "4.876927702336787390535723208392195312680380995235400234563172353460484039061383367037381490416091595E-435"), - ("-20.07", "1.921806899438469499721914055500607234723811054459447828795824348465763824284589956630853464778332349E-9"), - ("10", "22026.46579480671651695790064528424436635351261855678107423542635522520281857079257519912096816452590"), - ("20", "485165195.4097902779691068305415405586846389889448472543536108003159779961427097401659798506527473494"), - //("777.7", "5.634022488451236612534495413455282583175841288248965283178668787259870456538271615076138061788051442E+337"), - ]; - for &(x, y) in vals.iter() { - let a = BigDecimal::from_str(x).unwrap().exp(); - let b = BigDecimal::from_str(y).unwrap(); - assert_eq!(a, b); - } - } #[test] fn test_fmt() { @@ -2033,80 +110,4 @@ mod bigdecimal_tests { assert_eq!(format!("{:?}", var), expected); } } - - #[test] - fn test_signed() { - assert!(!BigDecimal::zero().is_positive()); - assert!(!BigDecimal::one().is_negative()); - - assert!(BigDecimal::one().is_positive()); - assert!((-BigDecimal::one()).is_negative()); - 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), - "1324000000"), - (BigDecimal::new(BigInt::from(1_900_000), 3), - BigDecimal::new(BigInt::from(19), -2), - "1900"), - (BigDecimal::new(BigInt::from(0), -3), - BigDecimal::zero(), - "0"), - (BigDecimal::new(BigInt::from(0), 5), - BigDecimal::zero(), - "0"), - ]; - - 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); - } - } - - #[test] - fn test_from_i128() { - let value = BigDecimal::from_i128(-368934881474191032320).unwrap(); - let expected = BigDecimal::from_str("-368934881474191032320").unwrap(); - assert_eq!(value, expected); - } - - #[test] - fn test_from_u128() { - let value = BigDecimal::from_u128(668934881474191032320).unwrap(); - let expected = BigDecimal::from_str("668934881474191032320").unwrap(); - assert_eq!(value, expected); - } -} - - -#[cfg(test)] -#[allow(non_snake_case)] -mod test_with_scale_round { - use super::*; - use paste::paste; - - include!("lib.tests.with_scale_round.rs"); -} - - -#[cfg(all(test, property_tests))] -extern crate proptest; - -#[cfg(all(test, property_tests))] -mod proptests { - use super::*; - use paste::paste; - use proptest::*; - - include!("lib.tests.property-tests.rs"); } From 26cff3b16aff1b3092db28ed1f78991f2281ef04 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sun, 30 Jul 2023 11:27:32 -0400 Subject: [PATCH 17/54] Add scientific- & engineering- notation functions to impl_fmt.rs --- src/impl_fmt.rs | 75 +++++++++++++++++++++- src/impl_fmt.tests.engineering_notation.rs | 20 ++++++ src/impl_fmt.tests.scientific_notation.rs | 18 ++++++ src/lib.rs | 44 +++++++++++++ 4 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 src/impl_fmt.tests.engineering_notation.rs create mode 100644 src/impl_fmt.tests.scientific_notation.rs diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index cf47821..677299d 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -2,6 +2,7 @@ //! use crate::*; +use stdlib::fmt::Write; impl fmt::Display for BigDecimal { @@ -68,8 +69,68 @@ impl fmt::Debug for BigDecimal { } +#[inline(never)] +pub(crate) fn write_scientific_notation(n: &BigDecimal, w: &mut W) -> fmt::Result { + if n.is_zero() { + return w.write_str("0e0"); + } + + if n.int_val.sign() == Sign::Minus { + w.write_str("-")?; + } + + let digits = n.int_val.magnitude(); + + let dec_str = digits.to_str_radix(10); + let (first_digit, remaining_digits) = dec_str.as_str().split_at(1); + w.write_str(first_digit)?; + if !remaining_digits.is_empty() { + w.write_str(".")?; + w.write_str(remaining_digits)?; + } + write!(w, "e{}", remaining_digits.len() as i64 - n.scale) +} + + +#[inline(never)] +pub(crate) fn write_engineering_notation(n: &BigDecimal, out: &mut W) -> fmt::Result { + if n.is_zero() { + return out.write_str("0e0"); + } + + if n.int_val.sign() == Sign::Minus { + out.write_char('-')?; + } + + let digits = n.int_val.magnitude(); + + let dec_str = digits.to_str_radix(10); + let digit_count = dec_str.len(); + + let top_digit_exponent = digit_count as i128 - n.scale as i128; + + let shift_amount = match top_digit_exponent.rem_euclid(3) { + 0 => 3, + i => i, + }; + + let (head, rest) = dec_str.split_at(shift_amount as usize); + let exp = top_digit_exponent - shift_amount; + debug_assert_eq!(exp % 3, 0); + + out.write_str(head)?; + + if !rest.is_empty() { + out.write_char('.')?; + out.write_str(rest)?; + } + + return write!(out, "e{}", exp); +} + + #[cfg(test)] -mod tests { +mod test { use super::*; #[test] @@ -110,4 +171,16 @@ mod tests { assert_eq!(format!("{:?}", var), expected); } } + + mod write_scientific_notation { + use super::*; + + include!("impl_fmt.tests.scientific_notation.rs"); + } + + mod write_engineering_notation { + use super::*; + + include!("impl_fmt.tests.engineering_notation.rs"); + } } diff --git a/src/impl_fmt.tests.engineering_notation.rs b/src/impl_fmt.tests.engineering_notation.rs new file mode 100644 index 0000000..a726509 --- /dev/null +++ b/src/impl_fmt.tests.engineering_notation.rs @@ -0,0 +1,20 @@ + + +macro_rules! impl_case { + ($name:ident : $in:literal => $ex:literal) => { + #[test] + fn $name() { + let n: BigDecimal = $in.parse().unwrap(); + let s = n.to_engineering_notation(); + assert_eq!(&s, $ex); + } + }; +} + +impl_case!(case_4_1592480782835e9 : "4159248078.2835" => "4.1592480782835e9"); +impl_case!(case_12_34e_6 : "0.00001234" => "12.34e-6"); +impl_case!(case_0 : "0" => "0e0"); +impl_case!(case_1 : "1" => "1e0"); +impl_case!(case_2_00e0 : "2.00" => "2.00e0"); +impl_case!(case_neg_5_70e1 : "-57.0" => "-57.0e0"); +impl_case!(case_5_31e5 : "5.31e5" => "531e3"); diff --git a/src/impl_fmt.tests.scientific_notation.rs b/src/impl_fmt.tests.scientific_notation.rs new file mode 100644 index 0000000..6b34acf --- /dev/null +++ b/src/impl_fmt.tests.scientific_notation.rs @@ -0,0 +1,18 @@ + +macro_rules! impl_case { + ($name:ident : $in:literal => $ex:literal) => { + #[test] + fn $name() { + let n: BigDecimal = $in.parse().unwrap(); + let s = n.to_scientific_notation(); + assert_eq!(&s, $ex); + } + }; +} + +impl_case!(case_4_1592480782835e9 : "4159248078.2835" => "4.1592480782835e9"); +impl_case!(case_1_234e_5 : "0.00001234" => "1.234e-5"); +impl_case!(case_0 : "0" => "0e0"); +impl_case!(case_1 : "1" => "1e0"); +impl_case!(case_2_00e0 : "2.00" => "2.00e0"); +impl_case!(case_neg_5_70e1 : "-57.0" => "-5.70e1"); diff --git a/src/lib.rs b/src/lib.rs index b413163..c5e8401 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -882,6 +882,50 @@ impl BigDecimal { let scale = self.scale - trailing_count as i64; BigDecimal::new(int_val, scale) } + + ////////////////////////// + // Formatting methods + + /// Create string of this bigdecimal in scientific notation + /// + /// ``` + /// # use bigdecimal::BigDecimal; + /// let n = BigDecimal::from(12345678); + /// assert_eq!(&n.to_scientific_notation(), "1.2345678e7"); + /// ``` + pub fn to_scientific_notation(&self) -> String { + let mut output = String::new(); + self.write_scientific_notation(&mut output).expect("Could not write to string"); + output + } + + /// Write bigdecimal in scientific notation to writer `w` + pub fn write_scientific_notation(&self, w: &mut W) -> fmt::Result { + impl_fmt::write_scientific_notation(self, w) + } + + /// Create string of this bigdecimal in engineering notation + /// + /// Engineering notation is scientific notation with the exponent + /// coerced to a multiple of three + /// + /// ``` + /// # use bigdecimal::BigDecimal; + /// let n = BigDecimal::from(12345678); + /// assert_eq!(&n.to_engineering_notation(), "12.345678e6"); + /// ``` + /// + pub fn to_engineering_notation(&self) -> String { + let mut output = String::new(); + self.write_engineering_notation(&mut output).expect("Could not write to string"); + output + } + + /// Write bigdecimal in engineering notation to writer `w` + pub fn write_engineering_notation(&self, w: &mut W) -> fmt::Result { + impl_fmt::write_engineering_notation(self, w) + } + } #[derive(Debug, PartialEq)] From 3f994f733e8e754293c1887964a5587929d0cbca Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Tue, 15 Aug 2023 23:22:46 -0400 Subject: [PATCH 18/54] Update fmt::Debug tests --- src/impl_fmt.rs | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index 677299d..0cfe860 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -64,7 +64,11 @@ impl fmt::Display for BigDecimal { impl fmt::Debug for BigDecimal { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "BigDecimal(\"{}\")", self) + if self.scale.abs() < 40 { + write!(f, "BigDecimal(\"{}\")", self) + } else { + write!(f, "BigDecimal(\"{:?}e{}\")", self.int_val, -self.scale) + } } } @@ -156,20 +160,28 @@ mod test { } } - #[test] - fn test_debug() { - let vals = vec![ - ("BigDecimal(\"123.456\")", "123.456"), - ("BigDecimal(\"123.400\")", "123.400"), - ("BigDecimal(\"1.20\")", "01.20"), - // ("BigDecimal(\"1.2E3\")", "01.2E3"), <- ambiguous precision - ("BigDecimal(\"1200\")", "01.2E3"), - ]; + mod test_debug { + use super::*; - for (expected, source) in vals { - let var = BigDecimal::from_str(source).unwrap(); - assert_eq!(format!("{:?}", var), expected); + macro_rules! impl_case { + ($name:ident : $s:literal => $expected:literal) => { + #[test] + fn $name() { + let d: BigDecimal = $s.parse().unwrap(); + let s = format!("{:?}", d); + assert_eq!(s, $expected) + } + }; } + + impl_case!(case_0: "0" => r#"BigDecimal("0")"#); + impl_case!(case_1: "1" => r#"BigDecimal("1")"#); + impl_case!(case_123_400: "123.400" => r#"BigDecimal("123.400")"#); + impl_case!(case_123_456: "123.456" => r#"BigDecimal("123.456")"#); + impl_case!(case_01_20: "01.20" => r#"BigDecimal("1.20")"#); + impl_case!(case_1_20: "1.20" => r#"BigDecimal("1.20")"#); + impl_case!(case_01_2e3: "01.2E3" => r#"BigDecimal("1200")"#); + impl_case!(case_avagadro: "6.02214076e1023" => r#"BigDecimal("602214076e1015")"#); } mod write_scientific_notation { From d9117709072289b73b71895fef88af9b23b1af55 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Tue, 15 Aug 2023 23:27:27 -0400 Subject: [PATCH 19/54] Move {sci,eng}-notation tests to impl_fmt.rs --- src/impl_fmt.rs | 66 +++++++++++++++++----- src/impl_fmt.tests.engineering_notation.rs | 20 ------- src/impl_fmt.tests.scientific_notation.rs | 18 ------ 3 files changed, 53 insertions(+), 51 deletions(-) delete mode 100644 src/impl_fmt.tests.engineering_notation.rs delete mode 100644 src/impl_fmt.tests.scientific_notation.rs diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index 0cfe860..272c8b5 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -8,7 +8,7 @@ use stdlib::fmt::Write; impl fmt::Display for BigDecimal { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // Acquire the absolute integer as a decimal string - let mut abs_int = self.int_val.abs().to_str_radix(10); + let mut abs_int = self.int_val.magnitude().to_str_radix(10); // Split the representation at the decimal point let (before, after) = if self.scale >= abs_int.len() as i64 { @@ -137,6 +137,21 @@ pub(crate) fn write_engineering_notation(n: &BigDecimal, out: &mut W) mod test { use super::*; + /// test case builder for mapping decimal-string to formatted-string + /// define test_fmt_function! macro to test your function + #[cfg(test)] + macro_rules! impl_case { + ($name:ident : $in:literal => $ex:literal) => { + #[test] + fn $name() { + let n: BigDecimal = $in.parse().unwrap(); + let s = test_fmt_function!(n); + assert_eq!(&s, $ex); + } + }; + } + + #[test] fn test_fmt() { let vals = vec![ @@ -160,18 +175,11 @@ mod test { } } - mod test_debug { + mod fmt_debug { use super::*; - macro_rules! impl_case { - ($name:ident : $s:literal => $expected:literal) => { - #[test] - fn $name() { - let d: BigDecimal = $s.parse().unwrap(); - let s = format!("{:?}", d); - assert_eq!(s, $expected) - } - }; + macro_rules! test_fmt_function { + ($n:expr) => { format!("{:?}", $n) }; } impl_case!(case_0: "0" => r#"BigDecimal("0")"#); @@ -182,17 +190,49 @@ mod test { impl_case!(case_1_20: "1.20" => r#"BigDecimal("1.20")"#); impl_case!(case_01_2e3: "01.2E3" => r#"BigDecimal("1200")"#); impl_case!(case_avagadro: "6.02214076e1023" => r#"BigDecimal("602214076e1015")"#); + + impl_case!(case_1e99999999999999 : "1e99999999999999" => r#"BigDecimal("1e99999999999999")"#); } mod write_scientific_notation { use super::*; - include!("impl_fmt.tests.scientific_notation.rs"); + macro_rules! test_fmt_function { + ($n:expr) => { $n.to_scientific_notation() }; + } + + impl_case!(case_4_1592480782835e9 : "4159248078.2835" => "4.1592480782835e9"); + impl_case!(case_1_234e_5 : "0.00001234" => "1.234e-5"); + impl_case!(case_0 : "0" => "0e0"); + impl_case!(case_1 : "1" => "1e0"); + impl_case!(case_2_00e0 : "2.00" => "2.00e0"); + impl_case!(case_neg_5_70e1 : "-57.0" => "-5.70e1"); } mod write_engineering_notation { use super::*; - include!("impl_fmt.tests.engineering_notation.rs"); + macro_rules! test_fmt_function { + ($n:expr) => { $n.to_engineering_notation() }; + } + + impl_case!(case_4_1592480782835e9 : "4159248078.2835" => "4.1592480782835e9"); + impl_case!(case_12_34e_6 : "0.00001234" => "12.34e-6"); + impl_case!(case_0 : "0" => "0e0"); + impl_case!(case_1 : "1" => "1e0"); + impl_case!(case_2_00e0 : "2.00" => "2.00e0"); + impl_case!(case_neg_5_70e1 : "-57.0" => "-57.0e0"); + impl_case!(case_5_31e4 : "5.31e4" => "53.1e3"); + impl_case!(case_5_31e5 : "5.31e5" => "531e3"); + impl_case!(case_5_31e6 : "5.31e6" => "5.31e6"); + impl_case!(case_5_31e7 : "5.31e7" => "53.1e6"); + + impl_case!(case_4e99999999999999 : "4e99999999999999" => "4e99999999999999"); + impl_case!(case_4e99999999999998 : "4e99999999999998" => "400e99999999999996"); + impl_case!(case_44e99999999999998 : "44e99999999999998" => "4.4e99999999999999"); + impl_case!(case_4e99999999999997 : "4e99999999999997" => "40e99999999999996"); + impl_case!(case_41e99999999999997 : "41e99999999999997" => "410e99999999999996"); + impl_case!(case_413e99999999999997 : "413e99999999999997" => "4.13e99999999999999"); + // impl_case!(case_413e99999999999997 : "413e99999999999997" => "4.13e99999999999999"); } } diff --git a/src/impl_fmt.tests.engineering_notation.rs b/src/impl_fmt.tests.engineering_notation.rs deleted file mode 100644 index a726509..0000000 --- a/src/impl_fmt.tests.engineering_notation.rs +++ /dev/null @@ -1,20 +0,0 @@ - - -macro_rules! impl_case { - ($name:ident : $in:literal => $ex:literal) => { - #[test] - fn $name() { - let n: BigDecimal = $in.parse().unwrap(); - let s = n.to_engineering_notation(); - assert_eq!(&s, $ex); - } - }; -} - -impl_case!(case_4_1592480782835e9 : "4159248078.2835" => "4.1592480782835e9"); -impl_case!(case_12_34e_6 : "0.00001234" => "12.34e-6"); -impl_case!(case_0 : "0" => "0e0"); -impl_case!(case_1 : "1" => "1e0"); -impl_case!(case_2_00e0 : "2.00" => "2.00e0"); -impl_case!(case_neg_5_70e1 : "-57.0" => "-57.0e0"); -impl_case!(case_5_31e5 : "5.31e5" => "531e3"); diff --git a/src/impl_fmt.tests.scientific_notation.rs b/src/impl_fmt.tests.scientific_notation.rs deleted file mode 100644 index 6b34acf..0000000 --- a/src/impl_fmt.tests.scientific_notation.rs +++ /dev/null @@ -1,18 +0,0 @@ - -macro_rules! impl_case { - ($name:ident : $in:literal => $ex:literal) => { - #[test] - fn $name() { - let n: BigDecimal = $in.parse().unwrap(); - let s = n.to_scientific_notation(); - assert_eq!(&s, $ex); - } - }; -} - -impl_case!(case_4_1592480782835e9 : "4159248078.2835" => "4.1592480782835e9"); -impl_case!(case_1_234e_5 : "0.00001234" => "1.234e-5"); -impl_case!(case_0 : "0" => "0e0"); -impl_case!(case_1 : "1" => "1e0"); -impl_case!(case_2_00e0 : "2.00" => "2.00e0"); -impl_case!(case_neg_5_70e1 : "-57.0" => "-5.70e1"); From 6aebed75fa2eeaab3f222ec5aabf1ee8a2f1967c Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Wed, 16 Aug 2023 01:56:01 -0400 Subject: [PATCH 20/54] Fix potential panic in write_engineering_notation --- src/impl_fmt.rs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index 272c8b5..09e2988 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -115,11 +115,20 @@ pub(crate) fn write_engineering_notation(n: &BigDecimal, out: &mut W) let shift_amount = match top_digit_exponent.rem_euclid(3) { 0 => 3, - i => i, + i => i as usize, }; - let (head, rest) = dec_str.split_at(shift_amount as usize); - let exp = top_digit_exponent - shift_amount; + let exp = top_digit_exponent - shift_amount as i128; + + // handle adding zero padding + if let Some(padding_zero_count) = shift_amount.checked_sub(dec_str.len()) { + let zeros = &"000"[..padding_zero_count]; + out.write_str(&dec_str)?; + out.write_str(zeros)?; + return write!(out, "e{}", exp); + } + + let (head, rest) = dec_str.split_at(shift_amount); debug_assert_eq!(exp % 3, 0); out.write_str(head)?; @@ -227,6 +236,12 @@ mod test { impl_case!(case_5_31e6 : "5.31e6" => "5.31e6"); impl_case!(case_5_31e7 : "5.31e7" => "53.1e6"); + impl_case!(case_1e2 : "1e2" => "100e0"); + impl_case!(case_1e119 : "1e19" => "10e18"); + impl_case!(case_1e3000 : "1e3000" => "1e3000"); + impl_case!(case_4_2e7 : "4.2e7" => "42e6"); + impl_case!(case_4_2e8 : "4.2e8" => "420e6"); + impl_case!(case_4e99999999999999 : "4e99999999999999" => "4e99999999999999"); impl_case!(case_4e99999999999998 : "4e99999999999998" => "400e99999999999996"); impl_case!(case_44e99999999999998 : "44e99999999999998" => "4.4e99999999999999"); From 5ebe932a4ea736b0c52aea31a57a7c9ab60294ce Mon Sep 17 00:00:00 2001 From: Declan Kelly Date: Mon, 8 Jan 2024 12:34:56 -0800 Subject: [PATCH 21/54] Add more tests for parsing and formatting decimals **Description** - Add a test for the format of decimals with a large representation - Add a test which: 1. Takes a string and parses it to a decimal `N` 2. Converts `N` to a string representation `N_repr 3. Parses `N_repr` as a new decimal `M` 4. Asserts that `N` and `M` are equal - Add a property test which does that same thing as the previous test, just using rust primitives as the input instead of strings **Motivation** I'd like to make a change to how decimals are formatted when the `scale` integer is very large and I wanted to lock down the current behavior with some tests. **Testing Done** Added some tests (listed above), then ran: ```bash ./scripts/bigdecimal-property-tests enable cargo test ``` The oldest `rustc` that I could use was `1.65`: ``` active toolchain ---------------- 1.65-aarch64-apple-darwin (directory override for '/Users/thedeck/repos/github/declanvk/bigdecimal-rs') rustc 1.65.0 (897e37553 2022-11-02) ``` Probably because I'm using Apple silicon, didn't look too deeply --- src/impl_fmt.rs | 100 ++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 47 +++++++++++++++++++++++ 2 files changed, 147 insertions(+) diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index 09e2988..34dc398 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -184,6 +184,56 @@ mod test { } } + #[test] + fn test_fmt_with_large_values() { + let vals = vec![ + // b s ( {} {:.1} {:.4} {:4.1} {:+05.1} {:<4.1} + // Numbers with large scales + (1, 10_000, ( + String::from("0.") + &"0".repeat(9_999) + "1", + String::from("0.0"), + String::from("0.0000"), + String::from(" 0.0"), + String::from("+00.0"), + String::from("0.0 ") + )), + (1, -10_000, ( + String::from("1") + &"0".repeat(10_000), + String::from("1") + &"0".repeat(10_000) + ".0", + String::from("1") + &"0".repeat(10_000) + ".0000", + String::from("1") + &"0".repeat(10_000) + ".0", + String::from("+1") + &"0".repeat(10_000) + ".0", + String::from("1") + &"0".repeat(10_000) + ".0" + )), + // Numbers with many digits + (1234506789, 5, ( + "12345.06789".into(), + "12345.0".into(), + "12345.0678".into(), + "12345.0".into(), + "+12345.0".into(), + "12345.0".into() + )), + (1234506789, -5, ( + "123450678900000".into(), + "123450678900000.0".into(), + "123450678900000.0000".into(), + "123450678900000.0".into(), + "+123450678900000.0".into(), + "123450678900000.0".into() + )), + ]; + for (i, scale, results) in vals { + let x = BigDecimal::new(num_bigint::BigInt::from(i), scale); + assert_eq!(format!("{}", x), results.0); + 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.1}", x), results.4); + assert_eq!(format!("{:<4.1}", x), results.5); + } + } + mod fmt_debug { use super::*; @@ -251,3 +301,53 @@ mod test { // impl_case!(case_413e99999999999997 : "413e99999999999997" => "4.13e99999999999999"); } } + + +#[cfg(all(test, property_tests))] +mod proptests { + use super::*; + use paste::paste; + use proptest::*; + + macro_rules! impl_parsing_test { + ($t:ty) => { + paste! { proptest! { + #[test] + fn [< roudtrip_to_str_and_back_ $t >](n: $t) { + let original = BigDecimal::from(n); + let display = format!("{}", original); + let parsed = display.parse::().unwrap(); + + prop_assert_eq!(&original, &parsed); + } + } } + }; + (from-float $t:ty) => { + paste! { proptest! { + #[test] + fn [< roudtrip_to_str_and_back_ $t >](n: $t) { + let original = BigDecimal::try_from(n).unwrap(); + let display = format!("{}", original); + let parsed = display.parse::().unwrap(); + + prop_assert_eq!(&original, &parsed); + } + } } + }; + } + + impl_parsing_test!(u8); + impl_parsing_test!(u16); + impl_parsing_test!(u32); + impl_parsing_test!(u64); + impl_parsing_test!(u128); + + impl_parsing_test!(i8); + impl_parsing_test!(i16); + impl_parsing_test!(i32); + impl_parsing_test!(i64); + impl_parsing_test!(i128); + + impl_parsing_test!(from-float f32); + impl_parsing_test!(from-float f64); +} diff --git a/src/lib.rs b/src/lib.rs index c5e8401..34e9b9b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2032,6 +2032,53 @@ mod bigdecimal_tests { let expected = BigDecimal::from_str("668934881474191032320").unwrap(); assert_eq!(value, expected); } + + #[test] + fn test_parse_roundtrip() { + let vals = vec![ + "1.0", + "0.5", + "50", + "50000", + "0.001000000000000000020816681711721685132943093776702880859375", + "0.25", + "12.339999999999999857891452847979962825775146484375", + "0.15625", + "0.333333333333333314829616256247390992939472198486328125", + "3.141592653589793115997963468544185161590576171875", + "31415.926535897931898944079875946044921875", + "94247.779607693795696832239627838134765625", + "1331.107", + "1.0", + "2e1", + "0.00123", + "-123", + "-1230", + "12.3", + "123e-1", + "1.23e+1", + "1.23E+3", + "1.23E-8", + "-1.23E-10", + "123_", + "31_862_140.830_686_979", + "-1_1.2_2", + "999.521_939", + "679.35_84_03E-2", + "271576662.__E4", + // Large decimals with small text representations + "1E10000", + "1E-10000", + "1.129387461293874682630000000487984723987459E10000", + "11293874612938746826340000000087984723987459E10000", + ]; + for s in vals { + let expected = BigDecimal::from_str(s).unwrap(); + let display = format!("{}", expected); + let parsed = BigDecimal::from_str(&display).unwrap(); + assert_eq!(expected, parsed); + } + } } From c4dd7fe4ea4d164f36f87adc5f123396eb8b0476 Mon Sep 17 00:00:00 2001 From: Declan Kelly Date: Mon, 8 Jan 2024 15:17:49 -0800 Subject: [PATCH 22/54] Special case formatting of numbers with large scale **Description** Add a second path to the `Display` impl so that numbers that have a large scale relative to length of their integer representation are formatted differently. Specifically, I switched it to use exponential number format like: ``` 1.23456789E38 ``` instead of ``` 123456789000000000000000000000000000000 ``` **Motivation** If you extrapolate from the example above, and also looking at issue https://github.com/akubera/bigdecimal-rs/issues/108, you could see that with large exponents you run the risk of large allocations, since the resulting bytestring is proportional in size to the exponent. Any software using the `bigdecimal` crate and converting unprotected uer input into the `BigDecimal` type runs the risk of allocating way too much and potentially crashing, if they happen to use either the `Debug` or `Display` impls. This change is useful because it bounds the output and prevents unexpected large allocations when formatting the `BigDecimal` type **Testing Done** `cargo test && ./scripts/bigdecimal-property-tests test` **Backwards Compatibility Criteria** I believe that this does not constitue a backwards incompatible change because the `BigDecimal::from_str` method already supports exponential format numbers as input. --- src/impl_fmt.rs | 244 ++++++++++++++++++++++++++++++++---------------- src/lib.rs | 2 +- 2 files changed, 163 insertions(+), 83 deletions(-) diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index 34dc398..b13aac8 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -7,57 +7,16 @@ use stdlib::fmt::Write; impl fmt::Display for BigDecimal { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // Acquire the absolute integer as a decimal string - let mut abs_int = self.int_val.magnitude().to_str_radix(10); - - // Split the representation at the decimal point - let (before, after) = if self.scale >= abs_int.len() as i64 { - // First case: the integer representation falls - // completely behind the decimal point - let scale = self.scale as usize; - let after = "0".repeat(scale - abs_int.len()) + abs_int.as_str(); - ("0".to_string(), after) - } else { - // Second case: the integer representation falls - // around, or before the decimal point - let location = abs_int.len() as i64 - self.scale; - if location > abs_int.len() as i64 { - // Case 2.1, entirely before the decimal point - // We should prepend zeros - let zeros = location as usize - abs_int.len(); - let abs_int = abs_int + "0".repeat(zeros).as_str(); - (abs_int, "".to_string()) - } else { - // Case 2.2, somewhere around the decimal point - // Just split it in two - let after = abs_int.split_off(location as usize); - (abs_int, after) - } - }; + const EXPONENTIAL_FORMAT_THRESHOLD: i64 = 25; - // Alter precision after the decimal point - let after = if let Some(precision) = f.precision() { - let len = after.len(); - if len < precision { - after + "0".repeat(precision - len).as_str() - } else { - // TODO: Should we round? - after[0..precision].to_string() - } - } else { - after - }; + // Acquire the absolute integer as a decimal string + let abs_int = self.int_val.magnitude().to_str_radix(10); - // Concatenate everything - let complete_without_sign = if !after.is_empty() { - before + "." + after.as_str() + if (abs_int.len() as i64 - self.scale - 1).abs() > EXPONENTIAL_FORMAT_THRESHOLD { + format_exponential(self, f, abs_int) } else { - before - }; - - let non_negative = matches!(self.int_val.sign(), Sign::Plus | Sign::NoSign); - //pad_integral does the right thing although we have a decimal - f.pad_integral(non_negative, "", &complete_without_sign) + format_full_scale(self, f, abs_int) + } } } @@ -73,6 +32,141 @@ impl fmt::Debug for BigDecimal { } +fn format_full_scale( + this: &BigDecimal, + f: &mut fmt::Formatter, + mut abs_int: String, +) -> fmt::Result { + // Split the representation at the decimal point + let (before, after) = if this.scale >= abs_int.len() as i64 { + // First case: the integer representation falls + // completely behind the decimal point + let scale = this.scale as usize; + let after = "0".repeat(scale - abs_int.len()) + abs_int.as_str(); + ("0".to_string(), after) + } else { + // Second case: the integer representation falls + // around, or before the decimal point + let location = abs_int.len() as i64 - this.scale; + if location > abs_int.len() as i64 { + // Case 2.1, entirely before the decimal point + // We should prepend zeros + let zeros = location as usize - abs_int.len(); + let abs_int = abs_int + "0".repeat(zeros).as_str(); + (abs_int, "".to_string()) + } else { + // Case 2.2, somewhere around the decimal point + // Just split it in two + let after = abs_int.split_off(location as usize); + (abs_int, after) + } + }; + + // Alter precision after the decimal point + let after = if let Some(precision) = f.precision() { + let len = after.len(); + if len < precision { + after + "0".repeat(precision - len).as_str() + } else { + // TODO: Should we round? + after[0..precision].to_string() + } + } else { + after + }; + + // Concatenate everything + let complete_without_sign = if !after.is_empty() { + before + "." + after.as_str() + } else { + before + }; + + let non_negative = matches!(this.int_val.sign(), Sign::Plus | Sign::NoSign); + //pad_integral does the right thing although we have a decimal + f.pad_integral(non_negative, "", &complete_without_sign) +} + + +fn format_exponential( + this: &BigDecimal, + f: &mut fmt::Formatter, + mut abs_int: String, +) -> fmt::Result { + // Steps: + // 1. Truncate integer based on precision + // 2. calculate exponent from the scale and the length of the internal integer + // 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 + + if abs_int.len() > 1 { + // only modify for precision if there is more than 1 decimal digit + if let Some(precision) = f.precision() { + // add 1 precision to consider first digit + // TODO: Should we round? + abs_int.truncate(precision + 1); + } + } + + // Determine the exponent value based on the scale + // + // # First case: the integer representation falls completely behind the + // decimal point. + // + // Example of this.scale > abs_int.len(): + // 0.000001234509876 + // abs_int.len() = 10 + // scale = 15 + // target is 1.234509876 + // exponent = -6 + // + // Example of this.scale == abs_int.len(): + // 0.333333333333333314829616256247390992939472198486328125 + // abs_int.len() = 54 + // scale = 54 + // target is 3.33333333333333314829616256247390992939472198486328125 + // exponent = -1 + // + // # Second case: the integer representation falls around, or before the + // decimal point + // + // ## Case 2.1, entirely before the decimal point. + // Example of (abs_int.len() - this.scale) > abs_int.len(): + // 123450987600000 + // abs_int.len() = 10 + // scale = -5 + // location = 15 + // target is 1.234509876 + // exponent = 14 + // + // ## Case 2.2, somewhere around the decimal point. + // Example of (abs_int.len() - this.scale) < abs_int.len(): + // 12.339999999999999857891452847979962825775146484375 + // abs_int.len() = 50 + // scale = 48 + // target is 1.2339999999999999857891452847979962825775146484375 + // exponent = 1 + // + // For the (abs_int.len() - this.scale) == abs_int.len() I couldn't + // come up with an example + let exponent = abs_int.len() as i64 - this.scale - 1; + + if abs_int.len() > 1 { + // only add decimal point if there is more than 1 decimal digit + abs_int.insert(1, '.'); + } + + if exponent != 0 { + abs_int += "E"; + abs_int += &exponent.to_string(); + } + + let non_negative = matches!(this.int_val.sign(), Sign::Plus | Sign::NoSign); + //pad_integral does the right thing although we have a decimal + f.pad_integral(non_negative, "", &abs_int) +} + + #[inline(never)] pub(crate) fn write_scientific_notation(n: &BigDecimal, w: &mut W) -> fmt::Result { if n.is_zero() { @@ -189,48 +283,34 @@ mod test { let vals = vec![ // b s ( {} {:.1} {:.4} {:4.1} {:+05.1} {:<4.1} // Numbers with large scales - (1, 10_000, ( - String::from("0.") + &"0".repeat(9_999) + "1", - String::from("0.0"), - String::from("0.0000"), - String::from(" 0.0"), - String::from("+00.0"), - String::from("0.0 ") - )), - (1, -10_000, ( - String::from("1") + &"0".repeat(10_000), - String::from("1") + &"0".repeat(10_000) + ".0", - String::from("1") + &"0".repeat(10_000) + ".0000", - String::from("1") + &"0".repeat(10_000) + ".0", - String::from("+1") + &"0".repeat(10_000) + ".0", - String::from("1") + &"0".repeat(10_000) + ".0" - )), + (1, 10_000, ("1E-10000", "1E-10000", "1E-10000", "1E-10000", "+1E-10000", "1E-10000")), + (1, -10_000, ("1E10000", "1E10000", "1E10000", "1E10000", "+1E10000", "1E10000")), // Numbers with many digits (1234506789, 5, ( - "12345.06789".into(), - "12345.0".into(), - "12345.0678".into(), - "12345.0".into(), - "+12345.0".into(), - "12345.0".into() + "12345.06789", + "12345.0", + "12345.0678", + "12345.0", + "+12345.0", + "12345.0" )), (1234506789, -5, ( - "123450678900000".into(), - "123450678900000.0".into(), - "123450678900000.0000".into(), - "123450678900000.0".into(), - "+123450678900000.0".into(), - "123450678900000.0".into() + "123450678900000", + "123450678900000.0", + "123450678900000.0000", + "123450678900000.0", + "+123450678900000.0", + "123450678900000.0" )), ]; for (i, scale, results) in vals { let x = BigDecimal::new(num_bigint::BigInt::from(i), scale); - assert_eq!(format!("{}", x), results.0); - 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.1}", x), results.4); - assert_eq!(format!("{:<4.1}", x), results.5); + assert_eq!(format!("{}", x), results.0, "digits={i} scale={scale}"); + assert_eq!(format!("{:.1}", x), results.1, "digits={i} scale={scale}"); + assert_eq!(format!("{:.4}", x), results.2, "digits={i} scale={scale}"); + assert_eq!(format!("{:4.1}", x), results.3, "digits={i} scale={scale}"); + assert_eq!(format!("{:+05.1}", x), results.4, "digits={i} scale={scale}"); + assert_eq!(format!("{:<4.1}", x), results.5, "digits={i} scale={scale}"); } } diff --git a/src/lib.rs b/src/lib.rs index 34e9b9b..b489c81 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2076,7 +2076,7 @@ mod bigdecimal_tests { let expected = BigDecimal::from_str(s).unwrap(); let display = format!("{}", expected); let parsed = BigDecimal::from_str(&display).unwrap(); - assert_eq!(expected, parsed); + assert_eq!(expected, parsed, "[{s}] didn't round trip through [{display}]"); } } } From 93377ecd5ab45c7cf98934cbbccc2f7b544378e7 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Wed, 17 Jan 2024 20:19:31 -0500 Subject: [PATCH 23/54] Move BigDecimal::Display:fmt implementation into new function --- src/impl_fmt.rs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index b13aac8..aa746c3 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -9,14 +9,7 @@ impl fmt::Display for BigDecimal { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { const EXPONENTIAL_FORMAT_THRESHOLD: i64 = 25; - // Acquire the absolute integer as a decimal string - let abs_int = self.int_val.magnitude().to_str_radix(10); - - if (abs_int.len() as i64 - self.scale - 1).abs() > EXPONENTIAL_FORMAT_THRESHOLD { - format_exponential(self, f, abs_int) - } else { - format_full_scale(self, f, abs_int) - } + dynamically_format_decimal(self, f, EXPONENTIAL_FORMAT_THRESHOLD) } } @@ -31,6 +24,21 @@ impl fmt::Debug for BigDecimal { } } +fn dynamically_format_decimal( + this: &BigDecimal, + f: &mut fmt::Formatter, + threshold: i64, +) -> fmt::Result { + // Acquire the absolute integer as a decimal string + let abs_int = this.int_val.abs().to_str_radix(10); + + if (abs_int.len() as i64 - this.scale - 1).abs() > threshold { + format_exponential(this, f, abs_int) + } else { + format_full_scale(this, f, abs_int) + } +} + fn format_full_scale( this: &BigDecimal, From 78d350a512e019885e4191d620177c00769d96c9 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Wed, 17 Jan 2024 20:28:02 -0500 Subject: [PATCH 24/54] Impl Display for BigDecimalRef --- src/impl_fmt.rs | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index aa746c3..133d4fb 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -9,7 +9,15 @@ impl fmt::Display for BigDecimal { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { const EXPONENTIAL_FORMAT_THRESHOLD: i64 = 25; - dynamically_format_decimal(self, f, EXPONENTIAL_FORMAT_THRESHOLD) + dynamically_format_decimal(self.into(), f, EXPONENTIAL_FORMAT_THRESHOLD) + } +} + +impl fmt::Display for BigDecimalRef<'_> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + const EXPONENTIAL_FORMAT_THRESHOLD: i64 = 25; + + dynamically_format_decimal(*self, f, EXPONENTIAL_FORMAT_THRESHOLD) } } @@ -24,13 +32,14 @@ impl fmt::Debug for BigDecimal { } } + fn dynamically_format_decimal( - this: &BigDecimal, + this: BigDecimalRef, f: &mut fmt::Formatter, threshold: i64, ) -> fmt::Result { // Acquire the absolute integer as a decimal string - let abs_int = this.int_val.abs().to_str_radix(10); + let abs_int = this.digits.to_str_radix(10); if (abs_int.len() as i64 - this.scale - 1).abs() > threshold { format_exponential(this, f, abs_int) @@ -41,7 +50,7 @@ fn dynamically_format_decimal( fn format_full_scale( - this: &BigDecimal, + this: BigDecimalRef, f: &mut fmt::Formatter, mut abs_int: String, ) -> fmt::Result { @@ -90,14 +99,14 @@ fn format_full_scale( before }; - let non_negative = matches!(this.int_val.sign(), Sign::Plus | Sign::NoSign); + let non_negative = matches!(this.sign(), Sign::Plus | Sign::NoSign); //pad_integral does the right thing although we have a decimal f.pad_integral(non_negative, "", &complete_without_sign) } fn format_exponential( - this: &BigDecimal, + this: BigDecimalRef, f: &mut fmt::Formatter, mut abs_int: String, ) -> fmt::Result { @@ -169,7 +178,7 @@ fn format_exponential( abs_int += &exponent.to_string(); } - let non_negative = matches!(this.int_val.sign(), Sign::Plus | Sign::NoSign); + let non_negative = matches!(this.sign(), Sign::Plus | Sign::NoSign); //pad_integral does the right thing although we have a decimal f.pad_integral(non_negative, "", &abs_int) } From 57075470f79ce8e90a122a4168327339f1cfa579 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Thu, 18 Jan 2024 01:48:38 -0500 Subject: [PATCH 25/54] Fix Rust compatibility issue --- src/impl_fmt.rs | 12 ++++++------ src/lib.rs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index 133d4fb..fc5ccc7 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -322,12 +322,12 @@ mod test { ]; for (i, scale, results) in vals { let x = BigDecimal::new(num_bigint::BigInt::from(i), scale); - assert_eq!(format!("{}", x), results.0, "digits={i} scale={scale}"); - assert_eq!(format!("{:.1}", x), results.1, "digits={i} scale={scale}"); - assert_eq!(format!("{:.4}", x), results.2, "digits={i} scale={scale}"); - assert_eq!(format!("{:4.1}", x), results.3, "digits={i} scale={scale}"); - assert_eq!(format!("{:+05.1}", x), results.4, "digits={i} scale={scale}"); - assert_eq!(format!("{:<4.1}", x), results.5, "digits={i} scale={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!("{:.4}", x), results.2, "digits={} scale={}", i, scale); + assert_eq!(format!("{:4.1}", x), results.3, "digits={} scale={}", i, scale); + assert_eq!(format!("{:+05.1}", x), results.4, "digits={} scale={}", i, scale); + assert_eq!(format!("{:<4.1}", x), results.5, "digits={} scale={}", i, scale); } } diff --git a/src/lib.rs b/src/lib.rs index b489c81..28e7e5b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2076,7 +2076,7 @@ mod bigdecimal_tests { let expected = BigDecimal::from_str(s).unwrap(); let display = format!("{}", expected); let parsed = BigDecimal::from_str(&display).unwrap(); - assert_eq!(expected, parsed, "[{s}] didn't round trip through [{display}]"); + assert_eq!(expected, parsed, "[{}] didn't round trip through [{}]", s, display); } } } From 61919f3c703025ab57a671eacec923ace293a033 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Wed, 17 Jan 2024 02:18:08 -0500 Subject: [PATCH 26/54] Support RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD in build.rs --- build.rs | 18 ++++++++++++++++++ src/impl_fmt.rs | 8 ++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/build.rs b/build.rs index 8e43e1a..32d61f4 100644 --- a/build.rs +++ b/build.rs @@ -16,6 +16,7 @@ fn main() { let outdir: PathBuf = std::env::var_os("OUT_DIR").unwrap().into(); write_default_precision_file(&outdir); write_default_rounding_mode(&outdir); + write_exponential_format_threshold_file(&outdir); } @@ -46,3 +47,20 @@ fn write_default_rounding_mode(outdir: &Path) { std::fs::write(rust_file_path, rust_file_contents).unwrap(); } + +/// 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(|_| "25".to_owned()); + println!("cargo:rerun-if-env-changed=RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD"); + + let rust_file_path = outdir.join("exponential_format_threshold.rs"); + + let value: u32 = env_var + .parse::() + .expect("$RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD must be an integer > 0") + .into(); + + let rust_file_contents = format!("const EXPONENTIAL_FORMAT_THRESHOLD: i64 = {};", value); + + std::fs::write(rust_file_path, rust_file_contents).unwrap(); +} diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index fc5ccc7..3c0047c 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -5,18 +5,18 @@ use crate::*; use stdlib::fmt::Write; +// const EXPONENTIAL_FORMAT_THRESHOLD: i64 = ${RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD} or 25; +include!(concat!(env!("OUT_DIR"), "/exponential_format_threshold.rs")); + + impl fmt::Display for BigDecimal { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - const EXPONENTIAL_FORMAT_THRESHOLD: i64 = 25; - dynamically_format_decimal(self.into(), f, EXPONENTIAL_FORMAT_THRESHOLD) } } impl fmt::Display for BigDecimalRef<'_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - const EXPONENTIAL_FORMAT_THRESHOLD: i64 = 25; - dynamically_format_decimal(*self, f, EXPONENTIAL_FORMAT_THRESHOLD) } } From a75e152f6b47ba523f3a857c1b73491ea930121f Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Wed, 17 Jan 2024 21:42:30 -0500 Subject: [PATCH 27/54] Change exp-form threshold calculation --- src/impl_fmt.rs | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index 3c0047c..06dd068 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -41,7 +41,7 @@ fn dynamically_format_decimal( // Acquire the absolute integer as a decimal string let abs_int = this.digits.to_str_radix(10); - if (abs_int.len() as i64 - this.scale - 1).abs() > threshold { + if this.scale < 0 || (this.scale > abs_int.len() as i64 + threshold) { format_exponential(this, f, abs_int) } else { format_full_scale(this, f, abs_int) @@ -271,6 +271,48 @@ mod test { }; } + /// "Mock" Formatter + /// + /// Given callable, forwards formatter to callable. + /// Required work-around due to lack of constructor in fmt::Formatter + /// + struct Fmt(F) + where F: Fn(&mut fmt::Formatter) -> fmt::Result; + + impl fmt::Display for Fmt + where F: Fn(&mut fmt::Formatter) -> fmt::Result + { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // call closure with given formatter + (self.0)(f) + } + } + + impl fmt::Debug for Fmt + where F: Fn(&mut fmt::Formatter) -> fmt::Result + { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + (self.0)(f) + } + } + + mod dynamic_fmt { + use super::*; + + macro_rules! test_fmt_function { + ($n:ident) => {{ + format!("{}", Fmt(|f| dynamically_format_decimal($n.to_ref(), f, 2))) + }}; + } + + impl_case!(case_0d123: "0.123" => "0.123"); + impl_case!(case_0d0123: "0.0123" => "0.0123"); + impl_case!(case_0d00123: "0.00123" => "0.00123"); + impl_case!(case_0d000123: "0.000123" => "1.23E-4"); + + impl_case!(case_123d: "123." => "123"); + impl_case!(case_123de1: "123.e1" => "1.23E3"); + } #[test] fn test_fmt() { From b57f528840323a2e2b94d469266d5d0e34372cc8 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sat, 20 Jan 2024 16:06:35 -0500 Subject: [PATCH 28/54] Update format_full_scale to round and avoid allocations --- src/impl_fmt.rs | 171 ++++++++++++++++++++++++++++++++++++------------ src/lib.rs | 1 + 2 files changed, 129 insertions(+), 43 deletions(-) diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index 06dd068..aad97f0 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -41,6 +41,9 @@ fn dynamically_format_decimal( // 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) { format_exponential(this, f, abs_int) } else { @@ -52,56 +55,83 @@ fn dynamically_format_decimal( fn format_full_scale( this: BigDecimalRef, f: &mut fmt::Formatter, - mut abs_int: String, + abs_int: String, ) -> fmt::Result { - // Split the representation at the decimal point - let (before, after) = if this.scale >= abs_int.len() as i64 { - // First case: the integer representation falls - // completely behind the decimal point - let scale = this.scale as usize; - let after = "0".repeat(scale - abs_int.len()) + abs_int.as_str(); - ("0".to_string(), after) - } else { - // Second case: the integer representation falls - // around, or before the decimal point - let location = abs_int.len() as i64 - this.scale; - if location > abs_int.len() as i64 { - // Case 2.1, entirely before the decimal point - // We should prepend zeros - let zeros = location as usize - abs_int.len(); - let abs_int = abs_int + "0".repeat(zeros).as_str(); - (abs_int, "".to_string()) - } else { - // Case 2.2, somewhere around the decimal point - // Just split it in two - let after = abs_int.split_off(location as usize); - (abs_int, after) + use stdlib::cmp::Ordering::*; + + let mut digits = abs_int.into_bytes(); + let mut exp = -this.scale; + let non_negative = matches!(this.sign, Sign::Plus | Sign::NoSign); + + 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() => { + debug_assert_ne!(prec, 0); + _apply_rounding_to_ascii_digits(&mut digits, &mut exp, prec, this.sign); + debug_assert_eq!(digits.len(), prec); + }, + _ => { + // not limited by precision } }; - // Alter precision after the decimal point - let after = if let Some(precision) = f.precision() { - let len = after.len(); - if len < precision { - after + "0".repeat(precision - len).as_str() - } else { - // TODO: Should we round? - after[0..precision].to_string() + // add the decimal point to 'digits' buffer + match exp.cmp(&0) { + // do not add decimal point for "full" integer + Equal => { } - } else { - after - }; - // Concatenate everything - let complete_without_sign = if !after.is_empty() { - before + "." + after.as_str() - } else { - before - }; + // never decimal point if only one digit long + Greater if digits.len() == 1 => { + } - let non_negative = matches!(this.sign(), Sign::Plus | Sign::NoSign); - //pad_integral does the right thing although we have a decimal - f.pad_integral(non_negative, "", &complete_without_sign) + // 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 i64 - 1; + + // 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); + + // exp = 0 means exponential-part will be ignored in output + exp = 0; + } + + // decimal point is to the left of digits (0.0000dddddddd) + Less => { + let digits_to_shift = exp.abs() as usize - digits.len(); + + 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); + + exp = 0; + } + } + + // move digits back into String form + let mut buf = String::from_utf8(digits).unwrap(); + + // add exp part to buffer (if not zero) + if exp != 0 { + write!(buf, "E{:+}", exp)?; + } + + // write buffer to formatter + f.pad_integral(non_negative, "", &buf) } @@ -253,6 +283,61 @@ pub(crate) fn write_engineering_notation(n: &BigDecimal, out: &mut W) } +/// Round big-endian digits in ascii +fn _apply_rounding_to_ascii_digits(ascii_digits: &mut Vec, exp: &mut i64, prec: usize, sign: Sign) { + let digit_count_to_remove = ascii_digits.len() - prec; + *exp += digit_count_to_remove as i64; + + // true if all ascii_digits after precision are zeros + let trailing_zeros = ascii_digits[prec + 1..].iter().all(|&d| d == b'0'); + + let sig_ascii = ascii_digits[prec - 1]; + let s = sig_ascii - b'0'; + let i = ascii_digits[prec] - b'0'; + let r = Context::default().round_pair(sign, s, i, trailing_zeros); + + // remove insignificant digits + ascii_digits.truncate(prec - 1); + + // push rounded value + if r < 10 { + ascii_digits.push(r + b'0'); + return + } + + debug_assert_eq!(r, 10); + + // push zero and carry-the-one + ascii_digits.push(b'0'); + + // loop through digits in reverse order + let mut digit_it = ascii_digits.iter_mut().rev().peekable(); + debug_assert_eq!(**digit_it.peek().unwrap(), sig_ascii); + + loop { + match digit_it.next() { + // carried one to 9 and continue + Some(d) if *d == b'9' => { + *d = b'0'; + continue; + }, + // add the carried one and return + Some(d) => { + *d += 1; + break; + }, + // case where all values were nines + None => { + // 'trim' the last zero + ascii_digits[0] = b'1'; + *exp += 1; + break; + } + } + } +} + + #[cfg(test)] mod test { use super::*; diff --git a/src/lib.rs b/src/lib.rs index 28e7e5b..def1742 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -84,6 +84,7 @@ use self::stdlib::iter::Sum; use self::stdlib::str::FromStr; use self::stdlib::string::{String, ToString}; use self::stdlib::fmt; +use self::stdlib::Vec; use num_bigint::{BigInt, BigUint, ParseBigIntError, Sign}; use num_integer::Integer as IntegerTrait; From 2c0d3e377cb3a24881ebae3ac82d4120d3a6d8d2 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sat, 20 Jan 2024 16:08:25 -0500 Subject: [PATCH 29/54] Change default EXPONENTIAL_THRESHOLD to 5 --- build.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.rs b/build.rs index 32d61f4..0db0ca3 100644 --- a/build.rs +++ b/build.rs @@ -50,7 +50,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(|_| "25".to_owned()); + 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 rust_file_path = outdir.join("exponential_format_threshold.rs"); From 467ec4f78b90acad761ca9a6233c87fa932274b3 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sat, 20 Jan 2024 16:27:51 -0500 Subject: [PATCH 30/54] Implement fmt::{Upper,Lower}Exp traits and improve Debug --- src/impl_fmt.rs | 45 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index aad97f0..c8256c5 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -11,7 +11,7 @@ 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.into(), f, EXPONENTIAL_FORMAT_THRESHOLD) + dynamically_format_decimal(self.to_ref(), f, EXPONENTIAL_FORMAT_THRESHOLD) } } @@ -22,12 +22,43 @@ impl fmt::Display for BigDecimalRef<'_> { } +impl fmt::LowerExp for BigDecimal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::LowerExp::fmt(&self.to_ref(), f) + } +} + +impl fmt::LowerExp for BigDecimalRef<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let abs_int = self.digits.to_str_radix(10); + format_exponential(*self, f, abs_int, "e") + } +} + + +impl fmt::UpperExp for BigDecimal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::UpperExp::fmt(&self.to_ref(), f) + } +} + +impl fmt::UpperExp for BigDecimalRef<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let abs_int = self.digits.to_str_radix(10); + format_exponential(*self, f, abs_int, "E") + } +} + + impl fmt::Debug for BigDecimal { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if self.scale.abs() < 40 { - write!(f, "BigDecimal(\"{}\")", self) + if f.alternate() { + write!(f, "BigDecimal(\"{}e{:+}\")", self.int_val, -self.scale) } else { - write!(f, "BigDecimal(\"{:?}e{}\")", self.int_val, -self.scale) + write!(f, + "BigDecimal(sign={:?}, scale={}, digits={:?})", + self.sign(), self.scale, self.int_val.magnitude().to_u64_digits() + ) } } } @@ -45,7 +76,7 @@ fn dynamically_format_decimal( // "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) { - format_exponential(this, f, abs_int) + format_exponential(this, f, abs_int, "E") } else { format_full_scale(this, f, abs_int) } @@ -139,6 +170,7 @@ fn format_exponential( this: BigDecimalRef, f: &mut fmt::Formatter, mut abs_int: String, + e_symbol: &str, ) -> fmt::Result { // Steps: // 1. Truncate integer based on precision @@ -204,8 +236,7 @@ fn format_exponential( } if exponent != 0 { - abs_int += "E"; - abs_int += &exponent.to_string(); + write!(abs_int, "{}{:+}", e_symbol, exponent)?; } let non_negative = matches!(this.sign(), Sign::Plus | Sign::NoSign); From 13ee734f32976a1254752936c69770788fc9683a Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sat, 20 Jan 2024 17:05:11 -0500 Subject: [PATCH 31/54] Update test_normalize for new fmt implementation --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index def1742..58b17c1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2001,10 +2001,10 @@ mod bigdecimal_tests { "0.1"), (BigDecimal::new(BigInt::from(132400), -4), BigDecimal::new(BigInt::from(1324), -6), - "1324000000"), + "1.324E+9"), (BigDecimal::new(BigInt::from(1_900_000), 3), BigDecimal::new(BigInt::from(19), -2), - "1900"), + "1.9E+3"), (BigDecimal::new(BigInt::from(0), -3), BigDecimal::zero(), "0"), From 2a0b0ec00e937d7f0d0b77ee0a88594308773044 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sat, 20 Jan 2024 17:51:52 -0500 Subject: [PATCH 32/54] Update fmt_debug for new behavior --- src/impl_fmt.rs | 158 +++++++++++++++++++++++++++++++----------------- 1 file changed, 103 insertions(+), 55 deletions(-) diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index c8256c5..2465e56 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -53,7 +53,7 @@ impl fmt::UpperExp for BigDecimalRef<'_> { impl fmt::Debug for BigDecimal { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if f.alternate() { - write!(f, "BigDecimal(\"{}e{:+}\")", self.int_val, -self.scale) + write!(f, "BigDecimal(\"{}e{:}\")", self.int_val, -self.scale) } else { write!(f, "BigDecimal(sign={:?}, scale={}, digits={:?})", @@ -98,9 +98,8 @@ fn format_full_scale( match f.precision() { // precision limits the number of digits - we have to round - Some(prec) if prec < digits.len() => { - debug_assert_ne!(prec, 0); - _apply_rounding_to_ascii_digits(&mut digits, &mut exp, prec, this.sign); + 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); }, _ => { @@ -169,7 +168,7 @@ fn format_full_scale( fn format_exponential( this: BigDecimalRef, f: &mut fmt::Formatter, - mut abs_int: String, + abs_int: String, e_symbol: &str, ) -> fmt::Result { // Steps: @@ -178,15 +177,18 @@ 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 - if abs_int.len() > 1 { + let mut exp = -this.scale; + let mut digits = abs_int.into_bytes(); + + if digits.len() > 1 { // only modify for precision if there is more than 1 decimal digit - if let Some(precision) = f.precision() { - // add 1 precision to consider first digit - // TODO: Should we round? - abs_int.truncate(precision + 1); + if let Some(prec) = f.precision() { + apply_rounding_to_ascii_digits(&mut digits, &mut exp, prec, this.sign); } } + let mut abs_int = String::from_utf8(digits).unwrap(); + // Determine the exponent value based on the scale // // # First case: the integer representation falls completely behind the @@ -228,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 i64 - this.scale - 1; + let exponent = abs_int.len() as i64 + exp - 1; if abs_int.len() > 1 { // only add decimal point if there is more than 1 decimal digit @@ -315,63 +317,65 @@ pub(crate) fn write_engineering_notation(n: &BigDecimal, out: &mut W) /// Round big-endian digits in ascii -fn _apply_rounding_to_ascii_digits(ascii_digits: &mut Vec, exp: &mut i64, prec: usize, sign: Sign) { - let digit_count_to_remove = ascii_digits.len() - prec; - *exp += digit_count_to_remove as i64; +fn apply_rounding_to_ascii_digits(ascii_digits: &mut Vec, exp: &mut i64, prec: usize, sign: Sign) { + if ascii_digits.len() < prec { + return; + } + + // shift exp to align with new length of digits + *exp += (ascii_digits.len() - prec) as i64; // true if all ascii_digits after precision are zeros let trailing_zeros = ascii_digits[prec + 1..].iter().all(|&d| d == b'0'); - let sig_ascii = ascii_digits[prec - 1]; - let s = sig_ascii - b'0'; - let i = ascii_digits[prec] - b'0'; - let r = Context::default().round_pair(sign, s, i, trailing_zeros); + let sig_digit = ascii_digits[prec - 1] - b'0'; + let insig_digit = ascii_digits[prec] - b'0'; + let rounded_digit = Context::default().round_pair(sign, sig_digit, insig_digit, trailing_zeros); // remove insignificant digits ascii_digits.truncate(prec - 1); // push rounded value - if r < 10 { - ascii_digits.push(r + b'0'); + if rounded_digit < 10 { + ascii_digits.push(rounded_digit + b'0'); return } - debug_assert_eq!(r, 10); + debug_assert_eq!(rounded_digit, 10); // push zero and carry-the-one ascii_digits.push(b'0'); - // loop through digits in reverse order - let mut digit_it = ascii_digits.iter_mut().rev().peekable(); - debug_assert_eq!(**digit_it.peek().unwrap(), sig_ascii); - - loop { - match digit_it.next() { - // carried one to 9 and continue - Some(d) if *d == b'9' => { - *d = b'0'; - continue; - }, - // add the carried one and return - Some(d) => { - *d += 1; - break; - }, - // case where all values were nines - None => { - // 'trim' the last zero - ascii_digits[0] = b'1'; - *exp += 1; - break; - } + // loop through digits in reverse order (skip the 0 we just pushed) + let digits = ascii_digits.iter_mut().rev().skip(1); + for digit in digits { + if *digit < b'9' { + // we've carried the one as far as it will go + *digit += 1; + return; } + + debug_assert_eq!(*digit, b'9'); + + // digit was a 9, set to zero and carry the one + // to the next digit + *digit = b'0'; } + + // at this point all digits have become zero + // just set significant digit to 1 and increase exponent + // + // eg: 9999e2 ~> 0000e2 ~> 1000e3 + // + ascii_digits[0] = b'1'; + *exp += 1; } #[cfg(test)] mod test { use super::*; + use paste::*; /// test case builder for mapping decimal-string to formatted-string /// define test_fmt_function! macro to test your function @@ -492,20 +496,64 @@ mod test { mod fmt_debug { use super::*; - macro_rules! test_fmt_function { - ($n:expr) => { format!("{:?}", $n) }; + macro_rules! impl_case { + ($name:ident: $input:literal => $expected:literal => $expected_alt:literal) => { + paste! { + #[test] + fn $name() { + let x: BigDecimal = $input.parse().unwrap(); + let y = format!("{:?}", x); + assert_eq!(y, $expected); + } + + #[test] + fn [< $name _alt >]() { + let x: BigDecimal = $input.parse().unwrap(); + let y = format!("{:#?}", x); + assert_eq!(y, $expected_alt); + } + } + } } - impl_case!(case_0: "0" => r#"BigDecimal("0")"#); - impl_case!(case_1: "1" => r#"BigDecimal("1")"#); - impl_case!(case_123_400: "123.400" => r#"BigDecimal("123.400")"#); - impl_case!(case_123_456: "123.456" => r#"BigDecimal("123.456")"#); - impl_case!(case_01_20: "01.20" => r#"BigDecimal("1.20")"#); - impl_case!(case_1_20: "1.20" => r#"BigDecimal("1.20")"#); - impl_case!(case_01_2e3: "01.2E3" => r#"BigDecimal("1200")"#); - impl_case!(case_avagadro: "6.02214076e1023" => r#"BigDecimal("602214076e1015")"#); + impl_case!(case_0: "0" => r#"BigDecimal(sign=NoSign, scale=0, digits=[])"# + => r#"BigDecimal("0e0")"#); + + impl_case!(case_n0: "-0" => r#"BigDecimal(sign=NoSign, scale=0, digits=[])"# + => r#"BigDecimal("0e0")"#); + + impl_case!(case_1: "1" => r#"BigDecimal(sign=Plus, scale=0, digits=[1])"# + => r#"BigDecimal("1e0")"#); + + impl_case!(case_123_400: "123.400" => r#"BigDecimal(sign=Plus, scale=3, digits=[123400])"# + => r#"BigDecimal("123400e-3")"#); + + impl_case!(case_123_4en2: "123.4e-2" => r#"BigDecimal(sign=Plus, scale=3, digits=[1234])"# + => r#"BigDecimal("1234e-3")"#); + + impl_case!(case_123_456: "123.456" => r#"BigDecimal(sign=Plus, scale=3, digits=[123456])"# + => r#"BigDecimal("123456e-3")"#); + + impl_case!(case_01_20: "01.20" => r#"BigDecimal(sign=Plus, scale=2, digits=[120])"# + => r#"BigDecimal("120e-2")"#); + + impl_case!(case_1_20: "1.20" => r#"BigDecimal(sign=Plus, scale=2, digits=[120])"# + => r#"BigDecimal("120e-2")"#); + impl_case!(case_01_2e3: "01.2E3" => r#"BigDecimal(sign=Plus, scale=-2, digits=[12])"# + => r#"BigDecimal("12e2")"#); + + impl_case!(case_avagadro: "6.02214076e1023" => r#"BigDecimal(sign=Plus, scale=-1015, digits=[602214076])"# + => r#"BigDecimal("602214076e1015")"#); + + impl_case!(case_1e99999999999999 : "1e99999999999999" => r#"BigDecimal(sign=Plus, scale=-99999999999999, digits=[1])"# + => r#"BigDecimal("1e99999999999999")"#); + + impl_case!(case_n144d3308279 : "-144.3308279" => r#"BigDecimal(sign=Minus, scale=7, digits=[1443308279])"# + => r#"BigDecimal("-1443308279e-7")"#); - impl_case!(case_1e99999999999999 : "1e99999999999999" => r#"BigDecimal("1e99999999999999")"#); + impl_case!(case_n349983058835858339619e2 : "-349983058835858339619e2" + => r#"BigDecimal(sign=Minus, scale=-2, digits=[17941665509086410531, 18])"# + => r#"BigDecimal("-349983058835858339619e2")"#); } mod write_scientific_notation { From b0efb6350e2f684fed21592c4b08cc28fa0896fc Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sat, 20 Jan 2024 19:55:00 -0500 Subject: [PATCH 33/54] Update formatting tests to new standards --- src/impl_fmt.rs | 149 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 114 insertions(+), 35 deletions(-) diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index 2465e56..b65b0de 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -431,20 +431,113 @@ mod test { impl_case!(case_0d000123: "0.000123" => "1.23E-4"); impl_case!(case_123d: "123." => "123"); - impl_case!(case_123de1: "123.e1" => "1.23E3"); + impl_case!(case_123de1: "123.e1" => "1.23E+3"); + } + + mod fmt_options { + use super::*; + + macro_rules! impl_case { + ($name:ident: $fmt:literal => $expected:literal) => { + #[test] + fn $name() { + let x = test_input(); + let y = format!($fmt, x); + assert_eq!(y, $expected); + } + }; + } + + mod dec_1 { + use super::*; + + fn test_input() -> BigDecimal { + "1".parse().unwrap() + } + + 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"); + } + + mod dec_123456 { + use super::*; + + fn test_input() -> BigDecimal { + "123456".parse().unwrap() + } + + 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"); + } + + mod dec_9999999 { + use super::*; + + fn test_input() -> BigDecimal { + "9999999".parse().unwrap() + } + + impl_case!(fmt_d4: "{:.4}" => "1.000E+7"); + impl_case!(fmt_d8: "{:.8}" => "9999999"); + } + + mod dec_19073d97235939614856 { + use super::*; + + fn test_input() -> BigDecimal { + "19073.97235939614856".parse().unwrap() + } + + 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 "); + } + + mod dec_491326en12 { + use super::*; + + fn test_input() -> BigDecimal { + "491326e-12".parse().unwrap() + } + + 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 "); + } } #[test] fn test_fmt() { let vals = vec![ - // 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" )), + // 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 " )), ]; for (i, scale, results) in vals { let x = BigDecimal::new(num_bigint::BigInt::from(i), scale); @@ -452,44 +545,30 @@ 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.1}", x), results.4); - assert_eq!(format!("{:<4.1}", x), results.5); + assert_eq!(format!("{:+05.7}", x), results.4); + assert_eq!(format!("{:<6.4}", x), results.5); } } #[test] fn test_fmt_with_large_values() { let vals = vec![ - // b s ( {} {:.1} {:.4} {:4.1} {:+05.1} {:<4.1} + // 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, ("1E10000", "1E10000", "1E10000", "1E10000", "+1E10000", "1E10000")), - // Numbers with many digits - (1234506789, 5, ( - "12345.06789", - "12345.0", - "12345.0678", - "12345.0", - "+12345.0", - "12345.0" - )), - (1234506789, -5, ( - "123450678900000", - "123450678900000.0", - "123450678900000.0000", - "123450678900000.0", - "+123450678900000.0", - "123450678900000.0" - )), + (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!("{:.4}", x), results.2, "digits={} scale={}", i, scale); - assert_eq!(format!("{:4.1}", x), results.3, "digits={} scale={}", i, scale); - assert_eq!(format!("{:+05.1}", x), results.4, "digits={} scale={}", i, scale); - assert_eq!(format!("{:<4.1}", x), results.5, "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); } } From 2bf132f9dc5e55cbaf46a89c911ae6456a6847c1 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sat, 20 Jan 2024 20:04:58 -0500 Subject: [PATCH 34/54] Add script to generate formatting tests --- scripts/make-fmt-format-tests.py | 175 +++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100755 scripts/make-fmt-format-tests.py diff --git a/scripts/make-fmt-format-tests.py b/scripts/make-fmt-format-tests.py new file mode 100755 index 0000000..bd2dcf4 --- /dev/null +++ b/scripts/make-fmt-format-tests.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python +# +# Generate Rust code given Decimals and formatting strings +# + +import textwrap +from decimal import * + + +FORMATS = [ + "{}", + "{:+05.1}", + "{:.1}", + "{:.4}", + "{:4.1}", + "{:>4.3}", + "{:>4.4}", + "{:<4.1}", +] + +# FORMATS = [ +# "{:.4}", +# "{:.8}", +# ] + +FORMATS = [ + "{}", + "{:+05.7}", + "{:.3}", + "{:0.4}", + "{:4.1}", + "{:>8.3}", + "{:>8.4}", + "{:<8.1}", +] + + +# Decimal strings to the testing formats +INPUTS = { + "1": [ + "{}", + "{:.1}", + "{:.4}", + "{:4.1}", + "{:>4.1}", + "{:<4.1}", + "{:+05.1}", + ], + "123456": [ + "{}", + "{:+05.1}", + "{:.1}", + "{:.4}", + "{:4.1}", + "{:>4.3}", + "{:>4.4}", + "{:<4.1}", + ], + "9999999": [ + "{:.4}", + "{:.8}", + ], + "19073.97235939614856": [ + "{}", + "{:+05.7}", + "{:.3}", + "{:0.4}", + "{:4.1}", + "{:>8.3}", + "{:>8.4}", + "{:<8.1}", + ], + "491326e-12": [ + "{}", + "{:+015.7}", + "{:.3}", + "{:0.4}", + "{:4.1}", + "{:>8.3}", + "{:>8.4}", + "{:<8.1}", + ], +} + + +def main(): + for dec_str, formats in INPUTS.items(): + dec = Decimal(dec_str) + mod_name = "dec_%s" % gen_name_from_dec(dec_str) + + formats_and_outputs = [ + (fmt, gen_name_from_fmt(fmt), fmt.format(dec)) for fmt in formats + ] + + max_name_len = max(len(name) + 7 for _, name, _ in formats_and_outputs) + + max_len_fmt = max(len(fmt) + 5 for fmt, _, _ in formats_and_outputs) + + max_len_name_and_fmt = max( + len(fmt) + len(name) for fmt, name, _ in formats_and_outputs + ) + + lines = [] + for fmt, name, value in formats_and_outputs: + spacer = " " * (max_len_name_and_fmt - (len(fmt) + len(name)) + 2) + # fmt = f'"{fmt}" =>'.rjust(max_len_fmt) + # fmt = fmt.rjust(max_len_fmt) + lines.append(f'impl_case!(fmt_{name}:{spacer}"{fmt}" => "{value}");') + # print('impl_case!(fmt_%s: "%s" => "%s");' % (name, fmt, )) + + body = textwrap.indent("\n".join(lines), " ") + text = textwrap.dedent( + f""" + mod {mod_name} {{ + use super::*; + + fn test_input() -> BigDecimal {{ + "{dec_str}".parse().unwrap() + }} + + %s + }}""" + ) + + print(text % body) + continue + + print( + "\n".join( + [ + f"mod {mod_name} {{", + "use super::*;", + f'fn test_input() -> BigDecimal {{ "{dec_str}".parse().unwrap() }}', + body, + "}", + ] + ) + ) + + +# x = Decimal("1") +x = 123456.0 + + +def gen_name_from_dec(s: str) -> str: + return "".join(map_fmt_char_to_name_char(c) for c in s) + + +def gen_name_from_fmt(fmt: str) -> str: + if fmt == "{}": + return "default" + else: + return "".join(map_fmt_char_to_name_char(c) for c in fmt[2:-1]) + + +def map_fmt_char_to_name_char(c: str) -> str: + match c: + case ".": + return "d" + case "+": + return "p" + case "-": + return "n" + case "<": + return "l" + case ">": + return "r" + case ":": + return "c" + case _: + return str(c) + + +if __name__ == "__main__": + main() From 379e1c20e77f8702414dfc05ab6bcc927371b283 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sat, 20 Jan 2024 20:32:16 -0500 Subject: [PATCH 35/54] Fix 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 e8bdac7..6ba601d 100644 --- a/src/impl_serde.rs +++ b/src/impl_serde.rs @@ -101,7 +101,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_1e12: "1e12" => "1000000000000"); + impl_case!(case_10e11: "10e11" => "1.0E+12"); impl_case!(case_d25: ".25" => "0.25"); impl_case!(case_12d34e1: "12.34e1" => "123.4"); impl_case!(case_40d0010: "40.0010" => "40.0010"); From 6a9a3d84ab527103b70e0fe85337fb06d3f32951 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sun, 21 Jan 2024 12:35:31 -0500 Subject: [PATCH 36/54] Clean code and add docstrings to make-fmt-format-tests.py --- scripts/make-fmt-format-tests.py | 122 ++++++++++++------------------- 1 file changed, 45 insertions(+), 77 deletions(-) diff --git a/scripts/make-fmt-format-tests.py b/scripts/make-fmt-format-tests.py index bd2dcf4..8aa08fd 100755 --- a/scripts/make-fmt-format-tests.py +++ b/scripts/make-fmt-format-tests.py @@ -3,38 +3,10 @@ # Generate Rust code given Decimals and formatting strings # +import sys import textwrap from decimal import * - -FORMATS = [ - "{}", - "{:+05.1}", - "{:.1}", - "{:.4}", - "{:4.1}", - "{:>4.3}", - "{:>4.4}", - "{:<4.1}", -] - -# FORMATS = [ -# "{:.4}", -# "{:.8}", -# ] - -FORMATS = [ - "{}", - "{:+05.7}", - "{:.3}", - "{:0.4}", - "{:4.1}", - "{:>8.3}", - "{:>8.4}", - "{:<8.1}", -] - - # Decimal strings to the testing formats INPUTS = { "1": [ @@ -84,76 +56,72 @@ def main(): - for dec_str, formats in INPUTS.items(): - dec = Decimal(dec_str) - mod_name = "dec_%s" % gen_name_from_dec(dec_str) - - formats_and_outputs = [ - (fmt, gen_name_from_fmt(fmt), fmt.format(dec)) for fmt in formats - ] + from argparse import ArgumentParser, FileType - max_name_len = max(len(name) + 7 for _, name, _ in formats_and_outputs) + parser = ArgumentParser(description="Create test modules for src/impl_fmt.rs") + parser.add_argument("-o", "--output", nargs="?", default="-", type=FileType("w")) + args = parser.parse_args() - max_len_fmt = max(len(fmt) + 5 for fmt, _, _ in formats_and_outputs) + for dec_str, formats in INPUTS.items(): + src = make_test_module_src(dec_str, formats) + args.output.write(src) - max_len_name_and_fmt = max( - len(fmt) + len(name) for fmt, name, _ in formats_and_outputs - ) + return 0 - lines = [] - for fmt, name, value in formats_and_outputs: - spacer = " " * (max_len_name_and_fmt - (len(fmt) + len(name)) + 2) - # fmt = f'"{fmt}" =>'.rjust(max_len_fmt) - # fmt = fmt.rjust(max_len_fmt) - lines.append(f'impl_case!(fmt_{name}:{spacer}"{fmt}" => "{value}");') - # print('impl_case!(fmt_%s: "%s" => "%s");' % (name, fmt, )) - body = textwrap.indent("\n".join(lines), " ") - text = textwrap.dedent( - f""" - mod {mod_name} {{ - use super::*; +def make_test_module_src(dec_str: Decimal, formats: list[str]) -> str: + """ + Return Rust module source which tests given decimal aginst given formats + """ + dec = Decimal(dec_str) + mod_name = "dec_%s" % gen_name_from_dec(dec_str) - fn test_input() -> BigDecimal {{ - "{dec_str}".parse().unwrap() - }} + formats_and_outputs = [ + (fmt, gen_name_from_fmt(fmt), fmt.format(dec)) for fmt in formats + ] - %s - }}""" - ) + max_len_name_and_fmt = max( + len(fmt) + len(name) for fmt, name, _ in formats_and_outputs + ) - print(text % body) - continue + lines = [] + for fmt, name, value in formats_and_outputs: + spacer = " " * (max_len_name_and_fmt - (len(fmt) + len(name)) + 2) + lines.append(f'impl_case!(fmt_{name}:{spacer}"{fmt}" => "{value}");') - print( - "\n".join( - [ - f"mod {mod_name} {{", - "use super::*;", - f'fn test_input() -> BigDecimal {{ "{dec_str}".parse().unwrap() }}', - body, - "}", - ] - ) - ) + body = textwrap.indent("\n".join(lines), " ") + text = textwrap.dedent( + f""" + mod {mod_name} {{ + use super::*; + fn test_input() -> BigDecimal {{ + "{dec_str}".parse().unwrap() + }} -# x = Decimal("1") -x = 123456.0 + %s + }} + """ + ) + return text % body -def gen_name_from_dec(s: str) -> str: - return "".join(map_fmt_char_to_name_char(c) for c in s) +def gen_name_from_dec(dec_str: str) -> str: + """Given decimal input string return valid function/module name""" + return "".join(map_fmt_char_to_name_char(c) for c in dec_str) def gen_name_from_fmt(fmt: str) -> str: + """Given decimal input string return valid function/module name""" if fmt == "{}": return "default" else: + assert fmt.startswith("{:") and fmt.endswith("}") return "".join(map_fmt_char_to_name_char(c) for c in fmt[2:-1]) def map_fmt_char_to_name_char(c: str) -> str: + """Turn special characters into identifiers""" match c: case ".": return "d" @@ -172,4 +140,4 @@ def map_fmt_char_to_name_char(c: str) -> str: if __name__ == "__main__": - main() + sys.exit(main()) From a6e72f0d33154fcd890ffc487ceefaad3cd2273e Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Wed, 17 Jan 2024 02:47:57 -0500 Subject: [PATCH 37/54] Update README with more info on compile-time variables --- README.md | 81 ++++++++++++++++++++++++++++++++++++++++++++++--- src/impl_fmt.rs | 2 +- 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2644046..b05e7c1 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,10 @@ Arbitary-precision decimal numbers implemented in pure Rust. +## Community + +Join the conversation on Zulip: https://bigdecimal-rs.zulipchat.com + ## Usage Add bigdecimal as a dependency to your `Cargo.toml` file: @@ -41,6 +45,19 @@ sqrt(2) = 1.41421356237309504880168872420969807856967187537694807317667973799073 ``` +### 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 | ++-------------------------------------------------+------------+ + + #### Default precision Default precision may be set at compile time with the environment variable `RUST_BIGDECIMAL_DEFAULT_PRECISION`. @@ -56,6 +73,65 @@ The user will have to manually trim the number of digits after calculations to r A new set of methods with explicit precision and rounding modes is being worked on, but even after those are introduced the default precision will have to be used as the implicit value. + +#### Rounding mode + +The default Context uses this value for rounding. +Valid values are the variants of the [RoundingMode] enum. + +Defaults to `HalfEven`. + +[RoundingMode]: https://docs.rs/bigdecimal/latest/bigdecimal/rounding/enum.RoundingMode.html + + +#### Exponential Format Threshold + +The maximum number of leading zeros after the decimal place before +the formatter uses exponential form (i.e. scientific notation). + +There is currently no mechanism to change this during runtime. +If you know of a good solution for number formatting in Rust, please let me know! + + +#### Example Compile time configuration + +Given the program: + +```rust +fn main() { + let n = BigDecimal::from(700); + println!("1/{n} = {}", n.inverse()); +} +``` + +Compiling with different environment variables prints different results + +``` +$ export BIG_DECIMAL_DEFAULT_PRECISION=8 +$ cargo run +1/700 = 0.0014285714 + +$ export RUST_BIGDECIMAL_DEFAULT_PRECISION=5 +$ cargo run +1/700 = 0.0014286 + +$ export RUST_BIGDECIMAL_DEFAULT_ROUNDING_MODE=Down +$ cargo run +1/700 = 0.0014285 + +$ export RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD=2 +$ cargo run +1/700 = 1.4285E-3 +``` + +> [!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. +> +> This is for flexibility and performance. + + ## Improvements Work is being done on this codebase again and there are many features @@ -82,8 +158,3 @@ Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. - - -## Community - -Join the conversation on Zulip: https://bigdecimal-rs.zulipchat.com diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index b65b0de..d2c50e0 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -5,7 +5,7 @@ use crate::*; use stdlib::fmt::Write; -// const EXPONENTIAL_FORMAT_THRESHOLD: i64 = ${RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD} or 25; +// const EXPONENTIAL_FORMAT_THRESHOLD: i64 = ${RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD} or 5; include!(concat!(env!("OUT_DIR"), "/exponential_format_threshold.rs")); From f35ed99a4a4aa2e2062a0b39296e622371d1d582 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sun, 21 Jan 2024 13:14:23 -0500 Subject: [PATCH 38/54] Add roundtrip tests for {scientific,engineering}-notation methods --- src/impl_fmt.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index d2c50e0..2d299e1 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -689,7 +689,7 @@ mod test { mod proptests { use super::*; use paste::paste; - use proptest::*; + use proptest::prelude::*; macro_rules! impl_parsing_test { ($t:ty) => { @@ -732,4 +732,26 @@ mod proptests { impl_parsing_test!(from-float f32); impl_parsing_test!(from-float f64); + + proptest! { + #![proptest_config(ProptestConfig::with_cases(1000))] + + #[test] + fn scientific_notation_roundtrip(f: f64) { + prop_assume!(!f.is_nan() && !f.is_infinite()); + let n = BigDecimal::from_f64(f).unwrap(); + let s = n.to_scientific_notation(); + let m: BigDecimal = s.parse().unwrap(); + prop_assert_eq!(n, m); + } + + #[test] + fn engineering_notation_roundtrip(f: f64) { + prop_assume!(!f.is_nan() && !f.is_infinite()); + let n = BigDecimal::from_f64(f).unwrap(); + let s = n.to_engineering_notation(); + let m: BigDecimal = s.parse().unwrap(); + prop_assert_eq!(n, m); + } + } } From 9a0efd05f1d9ea512622eb4e016f845278814826 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sun, 21 Jan 2024 13:15:11 -0500 Subject: [PATCH 39/54] Add help/usage option to bigdecimal-property-tests script --- scripts/bigdecimal-property-tests | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/scripts/bigdecimal-property-tests b/scripts/bigdecimal-property-tests index 8196fcb..7141277 100755 --- a/scripts/bigdecimal-property-tests +++ b/scripts/bigdecimal-property-tests @@ -5,12 +5,27 @@ # Tests are defined in src/lib.tests.property-test.rs # +SED=$(command -v gsed || command -v sed) +if [ -z "$SED" ]; then + echo "This program requires sed" + exit 1 +fi + +usage() { + echo "Usage: $0 [enable|disable|test|run ]" + echo "" + echo " enable: Enable property tests by removing comments in Cargo.toml & build.rs" + echo " disable: Restore property-tests comments in Cargo.toml & build.rs" + echo " test: Runs 'cargo test' between enabling & disabling property tests" + echo "run : Run user supplied command between enabling & disabling property tests" +} + enable_property_tests() { # enable property-test dependencies in Cargo - sed -i.bak -e 's|# PROPERTY-TESTS: ||' Cargo.toml + ${SED} -i.bak -e 's|# PROPERTY-TESTS: ||' Cargo.toml # add the property-test configuration in build.rs - sed -i.bak -e 's|// ::PROPERTY-TESTS:: ||' build.rs + ${SED} -i.bak -e 's|// ::PROPERTY-TESTS:: ||' build.rs } @@ -21,7 +36,7 @@ restore_disabled_property_tests() { } -DEFAULT_CMD=run +DEFAULT_CMD=help CMD=${1:-$DEFAULT_CMD} shift @@ -46,4 +61,8 @@ case "${CMD}" in disable) restore_disabled_property_tests ;; + + *) + usage + ;; esac From 0d8a55312e8e8934d5332531c88f654526620a24 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Sun, 21 Jan 2024 13:40:00 -0500 Subject: [PATCH 40/54] Enable property tests in circle-ci and cache via lockfile --- .circleci/config.yml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index fba6b07..01f2348 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -15,6 +15,9 @@ jobs: rust-features: type: string default: "--all-targets" + proptest-enable: + type: boolean + default: false docker: - image: rust:<< parameters.rust-version >>-<< parameters.debian-version >> environment: @@ -25,9 +28,18 @@ jobs: - run: name: Rust Version command: rustc --version; cargo --version + - when: + condition: << parameters.proptest-enable >> + steps: + - run: + name: Enable Running Property Tests + command: scripts/bigdecimal-property-tests enable + - run: + name: Generate cargo.lock + command: cargo generate-lockfile - restore_cache: keys: - - bigdecimal-cargo-<< parameters.rust-version >>-{{ checksum "Cargo.toml" }} + - bigdecimal-cargo-<< parameters.rust-version >>-{{ checksum "Cargo.lock" }} - bigdecimal-cargo- - run: name: Check @@ -35,7 +47,7 @@ jobs: - save_cache: paths: - /usr/local/cargo - key: bigdecimal-cargo-<< parameters.rust-version >>-{{ checksum "Cargo.toml" }} + key: bigdecimal-cargo-<< parameters.rust-version >>-{{ checksum "Cargo.lock" }} - run: name: Build command: cargo build << parameters.rust-features >> @@ -137,6 +149,7 @@ workflows: - build-and-test: name: build-and-test:latest:serde rust-features: "--all-targets --features='serde'" + proptest-enable: true - build-and-test: name: build-and-test:no_std From f5746390c196803d4c7b4ed17f86b3a729dd3044 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Mon, 12 Feb 2024 18:45:29 -0500 Subject: [PATCH 41/54] Move rounding tests into separate file --- src/rounding.rs | 271 +----------------------------------------- src/rounding.tests.rs | 271 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 272 insertions(+), 270 deletions(-) create mode 100644 src/rounding.tests.rs diff --git a/src/rounding.rs b/src/rounding.rs index bbce7f1..0a39655 100644 --- a/src/rounding.rs +++ b/src/rounding.rs @@ -199,273 +199,4 @@ impl RoundingMode { #[cfg(test)] -#[allow(non_snake_case)] -mod test_round_pair { - use paste::paste; - use super::*; - - macro_rules! impl_test { - ( $($mode:ident),+ => $expected:literal) => { - $( - paste! { - #[test] - fn [< mode_ $mode >]() { - let (pair, sign, trailing_zeros) = test_input(); - let mode = self::RoundingMode::$mode; - let result = mode.round_pair(sign, pair, trailing_zeros); - assert_eq!(result, $expected); - } - } - )* - } - } - - macro_rules! define_test_input { - ( - $lhs:literal . $rhs:literal $($t:tt)* ) => { - define_test_input!(sign=Sign::Minus, pair=($lhs, $rhs), $($t)*); - }; - ( $lhs:literal . $rhs:literal $($t:tt)*) => { - define_test_input!(sign=Sign::Plus, pair=($lhs, $rhs), $($t)*); - }; - ( sign=$sign:expr, pair=$pair:expr, ) => { - define_test_input!(sign=$sign, pair=$pair, trailing_zeros=true); - }; - ( sign=$sign:expr, pair=$pair:expr, 000x ) => { - define_test_input!(sign=$sign, pair=$pair, trailing_zeros=false); - }; - ( sign=$sign:expr, pair=$pair:expr, trailing_zeros=$trailing_zeros:literal ) => { - fn test_input() -> ((u8, u8), Sign, bool) { ($pair, $sign, $trailing_zeros) } - }; - } - - mod case_0_1 { - use super::*; - - define_test_input!(0 . 1); - - impl_test!(Up, Ceiling => 1); - impl_test!(Down, Floor, HalfUp, HalfDown, HalfEven => 0); - } - - mod case_neg_0_1 { - use super::*; - - define_test_input!(-0 . 1); - - impl_test!(Up, Floor => 1); - impl_test!(Down, Ceiling, HalfUp, HalfDown, HalfEven => 0); - } - - mod case_0_5 { - use super::*; - - define_test_input!( 0 . 5 ); - - impl_test!(Up, Ceiling, HalfUp => 1); - impl_test!(Down, Floor, HalfDown, HalfEven => 0); - } - - mod case_neg_0_5 { - use super::*; - - define_test_input!(-0 . 5); - - impl_test!(Up, Floor, HalfUp => 1); - impl_test!(Down, Ceiling, HalfDown, HalfEven => 0); - } - - mod case_0_5_000x { - use super::*; - - // ...000x indicates a non-zero trailing digit; affects behavior of rounding N.0 and N.5 - define_test_input!(0 . 5 000x); - - impl_test!(Up, Ceiling, HalfUp, HalfDown, HalfEven => 1); - impl_test!(Down, Floor => 0); - } - - mod case_neg_0_5_000x { - use super::*; - - define_test_input!(-0 . 5 000x); - - impl_test!(Up, Floor, HalfUp, HalfDown, HalfEven => 1); - impl_test!(Down, Ceiling => 0); - } - - mod case_0_7 { - use super::*; - - define_test_input!(0 . 7); - - impl_test!(Up, Ceiling, HalfUp, HalfDown, HalfEven => 1); - impl_test!(Down, Floor => 0); - } - - mod case_neg_0_7 { - use super::*; - - define_test_input!(-0 . 7); - - impl_test!(Up, Floor, HalfUp, HalfDown, HalfEven => 1); - impl_test!(Down, Ceiling => 0); - } - - mod case_neg_4_3_000x { - use super::*; - - define_test_input!(-4 . 3 000x); - - impl_test!(Up, Floor => 5); - impl_test!(Down, Ceiling, HalfUp, HalfDown, HalfEven => 4); - } - - - mod case_9_5_000x { - use super::*; - - define_test_input!(9 . 5 000x); - - impl_test!(Up, Ceiling, HalfDown, HalfUp, HalfEven => 10); - impl_test!(Down, Floor => 9); - } - - mod case_9_5 { - use super::*; - - define_test_input!(9 . 5); - - impl_test!(Up, Ceiling, HalfUp, HalfEven => 10); - impl_test!(Down, Floor, HalfDown => 9); - } - - mod case_8_5 { - use super::*; - - define_test_input!(8 . 5); - - impl_test!(Up, Ceiling, HalfUp => 9); - impl_test!(Down, Floor, HalfDown, HalfEven => 8); - } - - mod case_neg_6_5 { - use super::*; - - define_test_input!(-6 . 5); - - impl_test!(Up, Floor, HalfUp => 7); - impl_test!(Down, Ceiling, HalfDown, HalfEven => 6); - } - - mod case_neg_6_5_000x { - use super::*; - - define_test_input!(-6 . 5 000x); - - impl_test!(Up, Floor, HalfUp, HalfDown, HalfEven => 7); - impl_test!(Down, Ceiling => 6); - } - - mod case_3_0 { - use super::*; - - define_test_input!(3 . 0); - - impl_test!(Up, Down, Ceiling, Floor, HalfUp, HalfDown, HalfEven => 3); - } - - mod case_3_0_000x { - use super::*; - - define_test_input!(3 . 0 000x); - - impl_test!(Up, Ceiling => 4); - impl_test!(Down, Floor, HalfUp, HalfDown, HalfEven => 3); - } - - mod case_neg_2_0 { - use super::*; - - define_test_input!(-2 . 0); - - impl_test!(Up, Down, Ceiling, Floor, HalfUp, HalfDown, HalfEven => 2); - } - - mod case_neg_2_0_000x { - use super::*; - - define_test_input!(-2 . 0 000x); - - impl_test!(Up, Floor => 3); - impl_test!(Down, Ceiling, HalfUp, HalfDown, HalfEven => 2); - } -} - - -#[cfg(test)] -#[allow(non_snake_case)] -mod test_round_u32 { - use paste::paste; - use super::*; - - macro_rules! impl_test { - ( $pos:literal :: $($mode:ident),+ => $expected:literal) => { - $( - paste! { - #[test] - fn [< digit_ $pos _mode_ $mode >]() { - let (value, sign, trailing_zeros) = test_input(); - let mode = self::RoundingMode::$mode; - let pos = stdlib::num::NonZeroU8::new($pos as u8).unwrap(); - let result = mode.round_u32(pos, sign, value, trailing_zeros); - assert_eq!(result, $expected); - } - } - )* - } - } - - macro_rules! define_test_input { - ( - $value:literal $($t:tt)* ) => { - define_test_input!(sign=Sign::Minus, value=$value $($t)*); - }; - ( $value:literal $($t:tt)* ) => { - define_test_input!(sign=Sign::Plus, value=$value $($t)*); - }; - ( sign=$sign:expr, value=$value:literal ...000x ) => { - define_test_input!(sign=$sign, value=$value, trailing_zeros=false); - }; - ( sign=$sign:expr, value=$value:literal ) => { - define_test_input!(sign=$sign, value=$value, trailing_zeros=true); - }; - ( sign=$sign:expr, value=$value:expr, trailing_zeros=$trailing_zeros:literal ) => { - fn test_input() -> (u32, Sign, bool) { ($value, $sign, $trailing_zeros) } - }; - } - - mod case_13950000 { - use super::*; - - define_test_input!(13950000); - - impl_test!(3 :: Up => 13950000); - impl_test!(5 :: Up, Ceiling, HalfUp, HalfEven => 14000000); - impl_test!(5 :: Down, HalfDown => 13900000); - } - - mod case_neg_35488622_000x { - use super::*; - - // ...000x indicates non-zero trailing digit - define_test_input!(-35488622 ...000x); - - impl_test!(1 :: Up => 35488630); - impl_test!(1 :: Down => 35488620); - impl_test!(2 :: Up => 35488700); - impl_test!(2 :: Down => 35488600); - impl_test!(7 :: Up, Floor => 40000000); - impl_test!(7 :: Down, Ceiling => 30000000); - impl_test!(8 :: Up => 100000000); - impl_test!(8 :: Down => 0); - } -} +include!("rounding.tests.rs"); diff --git a/src/rounding.tests.rs b/src/rounding.tests.rs new file mode 100644 index 0000000..f530541 --- /dev/null +++ b/src/rounding.tests.rs @@ -0,0 +1,271 @@ + +#[allow(non_snake_case)] +mod test_round_pair { + use paste::paste; + use super::*; + + macro_rules! impl_test { + ( $($mode:ident),+ => $expected:literal) => { + $( + paste! { + #[test] + fn [< mode_ $mode >]() { + let (pair, sign, trailing_zeros) = test_input(); + let mode = self::RoundingMode::$mode; + let result = mode.round_pair(sign, pair, trailing_zeros); + assert_eq!(result, $expected); + } + } + )* + } + } + + macro_rules! define_test_input { + ( - $lhs:literal . $rhs:literal $($t:tt)* ) => { + define_test_input!(sign=Sign::Minus, pair=($lhs, $rhs), $($t)*); + }; + ( $lhs:literal . $rhs:literal $($t:tt)*) => { + define_test_input!(sign=Sign::Plus, pair=($lhs, $rhs), $($t)*); + }; + ( sign=$sign:expr, pair=$pair:expr, ) => { + define_test_input!(sign=$sign, pair=$pair, trailing_zeros=true); + }; + ( sign=$sign:expr, pair=$pair:expr, 000x ) => { + define_test_input!(sign=$sign, pair=$pair, trailing_zeros=false); + }; + ( sign=$sign:expr, pair=$pair:expr, trailing_zeros=$trailing_zeros:literal ) => { + fn test_input() -> ((u8, u8), Sign, bool) { ($pair, $sign, $trailing_zeros) } + }; + } + + mod case_0_1 { + use super::*; + + define_test_input!(0 . 1); + + impl_test!(Up, Ceiling => 1); + impl_test!(Down, Floor, HalfUp, HalfDown, HalfEven => 0); + } + + mod case_neg_0_1 { + use super::*; + + define_test_input!(-0 . 1); + + impl_test!(Up, Floor => 1); + impl_test!(Down, Ceiling, HalfUp, HalfDown, HalfEven => 0); + } + + mod case_0_5 { + use super::*; + + define_test_input!( 0 . 5 ); + + impl_test!(Up, Ceiling, HalfUp => 1); + impl_test!(Down, Floor, HalfDown, HalfEven => 0); + } + + mod case_neg_0_5 { + use super::*; + + define_test_input!(-0 . 5); + + impl_test!(Up, Floor, HalfUp => 1); + impl_test!(Down, Ceiling, HalfDown, HalfEven => 0); + } + + mod case_0_5_000x { + use super::*; + + // ...000x indicates a non-zero trailing digit; affects behavior of rounding N.0 and N.5 + define_test_input!(0 . 5 000x); + + impl_test!(Up, Ceiling, HalfUp, HalfDown, HalfEven => 1); + impl_test!(Down, Floor => 0); + } + + mod case_neg_0_5_000x { + use super::*; + + define_test_input!(-0 . 5 000x); + + impl_test!(Up, Floor, HalfUp, HalfDown, HalfEven => 1); + impl_test!(Down, Ceiling => 0); + } + + mod case_0_7 { + use super::*; + + define_test_input!(0 . 7); + + impl_test!(Up, Ceiling, HalfUp, HalfDown, HalfEven => 1); + impl_test!(Down, Floor => 0); + } + + mod case_neg_0_7 { + use super::*; + + define_test_input!(-0 . 7); + + impl_test!(Up, Floor, HalfUp, HalfDown, HalfEven => 1); + impl_test!(Down, Ceiling => 0); + } + + mod case_neg_4_3_000x { + use super::*; + + define_test_input!(-4 . 3 000x); + + impl_test!(Up, Floor => 5); + impl_test!(Down, Ceiling, HalfUp, HalfDown, HalfEven => 4); + } + + + mod case_9_5_000x { + use super::*; + + define_test_input!(9 . 5 000x); + + impl_test!(Up, Ceiling, HalfDown, HalfUp, HalfEven => 10); + impl_test!(Down, Floor => 9); + } + + mod case_9_5 { + use super::*; + + define_test_input!(9 . 5); + + impl_test!(Up, Ceiling, HalfUp, HalfEven => 10); + impl_test!(Down, Floor, HalfDown => 9); + } + + mod case_8_5 { + use super::*; + + define_test_input!(8 . 5); + + impl_test!(Up, Ceiling, HalfUp => 9); + impl_test!(Down, Floor, HalfDown, HalfEven => 8); + } + + mod case_neg_6_5 { + use super::*; + + define_test_input!(-6 . 5); + + impl_test!(Up, Floor, HalfUp => 7); + impl_test!(Down, Ceiling, HalfDown, HalfEven => 6); + } + + mod case_neg_6_5_000x { + use super::*; + + define_test_input!(-6 . 5 000x); + + impl_test!(Up, Floor, HalfUp, HalfDown, HalfEven => 7); + impl_test!(Down, Ceiling => 6); + } + + mod case_3_0 { + use super::*; + + define_test_input!(3 . 0); + + impl_test!(Up, Down, Ceiling, Floor, HalfUp, HalfDown, HalfEven => 3); + } + + mod case_3_0_000x { + use super::*; + + define_test_input!(3 . 0 000x); + + impl_test!(Up, Ceiling => 4); + impl_test!(Down, Floor, HalfUp, HalfDown, HalfEven => 3); + } + + mod case_neg_2_0 { + use super::*; + + define_test_input!(-2 . 0); + + impl_test!(Up, Down, Ceiling, Floor, HalfUp, HalfDown, HalfEven => 2); + } + + mod case_neg_2_0_000x { + use super::*; + + define_test_input!(-2 . 0 000x); + + impl_test!(Up, Floor => 3); + impl_test!(Down, Ceiling, HalfUp, HalfDown, HalfEven => 2); + } +} + + +#[cfg(test)] +#[allow(non_snake_case)] +mod test_round_u32 { + use paste::paste; + use super::*; + + macro_rules! impl_test { + ( $pos:literal :: $($mode:ident),+ => $expected:literal) => { + $( + paste! { + #[test] + fn [< digit_ $pos _mode_ $mode >]() { + let (value, sign, trailing_zeros) = test_input(); + let mode = self::RoundingMode::$mode; + let pos = stdlib::num::NonZeroU8::new($pos as u8).unwrap(); + let result = mode.round_u32(pos, sign, value, trailing_zeros); + assert_eq!(result, $expected); + } + } + )* + } + } + + macro_rules! define_test_input { + ( - $value:literal $($t:tt)* ) => { + define_test_input!(sign=Sign::Minus, value=$value $($t)*); + }; + ( $value:literal $($t:tt)* ) => { + define_test_input!(sign=Sign::Plus, value=$value $($t)*); + }; + ( sign=$sign:expr, value=$value:literal ...000x ) => { + define_test_input!(sign=$sign, value=$value, trailing_zeros=false); + }; + ( sign=$sign:expr, value=$value:literal ) => { + define_test_input!(sign=$sign, value=$value, trailing_zeros=true); + }; + ( sign=$sign:expr, value=$value:expr, trailing_zeros=$trailing_zeros:literal ) => { + fn test_input() -> (u32, Sign, bool) { ($value, $sign, $trailing_zeros) } + }; + } + + mod case_13950000 { + use super::*; + + define_test_input!(13950000); + + impl_test!(3 :: Up => 13950000); + impl_test!(5 :: Up, Ceiling, HalfUp, HalfEven => 14000000); + impl_test!(5 :: Down, HalfDown => 13900000); + } + + mod case_neg_35488622_000x { + use super::*; + + // ...000x indicates non-zero trailing digit + define_test_input!(-35488622 ...000x); + + impl_test!(1 :: Up => 35488630); + impl_test!(1 :: Down => 35488620); + impl_test!(2 :: Up => 35488700); + impl_test!(2 :: Down => 35488600); + impl_test!(7 :: Up, Floor => 40000000); + impl_test!(7 :: Down, Ceiling => 30000000); + impl_test!(8 :: Up => 100000000); + impl_test!(8 :: Down => 0); + } +} From 908a03eca4c6bae7bdbb79834a5a8b461e9a4402 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Thu, 22 Feb 2024 00:29:54 -0500 Subject: [PATCH 42/54] Avoid overflow error in to_string exponential calculation --- src/impl_fmt.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index 2d299e1..c9b1525 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 i64 + exp - 1; + let exponent = abs_int.len() as i128 + exp as i128 - 1; if abs_int.len() > 1 { // only add decimal point if there is more than 1 decimal digit From f90d436f826f112e666bbe95170b5a434f794fdf Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Thu, 22 Feb 2024 01:25:11 -0500 Subject: [PATCH 43/54] Fix overflows and false negatives in from_str_radix --- src/impl_num.rs | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/impl_num.rs b/src/impl_num.rs index 8136973..0afe9d5 100644 --- a/src/impl_num.rs +++ b/src/impl_num.rs @@ -44,16 +44,8 @@ impl Num for BigDecimal { // split and parse exponent field Some(loc) => { // slice up to `loc` and 1 after to skip the 'e' char - let (base, exp) = (&s[..loc], &s[loc + 1..]); - - // special consideration for rust 1.0.0 which would not - // parse a leading '+' - let exp = match exp.chars().next() { - Some('+') => &exp[1..], - _ => exp, - }; - - (base, i64::from_str(exp)?) + let (base, e_exp) = s.split_at(loc); + (base, i128::from_str(&e_exp[1..])?) } }; @@ -81,18 +73,21 @@ impl Num for BigDecimal { // count number of trailing digits let trail_digits = trail.chars().filter(|c| *c != '_').count(); - (digits, trail_digits as i64) + (digits, trail_digits as i128) } }; - let scale = match decimal_offset.checked_sub(exponent_value) { - Some(scale) => scale, - None => { - return Err(ParseBigDecimalError::Other( - format!("Exponent overflow when parsing '{}'", s) - )) - } - }; + // Calculate scale by subtracing the parsed exponential + // value from the number of decimal digits. + // Return error if anything overflows outside i64 boundary. + let scale = decimal_offset + .checked_sub(exponent_value) + .map(|scale| scale.to_i64()) + .flatten() + .ok_or_else(|| + ParseBigDecimalError::Other( + format!("Exponent overflow when parsing '{}'", s)) + )?; let big_int = BigInt::from_str_radix(&digits, radix)?; From 65a12d9ae8e0e9825e61061d71281828e1dbe8df Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Thu, 22 Feb 2024 01:45:27 -0500 Subject: [PATCH 44/54] Avoid unnecessary copies in from_str_radix --- src/impl_num.rs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/impl_num.rs b/src/impl_num.rs index 0afe9d5..32d1e5d 100644 --- a/src/impl_num.rs +++ b/src/impl_num.rs @@ -54,26 +54,33 @@ impl Num for BigDecimal { return Err(ParseBigDecimalError::Empty); } + let mut digit_buffer = String::new(); + + let last_digit_loc = base_part.len() - 1; + // split decimal into a digit string and decimal-point offset - let (digits, decimal_offset): (String, _) = match base_part.find('.') { + let (digits, decimal_offset) = match base_part.find('.') { // No dot! pass directly to BigInt - None => (base_part.to_string(), 0), - + None => (base_part, 0), + // dot at last digit, pass all preceding digits to BigInt + Some(loc) if loc == last_digit_loc => { + (&base_part[..last_digit_loc], 0) + } // decimal point found - necessary copy into new string buffer Some(loc) => { // split into leading and trailing digits let (lead, trail) = (&base_part[..loc], &base_part[loc + 1..]); + digit_buffer.reserve(lead.len() + trail.len()); // copy all leading characters into 'digits' string - let mut digits = String::from(lead); - + digit_buffer.push_str(lead); // copy all trailing characters after '.' into the digits string - digits.push_str(trail); + digit_buffer.push_str(trail); // count number of trailing digits let trail_digits = trail.chars().filter(|c| *c != '_').count(); - (digits, trail_digits as i128) + (digit_buffer.as_str(), trail_digits as i128) } }; @@ -89,7 +96,7 @@ impl Num for BigDecimal { format!("Exponent overflow when parsing '{}'", s)) )?; - let big_int = BigInt::from_str_radix(&digits, radix)?; + let big_int = BigInt::from_str_radix(digits, radix)?; Ok(BigDecimal::new(big_int, scale)) } From 24d9b5873174461ad8086c56497adde95b73a40f Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Thu, 22 Feb 2024 02:50:19 -0500 Subject: [PATCH 45/54] Change more i64 casts to i128 in impl_fmt --- src/impl_fmt.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index c9b1525..f727d33 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -91,7 +91,7 @@ fn format_full_scale( use stdlib::cmp::Ordering::*; let mut digits = abs_int.into_bytes(); - let mut exp = -this.scale; + let mut exp = (this.scale as i128).neg(); let non_negative = matches!(this.sign, Sign::Plus | Sign::NoSign); debug_assert_ne!(digits.len(), 0); @@ -122,7 +122,7 @@ fn format_full_scale( debug_assert!(digits.len() > 1); // increase exp by len(digits)-1 (eg [ddddd]E+{exp} => [d.dddd]E+{exp+4}) - exp += digits.len() as i64 - 1; + exp += digits.len() as i128 - 1; // push decimal point and rotate it to index '1' digits.push(b'.'); @@ -177,7 +177,7 @@ 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; + let mut exp = (this.scale as i128).neg(); let mut digits = abs_int.into_bytes(); if digits.len() > 1 { @@ -266,7 +266,7 @@ pub(crate) fn write_scientific_notation(n: &BigDecimal, w: &mut W) -> w.write_str(".")?; w.write_str(remaining_digits)?; } - write!(w, "e{}", remaining_digits.len() as i64 - n.scale) + write!(w, "e{}", remaining_digits.len() as i128 - n.scale as i128) } @@ -317,13 +317,18 @@ pub(crate) fn write_engineering_notation(n: &BigDecimal, out: &mut W) /// Round big-endian digits in ascii -fn apply_rounding_to_ascii_digits(ascii_digits: &mut Vec, exp: &mut i64, prec: usize, sign: Sign) { +fn apply_rounding_to_ascii_digits( + ascii_digits: &mut Vec, + exp: &mut i128, + prec: usize, + sign: Sign +) { if ascii_digits.len() < prec { return; } // shift exp to align with new length of digits - *exp += (ascii_digits.len() - prec) as i64; + *exp += (ascii_digits.len() - prec) as i128; // true if all ascii_digits after precision are zeros let trailing_zeros = ascii_digits[prec + 1..].iter().all(|&d| d == b'0'); From 6d6349dcc7c1c682cd908778631897bcc7710d00 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Thu, 22 Feb 2024 02:51:18 -0500 Subject: [PATCH 46/54] Add tests for parsing at the boundary --- src/impl_fmt.rs | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs index f727d33..d4710f9 100644 --- a/src/impl_fmt.rs +++ b/src/impl_fmt.rs @@ -532,6 +532,45 @@ mod test { } } + mod fmt_boundaries { + use super::*; + + macro_rules! impl_case { + ( $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); + + bd.to_scientific_notation(); + bd.to_engineering_notation(); + + let round_trip = BigDecimal::from_str(&result).unwrap(); + assert_eq!(round_trip, bd); + } + }; + ( (panics) $name:ident: $src:expr ) => { + #[test] + #[should_panic] + fn $name() { + let src = $src; + let _bd: BigDecimal = src.parse().unwrap(); + } + }; + } + + 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_min_multiple_digits: format!("271828182E-{}", i64::MAX) => "2.71828182E-9223372036854775799"); + + impl_case!((panics) test_max_exp_overflow: "1E9223372036854775809"); + impl_case!((panics) test_min_exp_overflow: "1E-9223372036854775808"); + } + #[test] fn test_fmt() { let vals = vec![ From f104e981f58639e2be4db46b62fbba651ce80109 Mon Sep 17 00:00:00 2001 From: Amejonah1200 Date: Tue, 5 Mar 2024 01:29:38 +0100 Subject: [PATCH 47/54] Add Clone to ParseBigDecimalError --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 58b17c1..8dd52e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -929,7 +929,7 @@ impl BigDecimal { } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub enum ParseBigDecimalError { ParseDecimal(ParseFloatError), ParseInt(ParseIntError), From 33f44aa46cdcb7861dcc1d3c6bf709ccecbf7872 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Mon, 4 Mar 2024 21:19:47 -0500 Subject: [PATCH 48/54] Fix markdown table and further improve README.md --- README.md | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index b05e7c1..90477b8 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,18 @@ -Arbitary-precision decimal numbers implemented in pure Rust. +Arbitrary-precision decimal numbers implemented in pure Rust. ## Community Join the conversation on Zulip: https://bigdecimal-rs.zulipchat.com +Please share important stuff like use-cases, issues, benchmarks, and +naming-convention preferences. + +This project is currently being re-written, so if performance or flexibility +is lacking, check again soon and it may be fixed. + ## Usage Add bigdecimal as a dependency to your `Cargo.toml` file: @@ -47,15 +53,21 @@ sqrt(2) = 1.41421356237309504880168872420969807856967187537694807317667973799073 ### Compile-Time Configuration -You can set a few default parameters at compile-time via environment variables. +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 | -+-------------------------------------------------+------------+ + +These allow setting the default [Context] fields globally without incurring a runtime lookup, +or having to pass Context parameters through all calculations. +(If you want runtime control over these fields, you will have to pass Contexts to your functions.) +Examine [build.rs] for how those are converted to constants in the code (if interested). + +[Context]: https://docs.rs/bigdecimal/latest/bigdecimal/struct.Context.html +[build.rs]: ./build.rs #### Default precision @@ -65,14 +77,14 @@ The default value of this variable is 100. This will be used as maximum precision for operations which may produce infinite digits (inverse, sqrt, ...). -Note that other operations, such as multiplication, will preserve all digits, so multiplying two 70 digit numbers -will result in one 140 digit number. -The user will have to manually trim the number of digits after calculations to reasonable amounts using the -`x.with_prec(30)` method. - -A new set of methods with explicit precision and rounding modes is being worked on, but even after those -are introduced the default precision will have to be used as the implicit value. +Note that other operations, such as multiplication, will preserve all digits; +so multiplying two 70 digit numbers will result in one 140 digit number. +The user will have to manually trim the number of digits after calculations to +reasonable amounts using the `x.with_prec(30)` method. +A new set of methods with explicit precision and rounding modes is being worked +on, but even after those are introduced the default precision will have to be +used as the implicit value. #### Rounding mode @@ -132,12 +144,6 @@ $ cargo run > This is for flexibility and performance. -## Improvements - -Work is being done on this codebase again and there are many features -and improvements on the way. - - ## About This repository contains code originally meant for a bigdecimal module From 2c66e3635a41635544c146bc193c2b748f17ce45 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Tue, 26 Sep 2023 00:15:59 -0400 Subject: [PATCH 49/54] Add diff & diff_usize functions to arithmetic module --- src/arithmetic/mod.rs | 27 +++++++++++++++++++++++++++ src/lib.rs | 3 +++ 2 files changed, 30 insertions(+) diff --git a/src/arithmetic/mod.rs b/src/arithmetic/mod.rs index 614503d..2f6bbf8 100644 --- a/src/arithmetic/mod.rs +++ b/src/arithmetic/mod.rs @@ -74,3 +74,30 @@ pub(crate) fn count_decimal_digits_uint(uint: &BigUint) -> u64 { } digits } + + +/// 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 +{ + use stdlib::cmp::Ordering::*; + + match a.cmp(&b) { + Less => (Less, (b - a).to_u64().unwrap()), + Greater => (Greater, (a - b).to_u64().unwrap()), + Equal => (Equal, 0), + } +} + +/// Return difference of two numbers, returning diff as usize +#[allow(dead_code)] +pub(crate) fn diff_usize(a: usize, b: usize) -> (Ordering, usize) { + use stdlib::cmp::Ordering::*; + + match a.cmp(&b) { + Less => (Less, b - a), + Greater => (Greater, a - b), + Equal => (Equal, 0), + } +} diff --git a/src/lib.rs b/src/lib.rs index 8dd52e6..42a0a0f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -141,10 +141,13 @@ pub use context::Context; use arithmetic::{ ten_to_the, ten_to_the_uint, + diff, + diff_usize, count_decimal_digits, count_decimal_digits_uint, }; + /// Internal function used for rounding /// /// returns 1 if most significant digit is >= 5, otherwise 0 From 061995477538cf2fb87682111c7256a09ebdefa8 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Tue, 5 Mar 2024 00:01:39 -0500 Subject: [PATCH 50/54] Move addition implementations to arithmetic module --- src/arithmetic/addition.rs | 87 ++++++++++++++++++++++++++++++++++++++ src/arithmetic/mod.rs | 1 + src/impl_ops_add.rs | 71 ++++--------------------------- 3 files changed, 97 insertions(+), 62 deletions(-) create mode 100644 src/arithmetic/addition.rs diff --git a/src/arithmetic/addition.rs b/src/arithmetic/addition.rs new file mode 100644 index 0000000..00d1881 --- /dev/null +++ b/src/arithmetic/addition.rs @@ -0,0 +1,87 @@ +//! addition routines +//! + +use crate::*; + + +pub(crate) fn add_bigdecimals( + mut a: BigDecimal, + mut b: BigDecimal, +) -> BigDecimal { + if b.is_zero() { + return a; + } + if a.is_zero() { + return b; + } + + match a.scale.cmp(&b.scale) { + Ordering::Equal => { + a.int_val += b.int_val; + a + } + Ordering::Less => a.take_and_scale(b.scale) + b, + Ordering::Greater => b.take_and_scale(a.scale) + a, + } +} + +pub(crate) fn add_bigdecimal_refs<'a, 'b, Lhs, Rhs>( + lhs: Lhs, + rhs: Rhs, +) -> BigDecimal +where + Rhs: Into>, + Lhs: Into>, +{ + let lhs = lhs.into(); + let rhs = rhs.into(); + if rhs.is_zero() { + return lhs.to_owned(); + } + if lhs.is_zero() { + return rhs.to_owned(); + } + if lhs.scale >= rhs.scale { + lhs.to_owned() + rhs + } else { + rhs.to_owned() + lhs + } +} + + +pub(crate) fn addassign_bigdecimals( + lhs: &mut BigDecimal, + rhs: BigDecimal, +) { + if rhs.is_zero() { + return; + } + if lhs.is_zero() { + *lhs = rhs; + return; + } + lhs.add_assign(rhs.to_ref()); +} + + +pub(crate) fn addassign_bigdecimal_ref<'a, T: Into>>( + lhs: &mut BigDecimal, + rhs: T, +) { + // TODO: Replace to_owned() with efficient addition algorithm + let rhs = rhs.into().to_owned(); + match lhs.scale.cmp(&rhs.scale) { + Ordering::Less => { + let scaled = lhs.with_scale(rhs.scale); + lhs.int_val = scaled.int_val + &rhs.int_val; + lhs.scale = rhs.scale; + } + Ordering::Greater => { + let scaled = rhs.with_scale(lhs.scale); + lhs.int_val += scaled.int_val; + } + Ordering::Equal => { + lhs.int_val += &rhs.int_val; + } + } +} diff --git a/src/arithmetic/mod.rs b/src/arithmetic/mod.rs index 2f6bbf8..9580441 100644 --- a/src/arithmetic/mod.rs +++ b/src/arithmetic/mod.rs @@ -2,6 +2,7 @@ use crate::*; +pub(crate) mod addition; pub(crate) mod sqrt; pub(crate) mod cbrt; pub(crate) mod inverse; diff --git a/src/impl_ops_add.rs b/src/impl_ops_add.rs index 8b80bfd..f056c95 100644 --- a/src/impl_ops_add.rs +++ b/src/impl_ops_add.rs @@ -10,22 +10,7 @@ impl Add for BigDecimal { #[inline] fn add(self, rhs: BigDecimal) -> BigDecimal { - if rhs.is_zero() { - return self; - } - if self.is_zero() { - return rhs; - } - - let mut lhs = self; - match lhs.scale.cmp(&rhs.scale) { - Ordering::Equal => { - lhs.int_val += rhs.int_val; - lhs - } - Ordering::Less => lhs.take_and_scale(rhs.scale) + rhs, - Ordering::Greater => rhs.take_and_scale(lhs.scale) + lhs, - } + arithmetic::addition::add_bigdecimals(self, rhs) } } @@ -61,18 +46,7 @@ impl Add for &'_ BigDecimal { impl<'a, T: Into>> Add for &'_ BigDecimal { type Output = BigDecimal; fn add(self, rhs: T) -> BigDecimal { - let rhs = rhs.into(); - if rhs.is_zero() { - return self.clone(); - } - if self.is_zero() { - return rhs.to_owned(); - } - if self.scale >= rhs.scale { - self.to_owned() + rhs - } else { - rhs.to_owned() + self - } + arithmetic::addition::add_bigdecimal_refs(self, rhs) } } @@ -98,12 +72,7 @@ impl Add for BigDecimalRef<'_> { impl<'a, T: Into>> Add for BigDecimalRef<'_> { type Output = BigDecimal; fn add(self, rhs: T) -> BigDecimal { - let rhs = rhs.into(); - if self.scale >= rhs.scale { - self.to_owned() + rhs - } else { - rhs.to_owned() + self - } + arithmetic::addition::add_bigdecimal_refs(self, rhs) } } @@ -122,7 +91,7 @@ impl Add for BigInt { #[inline] fn add(self, rhs: BigDecimal) -> BigDecimal { - rhs + self + BigDecimal::from(self) + rhs } } @@ -130,7 +99,7 @@ impl<'a> Add<&'a BigDecimal> for BigInt { type Output = BigDecimal; fn add(self, rhs: &BigDecimal) -> BigDecimal { - rhs.to_ref().add(self) + BigDecimal::from(self) + rhs } } @@ -138,7 +107,7 @@ impl<'a> Add> for BigInt { type Output = BigDecimal; fn add(self, rhs: BigDecimalRef<'_>) -> BigDecimal { - rhs.add(self) + BigDecimal::from(self) + rhs } } @@ -173,43 +142,21 @@ impl<'a> Add> for &BigInt { impl AddAssign for BigDecimal { fn add_assign(&mut self, rhs: BigDecimal) { - if rhs.is_zero() { - return; - } - if self.is_zero() { - *self = rhs; - return; - } - self.add_assign(rhs.to_ref()); + arithmetic::addition::addassign_bigdecimals(self, rhs) } } impl<'a, N: Into>> AddAssign for BigDecimal { #[inline] fn add_assign(&mut self, rhs: N) { - // TODO: Replace to_owned() with efficient addition algorithm - let rhs = rhs.into().to_owned(); - match self.scale.cmp(&rhs.scale) { - Ordering::Less => { - let scaled = self.with_scale(rhs.scale); - self.int_val = scaled.int_val + &rhs.int_val; - self.scale = rhs.scale; - } - Ordering::Greater => { - let scaled = rhs.with_scale(self.scale); - self.int_val += scaled.int_val; - } - Ordering::Equal => { - self.int_val += &rhs.int_val; - } - } + arithmetic::addition::addassign_bigdecimal_ref(self, rhs) } } impl AddAssign for BigDecimal { #[inline] fn add_assign(&mut self, rhs: BigInt) { - self.add_assign(&rhs); + self.add_assign(BigDecimal::from(rhs)); } } From a0c7df42eb44df8ed47ed1efba2da33410a17192 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Tue, 5 Mar 2024 01:19:01 -0500 Subject: [PATCH 51/54] Add ten_to_the_u64 --- src/arithmetic/mod.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/arithmetic/mod.rs b/src/arithmetic/mod.rs index 9580441..46644bc 100644 --- a/src/arithmetic/mod.rs +++ b/src/arithmetic/mod.rs @@ -15,6 +15,12 @@ pub(crate) fn ten_to_the(pow: u64) -> BigInt { ten_to_the_uint(pow).into() } +/// Return 10^{pow} as u64 +pub(crate) fn ten_to_the_u64(pow: u8) -> u64 { + debug_assert!(pow < 20); + 10u64.pow(pow as u32) +} + /// Return 10^pow pub(crate) fn ten_to_the_uint(pow: u64) -> BigUint { if pow < 20 { From 83f537d3a10b9b9a377e67246f60f4e5e14586e4 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Tue, 5 Mar 2024 01:21:31 -0500 Subject: [PATCH 52/54] Reimplement take_and_scale with diff and ten_to_the_u64 --- src/lib.rs | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 42a0a0f..c8b328c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -141,6 +141,7 @@ pub use context::Context; use arithmetic::{ ten_to_the, ten_to_the_uint, + ten_to_the_u64, diff, diff_usize, count_decimal_digits, @@ -383,20 +384,31 @@ impl BigDecimal { /// fn take_and_scale(mut self, new_scale: i64) -> BigDecimal { if self.int_val.is_zero() { - return BigDecimal::new(BigInt::zero(), new_scale); + self.scale = new_scale; + return self; } - match new_scale.cmp(&self.scale) { - Ordering::Greater => { - self.int_val *= ten_to_the((new_scale - self.scale) as u64); - BigDecimal::new(self.int_val, new_scale) + match diff(new_scale, self.scale) { + (Ordering::Greater, scale_diff) => { + self.scale = new_scale; + if scale_diff < 20 { + self.int_val *= ten_to_the_u64(scale_diff as u8); + } else { + self.int_val *= ten_to_the(scale_diff); + } } - Ordering::Less => { - self.int_val /= ten_to_the((self.scale - new_scale) as u64); - BigDecimal::new(self.int_val, new_scale) + (Ordering::Less, scale_diff) => { + self.scale = new_scale; + if scale_diff < 20 { + self.int_val /= ten_to_the_u64(scale_diff as u8); + } else { + self.int_val /= ten_to_the(scale_diff); + } } - Ordering::Equal => self, + (Ordering::Equal, _) => {}, } + + self } /// Take and return bigdecimal with the given sign From 03105f31ee97eaedfb1eea3155a1f258b6195e28 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Tue, 5 Mar 2024 01:52:30 -0500 Subject: [PATCH 53/54] Preserve scale when adding zero and add minor optimize add_bigdecimals --- src/arithmetic/addition.rs | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/arithmetic/addition.rs b/src/arithmetic/addition.rs index 00d1881..11f04b9 100644 --- a/src/arithmetic/addition.rs +++ b/src/arithmetic/addition.rs @@ -9,22 +9,45 @@ pub(crate) fn add_bigdecimals( mut b: BigDecimal, ) -> BigDecimal { if b.is_zero() { + if a.scale < b.scale { + a.int_val *= ten_to_the((b.scale - a.scale) as u64); + a.scale = b.scale; + } return a; } + if a.is_zero() { + if b.scale < a.scale { + b.int_val *= ten_to_the((a.scale - b.scale) as u64); + b.scale = a.scale; + } return b; } - match a.scale.cmp(&b.scale) { - Ordering::Equal => { - a.int_val += b.int_val; - a - } - Ordering::Less => a.take_and_scale(b.scale) + b, - Ordering::Greater => b.take_and_scale(a.scale) + a, + let (a, b) = match a.scale.cmp(&b.scale) { + Ordering::Equal => (a, b), + Ordering::Less => (a.take_and_scale(b.scale), b), + Ordering::Greater => (b.take_and_scale(a.scale), a), + }; + + add_aligned_bigdecimals(a, b) +} + +fn add_aligned_bigdecimals( + mut a: BigDecimal, + mut b: BigDecimal, +) -> BigDecimal { + debug_assert_eq!(a.scale, b.scale); + if a.int_val.bits() >= b.int_val.bits() { + a.int_val += b.int_val; + a + } else { + b.int_val += a.int_val; + b } } + pub(crate) fn add_bigdecimal_refs<'a, 'b, Lhs, Rhs>( lhs: Lhs, rhs: Rhs, From ea6eb2feecbb8b607221d795fc23e4a9d361e9bc Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Tue, 5 Mar 2024 01:59:36 -0500 Subject: [PATCH 54/54] Version 0.4.3 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 50c9dfd..b388e00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bigdecimal" -version = "0.4.3+dev" +version = "0.4.3" authors = ["Andrew Kubera"] description = "Arbitrary precision decimal numbers" documentation = "https://docs.rs/bigdecimal"