From c8544d57285f9b2f1de1a55cede6d3bea412f5c2 Mon Sep 17 00:00:00 2001 From: OliverNChalk <11343499+OliverNChalk@users.noreply.github.com> Date: Sat, 14 Sep 2024 19:15:27 -0500 Subject: [PATCH] feat: finish up `from_scaled` implementation --- CHANGELOG.md | 1 + benches/add.rs | 6 +- benches/div.rs | 6 +- benches/main.rs | 4 +- benches/mul.rs | 6 +- benches/sub.rs | 6 +- src/algorithms.rs | 11 +++ src/decimal.rs | 151 +++++++++++++++++++++++++------- src/display.rs | 12 +-- src/foreign_traits/borsh.rs | 4 +- src/foreign_traits/malachite.rs | 4 +- src/foreign_traits/proptest.rs | 4 +- src/integer.rs | 14 +-- src/lib.rs | 3 + src/macros.rs | 6 +- 15 files changed, 172 insertions(+), 66 deletions(-) create mode 100644 src/algorithms.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8049cdc..e7141c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to `const-decimal`. - Added `BorshSerialize` and `BorshDeserialize` behind `borsh` feature flag. - Added `AddAssign`, `SubAssign`, `MulAssign`, and `DivAssign`. +- Implemented `Decimal::from_scaled(integer: I, decimals: u8)`. ## 0.1.0 diff --git a/benches/add.rs b/benches/add.rs index c6fbbe5..35c7be4 100644 --- a/benches/add.rs +++ b/benches/add.rs @@ -1,4 +1,4 @@ -use const_decimal::{Decimal, Integer}; +use const_decimal::{Decimal, ScaledInteger}; use criterion::measurement::WallTime; use criterion::{black_box, BatchSize, BenchmarkGroup}; use num_traits::PrimInt; @@ -8,7 +8,7 @@ use proptest::prelude::*; pub fn bench_all(group: &mut BenchmarkGroup<'_, WallTime>) where - I: Integer + Arbitrary, + I: ScaledInteger + Arbitrary, { bench_primitive_add::(group); bench_decimal_add::(group); @@ -33,7 +33,7 @@ where fn bench_decimal_add(group: &mut BenchmarkGroup<'_, WallTime>) where - I: Integer + Arbitrary, + I: ScaledInteger + Arbitrary, { // Use proptest to generate arbitrary input values. let mut runner = TestRunner::deterministic(); diff --git a/benches/div.rs b/benches/div.rs index a06244b..2dbcf00 100644 --- a/benches/div.rs +++ b/benches/div.rs @@ -1,7 +1,7 @@ use std::fmt::Debug; use std::ops::Div; -use const_decimal::{Decimal, Integer}; +use const_decimal::{Decimal, ScaledInteger}; use criterion::measurement::WallTime; use criterion::{black_box, BatchSize, BenchmarkGroup}; use num_traits::{ConstOne, ConstZero, PrimInt}; @@ -14,7 +14,7 @@ pub fn bench_all( lo_strategy: impl Strategy + Clone, hi_strategy: impl Strategy + Clone, ) where - I: Integer + Debug + Div, + I: ScaledInteger + Debug + Div, { primitive_div::(group, lo_strategy.clone(), "lo"); decimal_div::(group, lo_strategy, "lo"); @@ -56,7 +56,7 @@ fn decimal_div( strategy: impl Strategy + Clone, strategy_label: &str, ) where - I: Integer + Debug, + I: ScaledInteger + Debug, { // Use proptest to generate arbitrary input values. let mut runner = TestRunner::deterministic(); diff --git a/benches/main.rs b/benches/main.rs index f593f35..5c87947 100644 --- a/benches/main.rs +++ b/benches/main.rs @@ -1,7 +1,7 @@ use std::fmt::Debug; use std::ops::{Div, Neg}; -use const_decimal::Integer; +use const_decimal::ScaledInteger; use criterion::measurement::WallTime; use criterion::BenchmarkGroup; use num_traits::ConstOne; @@ -67,7 +67,7 @@ fn bench_integers( hi_mul_range: impl Strategy + Clone + Debug, hi_div_range: impl Strategy + Clone + Debug, ) where - I: Integer + Arbitrary + Div, + I: ScaledInteger + Arbitrary + Div, { add::bench_all::(group); sub::bench_all::(group); diff --git a/benches/mul.rs b/benches/mul.rs index 73b6c77..f2c7900 100644 --- a/benches/mul.rs +++ b/benches/mul.rs @@ -1,6 +1,6 @@ use std::fmt::Debug; -use const_decimal::{Decimal, Integer}; +use const_decimal::{Decimal, ScaledInteger}; use criterion::measurement::WallTime; use criterion::{black_box, BatchSize, BenchmarkGroup}; use num_traits::PrimInt; @@ -13,7 +13,7 @@ pub fn bench_all( lo_strategy: impl Strategy + Clone, hi_strategy: impl Strategy + Clone, ) where - I: Integer + Debug, + I: ScaledInteger + Debug, { primitive_mul::(group, lo_strategy.clone(), "lo"); decimal_mul::(group, lo_strategy, "lo"); @@ -46,7 +46,7 @@ fn decimal_mul( strategy: impl Strategy + Clone, strategy_label: &str, ) where - I: Integer + Debug, + I: ScaledInteger + Debug, { // Use proptest to generate arbitrary input values. let mut runner = TestRunner::deterministic(); diff --git a/benches/sub.rs b/benches/sub.rs index 99e763d..5d5d135 100644 --- a/benches/sub.rs +++ b/benches/sub.rs @@ -1,4 +1,4 @@ -use const_decimal::{Decimal, Integer}; +use const_decimal::{Decimal, ScaledInteger}; use criterion::measurement::WallTime; use criterion::{black_box, BatchSize, BenchmarkGroup}; use num_traits::PrimInt; @@ -8,7 +8,7 @@ use proptest::prelude::*; pub fn bench_all(group: &mut BenchmarkGroup<'_, WallTime>) where - I: Integer + Arbitrary, + I: ScaledInteger + Arbitrary, { bench_primitive_sub::(group); bench_decimal_sub::(group); @@ -33,7 +33,7 @@ where fn bench_decimal_sub(group: &mut BenchmarkGroup<'_, WallTime>) where - I: Integer + Arbitrary, + I: ScaledInteger + Arbitrary, { // Use proptest to generate arbitrary input values. let mut runner = TestRunner::deterministic(); diff --git a/src/algorithms.rs b/src/algorithms.rs new file mode 100644 index 0000000..180d692 --- /dev/null +++ b/src/algorithms.rs @@ -0,0 +1,11 @@ +use crate::ScaledInteger; + +pub(crate) fn log10>(mut input: I) -> I { + let mut result = I::ZERO; + while input > I::ZERO { + input /= I::TEN; + result += I::ONE; + } + + result +} diff --git a/src/decimal.rs b/src/decimal.rs index 39d6b40..449109a 100644 --- a/src/decimal.rs +++ b/src/decimal.rs @@ -1,7 +1,7 @@ use std::cmp::Ordering; use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; -use crate::integer::{Integer, SignedInteger}; +use crate::integer::{ScaledInteger, SignedScaledInteger}; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] @@ -11,7 +11,7 @@ pub struct Decimal(pub I); impl Decimal where - I: Integer, + I: ScaledInteger, { pub const ZERO: Decimal = Decimal(I::ZERO); pub const ONE: Decimal = Decimal(I::SCALING_FACTOR); @@ -31,10 +31,52 @@ where Decimal(I::max_value()) } + // TODO: Add `try_from_scaled() -> Result<>`. + + /// Losslessly converts a scaled integer to this type. + /// + /// # Panics + /// + /// Panics if the integer cannot be represented without precision + /// loss/overflow. + /// + /// # Examples + /// + /// ```rust + /// use const_decimal::Decimal; + /// + /// let five = Decimal::::from_scaled(5, 0); + /// assert_eq!(five, Decimal::TWO + Decimal::TWO + Decimal::ONE); + /// assert_eq!(five.0, 5000); + /// ``` pub fn from_scaled(integer: I, scale: u8) -> Self { match scale.cmp(&D) { - Ordering::Greater => todo!(), - Ordering::Less => todo!(), + Ordering::Greater => { + // SAFETY: We know `scale > D` so this cannot underflow. + #[allow(clippy::arithmetic_side_effects)] + let divisor = I::TEN.pow(u32::from(scale - D)); + + // SAFETY: `divisor` cannot be zero as `x.pow(y)` cannot return 0. + #[allow(clippy::arithmetic_side_effects)] + let remainder = integer % divisor; + assert!( + remainder == I::ZERO, + "Cast would lose precision; input={integer}; scale={scale}; divisor={divisor}" + ); + + // SAFETY: `Decimal::mul` panics on divide by zero. + #[allow(clippy::arithmetic_side_effects)] + Decimal(integer / divisor) + } + Ordering::Less => { + // SAFETY: We know `scale < D` so this cannot underflow. + #[allow(clippy::arithmetic_side_effects)] + let multiplier = I::TEN.pow(u32::from(D - scale)); + + // SAFETY: `Decimal::mul` panics on overflow. + #[allow(clippy::arithmetic_side_effects)] + Decimal(integer * multiplier) + } Ordering::Equal => Decimal(integer), } } @@ -46,7 +88,7 @@ where impl Add for Decimal where - I: Integer, + I: ScaledInteger, { type Output = Self; @@ -58,7 +100,7 @@ where impl Sub for Decimal where - I: Integer, + I: ScaledInteger, { type Output = Self; @@ -70,7 +112,7 @@ where impl Mul for Decimal where - I: Integer, + I: ScaledInteger, { type Output = Self; @@ -82,7 +124,7 @@ where impl Div for Decimal where - I: Integer, + I: ScaledInteger, { type Output = Self; @@ -94,7 +136,7 @@ where impl Neg for Decimal where - I: SignedInteger, + I: SignedScaledInteger, { type Output = Self; @@ -105,7 +147,7 @@ where impl AddAssign for Decimal where - I: Integer, + I: ScaledInteger, { #[inline] fn add_assign(&mut self, rhs: Self) { @@ -115,7 +157,7 @@ where impl SubAssign for Decimal where - I: Integer, + I: ScaledInteger, { #[inline] fn sub_assign(&mut self, rhs: Self) { @@ -125,7 +167,7 @@ where impl MulAssign for Decimal where - I: Integer, + I: ScaledInteger, { #[inline] fn mul_assign(&mut self, rhs: Self) { @@ -135,7 +177,7 @@ where impl DivAssign for Decimal where - I: Integer, + I: ScaledInteger, { #[inline] fn div_assign(&mut self, rhs: Self) { @@ -145,10 +187,11 @@ where #[cfg(test)] mod tests { + use std::fmt::Debug; use std::ops::Shr; use malachite::num::basic::traits::Zero; - use malachite::Rational; + use malachite::{Integer, Rational}; use paste::paste; use proptest::prelude::*; @@ -402,13 +445,18 @@ mod tests { fn []() { differential_fuzz_div_assign::<$underlying, $decimals>(); } + + #[test] + fn []() { + differential_fuzz_from_scaled::<$underlying, $decimals>(); + } } }; } fn differential_fuzz_add() where - I: Integer + Arbitrary + std::panic::RefUnwindSafe, + I: ScaledInteger + Arbitrary + std::panic::RefUnwindSafe, Rational: From>, { proptest!(|(a: Decimal, b: Decimal)| { @@ -424,7 +472,7 @@ mod tests { fn differential_fuzz_sub() where - I: Integer + Arbitrary + std::panic::RefUnwindSafe, + I: ScaledInteger + Arbitrary + std::panic::RefUnwindSafe, Rational: From>, { proptest!(|(a: Decimal, b: Decimal)| { @@ -440,7 +488,7 @@ mod tests { fn differential_fuzz_mul() where - I: Integer + Arbitrary + std::panic::RefUnwindSafe + Into, + I: ScaledInteger + Arbitrary + std::panic::RefUnwindSafe + Into, Rational: From>, { proptest!(|(a: Decimal, b: Decimal)| { @@ -451,9 +499,9 @@ mod tests { let reference_out = Rational::from(a) * Rational::from(b); // If the multiplication contains truncation ignore it. - let scaling: malachite::Integer = Decimal::::SCALING_FACTOR.into(); - let divisor = malachite::Integer::from(reference_out.denominator_ref()); - if scaling % divisor != malachite::Integer::ZERO { + let scaling: Integer = Decimal::::SCALING_FACTOR.into(); + let divisor = Integer::from(reference_out.denominator_ref()); + if scaling % divisor != Integer::ZERO { // TODO: Can we assert they are within N of each other? return Ok(()); } @@ -464,7 +512,7 @@ mod tests { fn differential_fuzz_div() where - I: Integer + Arbitrary + std::panic::RefUnwindSafe + Into, + I: ScaledInteger + Arbitrary + std::panic::RefUnwindSafe + Into, Rational: From>, { proptest!(|(a: Decimal, b: Decimal)| { @@ -479,9 +527,9 @@ mod tests { let reference_out = Rational::from(a) / Rational::from(b); // If the division contains truncation ignore it. - let scaling: malachite::Integer = Decimal::::SCALING_FACTOR.into(); - let divisor = malachite::Integer::from(reference_out.denominator_ref()); - if scaling % divisor != malachite::Integer::ZERO { + let scaling: Integer = Decimal::::SCALING_FACTOR.into(); + let divisor = Integer::from(reference_out.denominator_ref()); + if scaling % divisor != Integer::ZERO { // TODO: Can we assert they are within N of each other? return Ok(()); } @@ -492,7 +540,7 @@ mod tests { fn differential_fuzz_add_assign() where - I: Integer + Arbitrary + std::panic::RefUnwindSafe, + I: ScaledInteger + Arbitrary + std::panic::RefUnwindSafe, Rational: From>, { proptest!(|(a: Decimal, b: Decimal)| { @@ -513,7 +561,7 @@ mod tests { fn differential_fuzz_sub_assign() where - I: Integer + Arbitrary + std::panic::RefUnwindSafe, + I: ScaledInteger + Arbitrary + std::panic::RefUnwindSafe, Rational: From>, { proptest!(|(a: Decimal, b: Decimal)| { @@ -534,7 +582,7 @@ mod tests { fn differential_fuzz_mul_assign() where - I: Integer + Arbitrary + std::panic::RefUnwindSafe + Into, + I: ScaledInteger + Arbitrary + std::panic::RefUnwindSafe + Into, Rational: From>, { proptest!(|(a: Decimal, b: Decimal)| { @@ -550,9 +598,9 @@ mod tests { let reference_out = Rational::from(a) * Rational::from(b); // If the multiplication contains truncation ignore it. - let scaling: malachite::Integer = Decimal::::SCALING_FACTOR.into(); - let divisor = malachite::Integer::from(reference_out.denominator_ref()); - if scaling % divisor != malachite::Integer::ZERO { + let scaling: Integer = Decimal::::SCALING_FACTOR.into(); + let divisor = Integer::from(reference_out.denominator_ref()); + if scaling % divisor != Integer::ZERO { // TODO: Can we assert they are within N of each other? return Ok(()); } @@ -563,7 +611,7 @@ mod tests { fn differential_fuzz_div_assign() where - I: Integer + Arbitrary + std::panic::RefUnwindSafe + Into, + I: ScaledInteger + Arbitrary + std::panic::RefUnwindSafe + Into, Rational: From>, { proptest!(|(a: Decimal, b: Decimal)| { @@ -579,9 +627,9 @@ mod tests { let reference_out = Rational::from(a) / Rational::from(b); // If the division contains truncation ignore it. - let scaling: malachite::Integer = Decimal::::SCALING_FACTOR.into(); - let divisor = malachite::Integer::from(reference_out.denominator_ref()); - if scaling % divisor != malachite::Integer::ZERO { + let scaling: Integer = Decimal::::SCALING_FACTOR.into(); + let divisor = Integer::from(reference_out.denominator_ref()); + if scaling % divisor != Integer::ZERO { // TODO: Can we assert they are within N of each other? return Ok(()); } @@ -590,6 +638,41 @@ mod tests { }); } + fn differential_fuzz_from_scaled() + where + I: ScaledInteger + Arbitrary + std::panic::RefUnwindSafe + Into + TryInto, + Rational: From + From>, + >::Error: Debug, + { + proptest!(|(integer: I, decimals_percent in 0..100u64)| { + let max_decimals: u64 = crate::algorithms::log10(I::max_value()).try_into().unwrap(); + let decimals = u8::try_from(decimals_percent * max_decimals / 100).unwrap(); + let scaling = I::TEN.pow(decimals as u32); + + let out = std::panic::catch_unwind(|| { + Decimal::from_scaled(integer, decimals) + }); + let reference_out = Rational::from_integers(integer.into(), scaling.into()); + + match out { + Ok(out) => assert_eq!(Rational::from(out), reference_out), + Err(_) => { + let scaling: Integer = Decimal::::SCALING_FACTOR.into(); + let remainder = &scaling % Integer::from(reference_out.denominator_ref()); + let information = &reference_out * Rational::from(scaling); + + assert!( + remainder != 0 + || information > Rational::from(I::max_value()) + || information < Rational::from(I::min_value()) , + "Failed to parse valid input; integer={integer}; input_scale={decimals}; \ + output_scale={D}", + ); + } + } + }); + } + crate::macros::apply_to_common_variants!(test_basic_ops); crate::macros::apply_to_common_variants!(fuzz_against_primitive); crate::macros::apply_to_common_variants!(differential_fuzz); diff --git a/src/display.rs b/src/display.rs index 3386ae0..4f2fcf1 100644 --- a/src/display.rs +++ b/src/display.rs @@ -5,11 +5,11 @@ use std::str::FromStr; use thiserror::Error; -use crate::{Decimal, Integer}; +use crate::{Decimal, ScaledInteger}; impl Display for Decimal where - I: Integer, + I: ScaledInteger, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let (sign, unsigned) = match self.0 < I::ONE { @@ -31,7 +31,7 @@ where impl FromStr for Decimal where - I: Integer, + I: ScaledInteger, { type Err = ParseDecimalError; @@ -210,7 +210,7 @@ mod tests { fn decimal_round_trip() where - I: Integer + Arbitrary, + I: ScaledInteger + Arbitrary, { let mut runner = TestRunner::default(); let input = Decimal::arbitrary(); @@ -248,7 +248,7 @@ mod tests { fn decimal_parse_no_panic() where - I: Integer, + I: ScaledInteger, { proptest!(|(decimal_s: String)| { let _ = decimal_s.parse::>(); @@ -277,7 +277,7 @@ mod tests { fn decimal_parse_numeric_no_panic() where - I: Integer, + I: ScaledInteger, { proptest!(|(decimal_s in "[0-9]{0,24}\\.[0-9]{0,24}")| { let _ = decimal_s.parse::>(); diff --git a/src/foreign_traits/borsh.rs b/src/foreign_traits/borsh.rs index bf26488..9b198ea 100644 --- a/src/foreign_traits/borsh.rs +++ b/src/foreign_traits/borsh.rs @@ -4,13 +4,13 @@ mod tests { use proptest::prelude::*; use crate::macros::generate_tests_for_common_variants; - use crate::{Decimal, Integer}; + use crate::{Decimal, ScaledInteger}; generate_tests_for_common_variants!(round_trip_borsh); fn round_trip_borsh() where - I: Integer + Arbitrary + BorshSerialize + BorshDeserialize, + I: ScaledInteger + Arbitrary + BorshSerialize + BorshDeserialize, { proptest!(|(input: Decimal)| { let serialized = borsh::to_vec(&input).unwrap(); diff --git a/src/foreign_traits/malachite.rs b/src/foreign_traits/malachite.rs index 7fc3883..f01b14f 100644 --- a/src/foreign_traits/malachite.rs +++ b/src/foreign_traits/malachite.rs @@ -1,11 +1,11 @@ use malachite::num::basic::integers::PrimitiveInt; use malachite::Rational; -use crate::{Decimal, Integer}; +use crate::{Decimal, ScaledInteger}; impl From> for Rational where - I: Integer + PrimitiveInt, + I: ScaledInteger + PrimitiveInt, malachite::Integer: From, { fn from(value: Decimal) -> Self { diff --git a/src/foreign_traits/proptest.rs b/src/foreign_traits/proptest.rs index 3dc20ff..ecdf864 100644 --- a/src/foreign_traits/proptest.rs +++ b/src/foreign_traits/proptest.rs @@ -3,11 +3,11 @@ use std::fmt::Debug; use proptest::arbitrary::Mapped; use proptest::prelude::{any, Arbitrary, Strategy}; -use crate::{Decimal, Integer}; +use crate::{Decimal, ScaledInteger}; impl Arbitrary for Decimal where - I: Integer + Arbitrary + Debug, + I: ScaledInteger + Arbitrary + Debug, { type Parameters = (); type Strategy = Mapped; diff --git a/src/integer.rs b/src/integer.rs index 466acb1..8c96b72 100644 --- a/src/integer.rs +++ b/src/integer.rs @@ -1,6 +1,6 @@ use std::fmt::Display; use std::num::ParseIntError; -use std::ops::{Not, Shr}; +use std::ops::{AddAssign, DivAssign, Not, Shr}; use std::str::FromStr; use num_traits::{CheckedNeg, CheckedRem, ConstOne, ConstZero, One, PrimInt, WrappingAdd}; @@ -8,7 +8,7 @@ use num_traits::{CheckedNeg, CheckedRem, ConstOne, ConstZero, One, PrimInt, Wrap use crate::cheats::Cheats; use crate::full_mul_div::FullMulDiv; -pub trait Integer: +pub trait ScaledInteger: PrimInt + ConstZero + ConstOne @@ -17,6 +17,8 @@ pub trait Integer: + CheckedRem + Not + Shr + + AddAssign + + DivAssign + Display + FromStr + Cheats @@ -24,7 +26,7 @@ pub trait Integer: { } -impl Integer for I where +impl ScaledInteger for I where I: PrimInt + ConstZero + ConstOne @@ -33,6 +35,8 @@ impl Integer for I where + CheckedRem + Not + Shr + + AddAssign + + DivAssign + Display + FromStr + Cheats @@ -40,6 +44,6 @@ impl Integer for I where { } -pub trait SignedInteger: Integer + CheckedNeg {} +pub trait SignedScaledInteger: ScaledInteger + CheckedNeg {} -impl SignedInteger for I where I: Integer + CheckedNeg {} +impl SignedScaledInteger for I where I: ScaledInteger + CheckedNeg {} diff --git a/src/lib.rs b/src/lib.rs index a5efda2..dc69a7f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,9 @@ mod integer; #[cfg(test)] #[macro_use] pub(crate) mod macros; +/// Underlying algorithms used by other operations. +#[cfg(test)] +pub(crate) mod algorithms; pub use aliases::*; pub use decimal::*; diff --git a/src/macros.rs b/src/macros.rs index 21c58f5..31ddc65 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -13,6 +13,7 @@ macro_rules! apply_to_common_variants { }; } +#[cfg(feature = "borsh")] macro_rules! generate_tests_for_common_variants { ($f:ident) => { crate::macros::generate_test!($f, u8, 1); @@ -28,6 +29,7 @@ macro_rules! generate_tests_for_common_variants { }; } +#[cfg(feature = "borsh")] macro_rules! generate_test { ($f:ident, $underlying:ty, $decimals:literal) => { ::paste::paste! { @@ -39,4 +41,6 @@ macro_rules! generate_test { }; } -pub(crate) use {apply_to_common_variants, generate_test, generate_tests_for_common_variants}; +pub(crate) use apply_to_common_variants; +#[cfg(feature = "borsh")] +pub(crate) use {generate_test, generate_tests_for_common_variants};