-
Notifications
You must be signed in to change notification settings - Fork 332
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Decimal checked multiplication and exponentiation #1239
Changes from 3 commits
56bc979
16a21b5
088171b
4fa4d6e
bbadca5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ use std::str::FromStr; | |
use thiserror::Error; | ||
|
||
use crate::errors::StdError; | ||
use crate::OverflowError; | ||
|
||
use super::Fraction; | ||
use super::Isqrt; | ||
|
@@ -151,6 +152,53 @@ impl Decimal { | |
Self::DECIMAL_PLACES as u32 | ||
} | ||
|
||
/// Multiplies one `Decimal` by another, returning an `OverflowError` if an overflow occurred. | ||
pub fn checked_mul(self, other: Self) -> Result<Self, OverflowError> { | ||
let result_as_uint256 = self.numerator().full_mul(other.numerator()) | ||
/ Uint256::from_uint128(Self::DECIMAL_FRACTIONAL); // from_uint128 is a const method and should be "free" | ||
result_as_uint256 | ||
.try_into() | ||
.map(Self) | ||
.map_err(|_| OverflowError { | ||
operation: crate::OverflowOperation::Mul, | ||
operand1: self.to_string(), | ||
operand2: other.to_string(), | ||
}) | ||
} | ||
|
||
/// Raises a value to the power of `exp`, returning an `OverflowError` if an overflow occurred. | ||
pub fn checked_pow(self, exp: u32) -> Result<Self, OverflowError> { | ||
// This uses the exponentiation by squaring algorithm: | ||
// https://en.wikipedia.org/wiki/Exponentiation_by_squaring#Basic_method | ||
|
||
fn inner(mut x: Decimal, mut n: u32) -> Result<Decimal, OverflowError> { | ||
if n == 0 { | ||
return Ok(Decimal::one()); | ||
} | ||
|
||
let mut y = Decimal::one(); | ||
|
||
while n > 1 { | ||
if n % 2 == 0 { | ||
x = x.checked_mul(x)?; | ||
n /= 2; | ||
} else { | ||
y = x.checked_mul(y)?; | ||
x = x.checked_mul(x)?; | ||
n = (n - 1) / 2; | ||
} | ||
} | ||
|
||
Ok(x * y) | ||
} | ||
|
||
inner(self, exp).map_err(|_| OverflowError { | ||
operation: crate::OverflowOperation::Pow, | ||
operand1: self.to_string(), | ||
operand2: exp.to_string(), | ||
}) | ||
} | ||
|
||
/// Returns the approximate square root as a Decimal. | ||
/// | ||
/// This should not overflow or panic. | ||
|
@@ -952,6 +1000,37 @@ mod tests { | |
let _value = Decimal::MAX * Decimal::percent(101); | ||
} | ||
|
||
#[test] | ||
fn decimal_checked_mul() { | ||
let test_data = [ | ||
(Decimal::zero(), Decimal::zero()), | ||
(Decimal::zero(), Decimal::one()), | ||
(Decimal::one(), Decimal::zero()), | ||
(Decimal::percent(10), Decimal::zero()), | ||
(Decimal::percent(10), Decimal::percent(5)), | ||
(Decimal::MAX, Decimal::one()), | ||
(Decimal::MAX / 2u128.into(), Decimal::percent(200)), | ||
(Decimal::permille(6), Decimal::permille(13)), | ||
]; | ||
|
||
// The regular std::ops::Mul is our source of truth for these tests. | ||
for (x, y) in test_data.iter().cloned() { | ||
assert_eq!(x * y, x.checked_mul(y).unwrap()); | ||
} | ||
} | ||
|
||
#[test] | ||
fn decimal_checked_mul_overflow() { | ||
assert_eq!( | ||
Decimal::MAX.checked_mul(Decimal::percent(200)), | ||
Err(OverflowError { | ||
operation: crate::OverflowOperation::Mul, | ||
operand1: Decimal::MAX.to_string(), | ||
operand2: Decimal::percent(200).to_string(), | ||
}) | ||
); | ||
} | ||
|
||
#[test] | ||
// in this test the Decimal is on the right | ||
fn uint128_decimal_multiply() { | ||
|
@@ -1068,6 +1147,89 @@ mod tests { | |
); | ||
} | ||
|
||
#[test] | ||
fn decimal_checked_pow() { | ||
for exp in 0..10 { | ||
assert_eq!(Decimal::one().checked_pow(exp).unwrap(), Decimal::one()); | ||
} | ||
|
||
// Do we leave 0^0 undefined? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about going with the Rust standard library behaviour of expecting 1 in the implementation, even though this is mathematically undefined. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense! Done. |
||
|
||
for exp in 1..10 { | ||
assert_eq!(Decimal::zero().checked_pow(exp).unwrap(), Decimal::zero()); | ||
} | ||
|
||
for num in &[ | ||
Decimal::percent(50), | ||
Decimal::percent(99), | ||
Decimal::percent(200), | ||
] { | ||
assert_eq!(num.checked_pow(0).unwrap(), Decimal::one()) | ||
} | ||
|
||
assert_eq!( | ||
Decimal::percent(20).checked_pow(2).unwrap(), | ||
Decimal::percent(4) | ||
); | ||
|
||
assert_eq!( | ||
Decimal::percent(20).checked_pow(3).unwrap(), | ||
Decimal::permille(8) | ||
); | ||
|
||
assert_eq!( | ||
Decimal::percent(200).checked_pow(4).unwrap(), | ||
Decimal::percent(1600) | ||
); | ||
|
||
assert_eq!( | ||
Decimal::percent(200).checked_pow(4).unwrap(), | ||
Decimal::percent(1600) | ||
); | ||
|
||
assert_eq!( | ||
Decimal::percent(700).checked_pow(5).unwrap(), | ||
Decimal::percent(1680700) | ||
); | ||
|
||
assert_eq!( | ||
Decimal::percent(700).checked_pow(8).unwrap(), | ||
Decimal::percent(576480100) | ||
); | ||
|
||
assert_eq!( | ||
Decimal::percent(700).checked_pow(10).unwrap(), | ||
Decimal::percent(28247524900) | ||
); | ||
|
||
assert_eq!( | ||
Decimal::percent(120).checked_pow(123).unwrap(), | ||
Decimal(5486473221892422150877397607u128.into()) | ||
); | ||
|
||
assert_eq!( | ||
Decimal::percent(10).checked_pow(2).unwrap(), | ||
Decimal(10000000000000000u128.into()) | ||
); | ||
|
||
assert_eq!( | ||
Decimal::percent(10).checked_pow(18).unwrap(), | ||
Decimal(1u128.into()) | ||
); | ||
} | ||
|
||
#[test] | ||
fn decimal_checked_pow_overflow() { | ||
assert_eq!( | ||
Decimal::MAX.checked_pow(2), | ||
Err(OverflowError { | ||
operation: crate::OverflowOperation::Pow, | ||
operand1: Decimal::MAX.to_string(), | ||
operand2: "2".to_string(), | ||
}) | ||
); | ||
} | ||
|
||
#[test] | ||
fn decimal_to_string() { | ||
// Integers | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice