From cf8b6e7e7e44f28b7f93f99f77772e90decb710c Mon Sep 17 00:00:00 2001 From: haraldmaida Date: Sat, 7 Jun 2025 23:47:32 +0200 Subject: [PATCH 1/2] feat: provide decimal number specific assertions `has_scale_of`, `has_precision_of` and `is_integer` --- src/assertions.rs | 136 +++++++++++++++++++++++++++++++++++ src/bigdecimal/mod.rs | 18 ++++- src/bigdecimal/tests.rs | 144 ++++++++++++++++++++++++++++++++++++++ src/expectations.rs | 13 ++++ src/number.rs | 77 ++++++++++++++++++-- src/properties.rs | 50 +++++++++++++ src/rust_decimal/mod.rs | 15 ++++ src/rust_decimal/tests.rs | 93 ++++++++++++++++++++++++ 8 files changed, 540 insertions(+), 6 deletions(-) diff --git a/src/assertions.rs b/src/assertions.rs index c5aac84..226e789 100644 --- a/src/assertions.rs +++ b/src/assertions.rs @@ -665,6 +665,142 @@ pub trait AssertNotANumber { fn is_a_number(self) -> Self; } +/// Assert decimal number specific properties. +pub trait AssertDecimalNumber { + /// Verifies the scale of a decimal number. + /// + /// It compares the scale, the total number of digits to the right of the + /// decimal point (including insignificant leading zeros), to the expected + /// scale. + /// + /// # Examples + /// + /// For `bigdecimal::BigDecimal` (requires crate feature `bigdecimal`): + /// + /// ``` + /// # #[cfg(not(feature = "bigdecimal"))] + /// # fn main() {} + /// # #[cfg(feature = "bigdecimal")] + /// # fn main() { + /// use asserting::prelude::*; + /// use bigdecimal::BigDecimal; + /// + /// let subject: BigDecimal = "42.0839".parse().unwrap(); + /// assert_that!(subject).has_scale_of(4); + /// + /// let subject: BigDecimal = "1.053700".parse().unwrap(); + /// assert_that!(&subject).has_scale_of(6); + /// assert_that!(subject.normalized()).has_scale_of(4); + /// # } + /// ``` + /// + /// For `rust_decimal::Decimal` (requires crate feature `rust-decimal`): + /// + /// ``` + /// # #[cfg(not(feature = "rust-decimal"))] + /// # fn main() {} + /// # #[cfg(feature = "rust-decimal")] + /// # fn main() { + /// use asserting::prelude::*; + /// use rust_decimal::Decimal; + /// + /// let subject: Decimal = "42.0839".parse().unwrap(); + /// assert_that!(subject).has_scale_of(4); + /// + /// let subject: Decimal = "1.053700".parse().unwrap(); + /// assert_that!(subject).has_scale_of(6); + /// assert_that!(subject.normalize()).has_scale_of(4); + /// # } + /// ``` + #[track_caller] + fn has_scale_of(self, expected_scale: i64) -> Self; + + /// Verifies the precision of a decimal number. + /// + /// It compares the precision, the total number of digits in the non-scaled + /// integer representation, to the expected precision. + /// + /// # Examples + /// + /// For `bigdecimal::BigDecimal` (requires crate feature `bigdecimal`): + /// + /// ``` + /// # #[cfg(not(feature = "bigdecimal"))] + /// # fn main() {} + /// # #[cfg(feature = "bigdecimal")] + /// # fn main() { + /// use asserting::prelude::*; + /// use bigdecimal::BigDecimal; + /// + /// let subject: BigDecimal = "42.0839".parse().unwrap(); + /// + /// assert_that!(subject).has_precision_of(6); + /// # } + /// ``` + /// + /// For `rust_decimal::Decimal` (requires crate feature `rust-decimal`): + /// + /// ``` + /// # #[cfg(not(feature = "rust-decimal"))] + /// # fn main() {} + /// # #[cfg(feature = "rust-decimal")] + /// # fn main() { + /// use asserting::prelude::*; + /// use rust_decimal::Decimal; + /// + /// let subject: Decimal = "42.083916".parse().unwrap(); + /// assert_that!(subject).has_precision_of(29); + /// + /// let subject: Decimal = "1.05".parse().unwrap(); + /// assert_that!(subject).has_precision_of(29); + /// # } + /// ``` + /// + /// Note: `rust_decimal::Decimal` is fixed precision decimal number. The + /// actual precision is always 29. + #[track_caller] + fn has_precision_of(self, expected_precision: u64) -> Self; + + /// Verifies that a decimal number has zero fractional digits (is equivalent + /// to an integer). + /// + /// # Examples + /// + /// For `bigdecimal::BigDecimal` (requires crate feature `bigdecimal`): + /// + /// ``` + /// # #[cfg(not(feature = "bigdecimal"))] + /// # fn main() {} + /// # #[cfg(feature = "bigdecimal")] + /// # fn main() { + /// use asserting::prelude::*; + /// use bigdecimal::BigDecimal; + /// + /// let subject: BigDecimal = "14_752.0".parse().unwrap(); + /// + /// assert_that!(subject).is_integer(); + /// # } + /// ``` + /// + /// For `rust_decimal::Decimal` (requires crate feature `rust-decimal`): + /// + /// ``` + /// # #[cfg(not(feature = "rust-decimal"))] + /// # fn main() {} + /// # #[cfg(feature = "rust-decimal")] + /// # fn main() { + /// use asserting::prelude::*; + /// use rust_decimal::Decimal; + /// + /// let subject: Decimal = "14_752.0".parse().unwrap(); + /// + /// assert_that!(subject).is_integer(); + /// # } + /// ``` + #[track_caller] + fn is_integer(self) -> Self; +} + /// Assert whether some value or expression is true or false. /// /// # Examples diff --git a/src/bigdecimal/mod.rs b/src/bigdecimal/mod.rs index 71089ef..aa48426 100644 --- a/src/bigdecimal/mod.rs +++ b/src/bigdecimal/mod.rs @@ -1,4 +1,6 @@ -use crate::properties::{AdditiveIdentityProperty, MultiplicativeIdentityProperty, SignumProperty}; +use crate::properties::{ + AdditiveIdentityProperty, DecimalProperties, MultiplicativeIdentityProperty, SignumProperty, +}; use bigdecimal::num_bigint::Sign; use bigdecimal::{BigDecimal, BigDecimalRef, One, Zero}; use lazy_static::lazy_static; @@ -52,6 +54,20 @@ impl MultiplicativeIdentityProperty for &BigDecimal { } } +impl DecimalProperties for BigDecimal { + fn precision_property(&self) -> u64 { + self.digits() + } + + fn scale_property(&self) -> i64 { + self.fractional_digit_count() + } + + fn is_integer_property(&self) -> bool { + self.is_integer() + } +} + impl SignumProperty for BigDecimalRef<'_> { fn is_negative_property(&self) -> bool { self.sign() == Sign::Minus diff --git a/src/bigdecimal/tests.rs b/src/bigdecimal/tests.rs index b8d2713..d59d2a0 100644 --- a/src/bigdecimal/tests.rs +++ b/src/bigdecimal/tests.rs @@ -239,3 +239,147 @@ fn bigdecimalref_is_zero() { fn bigdecimalref_is_one() { assert_that(BigDecimal::new(BigInt::from(1), 0).to_ref()).is_one(); } + +#[test] +fn bigdecimal_has_precision_of() { + let subject = BigDecimal::new(BigInt::from(420_831), 4); + + assert_that(subject).has_precision_of(6); +} + +#[test] +fn bigdecimal_has_precision_of_trailing_zeros() { + let subject = BigDecimal::new(BigInt::from(420_831_000), 7); + + assert_that(&subject).has_precision_of(9); + + assert_that(subject.normalized()).has_precision_of(6); +} + +#[test] +fn verify_bigdecimal_has_precision_of_fails() { + let subject = BigDecimal::new(BigInt::from(420_831_000), 7); + + let failures = verify_that(subject).has_precision_of(7).display_failures(); + + assert_eq!( + failures, + &[ + r"assertion failed: expected subject to have a precision of 7 + but was: 9 + expected: 7 +" + ] + ); +} + +#[test] +fn bigdecimal_has_scale_of() { + let subject = BigDecimal::new(BigInt::from(420_831), 4); + + assert_that(subject).has_scale_of(4); +} + +#[test] +fn bigdecimal_has_scale_of_with_zero_in_fraction() { + let subject = BigDecimal::new(BigInt::from(420_830), 1); + + assert_that(&subject).has_scale_of(1); + + assert_that(subject.normalized()).has_scale_of(0); +} + +#[test] +fn verify_bigdecimal_has_scale_of_fails() { + let subject = BigDecimal::new(BigInt::from(420_831_000), 5); + + let failures = verify_that(subject.normalized()) + .has_scale_of(5) + .display_failures(); + + assert_eq!( + failures, + &[r"assertion failed: expected subject to have a scale of 5 + but was: 2 + expected: 5 +"] + ); +} + +#[test] +fn bigdecimal_has_scale_of_trailing_zeros() { + let subject = BigDecimal::new(BigInt::from(420_831_000), 4); + + assert_that(subject).has_scale_of(4); +} + +#[test] +fn bigdecimal_is_integer() { + let subject = BigDecimal::new(BigInt::from(420_830), 0); + + assert_that(subject).is_integer(); +} + +#[test] +fn bigdecimal_is_integer_zero_in_fraction() { + let subject = BigDecimal::new(BigInt::from(420_830), 1); + + assert_that(subject).is_integer(); +} + +#[test] +fn verify_bigdecimal_is_integer_fails() { + let subject = BigDecimal::new(BigInt::from(420_810), 2); + + let failures = verify_that(subject).is_integer().display_failures(); + + assert_eq!( + failures, + &[r"assertion failed: expected subject to be an integer value + but was: BigDecimal(sign=Plus, scale=2, digits=[420810]) + expected: an integer value +"] + ); +} + +#[test] +fn borrowed_bigdecimal_has_precision_of() { + let subject = BigDecimal::new(BigInt::from(420_831), 4); + + assert_that(&subject).has_precision_of(6); +} + +#[test] +fn borrowed_bigdecimal_has_scale_of() { + let subject = BigDecimal::new(BigInt::from(420_831), 4); + + assert_that(&subject).has_scale_of(4); +} + +#[test] +fn borrowed_bigdecimal_is_integer() { + let subject = BigDecimal::new(BigInt::from(420_830), 0); + + assert_that(&subject).is_integer(); +} + +#[test] +fn mutable_borrowed_bigdecimal_has_precision_of() { + let mut subject = BigDecimal::new(BigInt::from(420_831), 4); + + assert_that(&mut subject).has_precision_of(6); +} + +#[test] +fn mutable_borrowed_bigdecimal_has_scale_of() { + let mut subject = BigDecimal::new(BigInt::from(420_831), 4); + + assert_that(&mut subject).has_scale_of(4); +} + +#[test] +fn mutable_borrowed_bigdecimal_is_integer() { + let mut subject = BigDecimal::new(BigInt::from(420_830), 0); + + assert_that(&mut subject).is_integer(); +} diff --git a/src/expectations.rs b/src/expectations.rs index 4dff83a..9664765 100644 --- a/src/expectations.rs +++ b/src/expectations.rs @@ -175,6 +175,19 @@ pub struct IsNotANumber; #[must_use] pub struct IsANumber; +#[must_use] +pub struct HasPrecisionOf { + pub expected_precision: u64, +} + +#[must_use] +pub struct HasScaleOf { + pub expected_scale: i64, +} + +#[must_use] +pub struct IsInteger; + #[must_use] pub struct IsLowerCase; diff --git a/src/number.rs b/src/number.rs index b89ed86..88b7699 100644 --- a/src/number.rs +++ b/src/number.rs @@ -1,14 +1,16 @@ //! Implementations of assertions specific for numbers. -use crate::assertions::{AssertInfinity, AssertNotANumber, AssertNumericIdentity, AssertSignum}; +use crate::assertions::{ + AssertDecimalNumber, AssertInfinity, AssertNotANumber, AssertNumericIdentity, AssertSignum, +}; use crate::colored::{mark_missing, mark_missing_substr, mark_unexpected}; use crate::expectations::{ - IsANumber, IsFinite, IsInfinite, IsNegative, IsNotANumber, IsNotNegative, IsNotPositive, IsOne, - IsPositive, IsZero, + HasPrecisionOf, HasScaleOf, IsANumber, IsFinite, IsInfinite, IsInteger, IsNegative, + IsNotANumber, IsNotNegative, IsNotPositive, IsOne, IsPositive, IsZero, }; use crate::properties::{ - AdditiveIdentityProperty, InfinityProperty, IsNanProperty, MultiplicativeIdentityProperty, - SignumProperty, + AdditiveIdentityProperty, DecimalProperties, InfinityProperty, IsNanProperty, + MultiplicativeIdentityProperty, SignumProperty, }; use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Spec}; use crate::std::fmt::Debug; @@ -228,3 +230,68 @@ where format!("expected {expression} is not a number (NaN)\n but was: {marked_actual}\n expected: {marked_expected}") } } + +impl AssertDecimalNumber for Spec<'_, S, R> +where + S: DecimalProperties + Debug, + R: FailingStrategy, +{ + fn has_scale_of(self, expected_scale: i64) -> Self { + self.expecting(HasScaleOf { expected_scale }) + } + + fn has_precision_of(self, expected_precision: u64) -> Self { + self.expecting(HasPrecisionOf { expected_precision }) + } + + fn is_integer(self) -> Self { + self.expecting(IsInteger) + } +} + +impl Expectation for HasScaleOf +where + S: DecimalProperties + Debug, +{ + fn test(&mut self, subject: &S) -> bool { + subject.scale_property() == self.expected_scale + } + + fn message(&self, expression: &Expression<'_>, actual: &S, format: &DiffFormat) -> String { + let expected_scale = self.expected_scale; + let marked_actual = mark_unexpected(&actual.scale_property(), format); + let marked_expected = mark_missing(&expected_scale, format); + format!("expected {expression} to have a scale of {expected_scale}\n but was: {marked_actual}\n expected: {marked_expected}") + } +} + +impl Expectation for HasPrecisionOf +where + S: DecimalProperties + Debug, +{ + fn test(&mut self, subject: &S) -> bool { + subject.precision_property() == self.expected_precision + } + + fn message(&self, expression: &Expression<'_>, actual: &S, format: &DiffFormat) -> String { + let expected_precision = self.expected_precision; + let marked_actual = mark_unexpected(&actual.precision_property(), format); + let marked_expected = mark_missing(&expected_precision, format); + format!("expected {expression} to have a precision of {expected_precision}\n but was: {marked_actual}\n expected: {marked_expected}") + } +} + +impl Expectation for IsInteger +where + S: DecimalProperties + Debug, +{ + fn test(&mut self, subject: &S) -> bool { + subject.is_integer_property() + } + + fn message(&self, expression: &Expression<'_>, actual: &S, format: &DiffFormat) -> String { + let marked_actual = mark_unexpected(&actual, format); + let marked_expected = mark_missing_substr("an integer value", format); + format!("expected {expression} to be an integer value\n but was: {marked_actual}\n expected: {marked_expected}") + } +} diff --git a/src/properties.rs b/src/properties.rs index 9a56023..f0adfd2 100644 --- a/src/properties.rs +++ b/src/properties.rs @@ -220,6 +220,56 @@ where } } +/// Properties of a decimal number. +pub trait DecimalProperties { + /// Returns the precision of this decimal number. + /// + /// The precision is the total number of digits in the non-scaled integer + /// representation of a decimal number. + fn precision_property(&self) -> u64; + + /// Returns the scale of this decimal number. + fn scale_property(&self) -> i64; + + /// Returns whether this decimal number has no fractional digits + /// (is equivalent to an integer). + fn is_integer_property(&self) -> bool; +} + +impl DecimalProperties for &T +where + T: DecimalProperties + ?Sized, +{ + fn precision_property(&self) -> u64 { + ::precision_property(self) + } + + fn scale_property(&self) -> i64 { + ::scale_property(self) + } + + fn is_integer_property(&self) -> bool { + ::is_integer_property(self) + } +} + +impl DecimalProperties for &mut T +where + T: DecimalProperties + ?Sized, +{ + fn precision_property(&self) -> u64 { + ::precision_property(self) + } + + fn scale_property(&self) -> i64 { + ::scale_property(self) + } + + fn is_integer_property(&self) -> bool { + ::is_integer_property(self) + } +} + /// The properties of a map-like type. pub trait MapProperties { /// The type of the keys in this map. diff --git a/src/rust_decimal/mod.rs b/src/rust_decimal/mod.rs index 36028f5..28893ef 100644 --- a/src/rust_decimal/mod.rs +++ b/src/rust_decimal/mod.rs @@ -1,3 +1,4 @@ +use crate::prelude::DecimalProperties; use crate::properties::{AdditiveIdentityProperty, MultiplicativeIdentityProperty, SignumProperty}; use rust_decimal::Decimal; @@ -35,5 +36,19 @@ impl MultiplicativeIdentityProperty for &Decimal { } } +impl DecimalProperties for Decimal { + fn precision_property(&self) -> u64 { + 29 + } + + fn scale_property(&self) -> i64 { + i64::from(self.scale()) + } + + fn is_integer_property(&self) -> bool { + self.is_integer() + } +} + #[cfg(test)] mod tests; diff --git a/src/rust_decimal/tests.rs b/src/rust_decimal/tests.rs index 6be69cf..57faf97 100644 --- a/src/rust_decimal/tests.rs +++ b/src/rust_decimal/tests.rs @@ -152,3 +152,96 @@ fn borrowed_decimal_is_zero() { fn borrowed_decimal_is_one() { assert_that(&Decimal::new(1, 0)).is_one(); } + +#[test] +fn decimal_has_precision_of() { + let subject = Decimal::new(420_831, 4); + + assert_that(subject).has_precision_of(29); +} + +#[test] +fn verify_decimal_has_precision_of_fails() { + let subject = Decimal::new(420_831_000, 7); + + let failures = verify_that(subject).has_precision_of(7).display_failures(); + + assert_eq!( + failures, + &[ + r"assertion failed: expected subject to have a precision of 7 + but was: 29 + expected: 7 +" + ] + ); +} + +#[test] +fn decimal_has_scale_of() { + let subject = Decimal::new(420_831, 4); + + assert_that(subject).has_scale_of(4); +} + +#[test] +fn decimal_has_scale_of_with_zero_in_fraction() { + let subject = Decimal::new(420_830, 1); + + assert_that(subject).has_scale_of(1); + + assert_that(subject.normalize()).has_scale_of(0); +} + +#[test] +fn decimal_has_scale_of_trailing_zeros() { + let subject = Decimal::new(420_831_000, 4); + + assert_that(subject).has_scale_of(4); +} + +#[test] +fn verify_decimal_has_scale_of_fails() { + let subject = Decimal::new(420_831_000, 5); + + let failures = verify_that(subject.normalize()) + .has_scale_of(5) + .display_failures(); + + assert_eq!( + failures, + &[r"assertion failed: expected subject to have a scale of 5 + but was: 2 + expected: 5 +"] + ); +} + +#[test] +fn decimal_is_integer() { + let subject = Decimal::new(420_830, 0); + + assert_that(subject).is_integer(); +} + +#[test] +fn decimal_is_integer_zero_in_fraction() { + let subject = Decimal::new(420_830, 1); + + assert_that(subject).is_integer(); +} + +#[test] +fn verify_decimal_is_integer_fails() { + let subject = Decimal::new(420_810, 2); + + let failures = verify_that(subject).is_integer().display_failures(); + + assert_eq!( + failures, + &[r"assertion failed: expected subject to be an integer value + but was: 4208.10 + expected: an integer value +"] + ); +} From 6d14803ac85f7a932ccdbac8b27a3a5b9b002269 Mon Sep 17 00:00:00 2001 From: haraldmaida Date: Sat, 7 Jun 2025 23:54:39 +0200 Subject: [PATCH 2/2] doc: list decimal number specific assertions in README --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index c9ab5e9..860481d 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,19 @@ for floating point numbers of type `f32` and `f64`: | is_not_a_number | verify that the subject is not a number | | is_a_number | verify that the subject is a number | +### Decimal number + +for decimal numbers of types + +* `bigdecimal:BigDecimal` and `bigdecimal:BigDecimalRef` (requires crate feature `bigdecimal`) +* `rust_decimal::Decimal` (requires crate feature `rust-decimal`) + +| assertion | description | +|------------------|----------------------------------------------------| +| has_scale_of | verify that the subject has the expected scale | +| has_precision_of | verify that the subject has the expected precision | +| is_integer | verify that the subject has zero fractional digits | + ### Float comparison for floating point numbers of type `f32` and `f64`.