diff --git a/CHANGELOG.md b/CHANGELOG.md index bd6ff727a6..af7a4f1f2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to - cosmwasm-std: Implement `checked_add`/`_sub`/`_div`/`_rem` for `Decimal`/`Decimal256`. - cosmwasm-std: Implement `pow`/`saturating_pow` for `Decimal`/`Decimal256`. +- cosmwasm-std: Implement `ceil`/`floor` for `Decimal`/`Decimal256`. [#1334]: https://github.com/CosmWasm/cosmwasm/pull/1334 diff --git a/packages/std/src/errors/mod.rs b/packages/std/src/errors/mod.rs index 64b1e96f92..bf7e61f433 100644 --- a/packages/std/src/errors/mod.rs +++ b/packages/std/src/errors/mod.rs @@ -6,7 +6,7 @@ mod verification_error; pub use recover_pubkey_error::RecoverPubkeyError; pub use std_error::{ CheckedFromRatioError, CheckedMultiplyRatioError, ConversionOverflowError, DivideByZeroError, - OverflowError, OverflowOperation, StdError, StdResult, + OverflowError, OverflowOperation, RoundUpOverflowError, StdError, StdResult, }; pub use system_error::SystemError; pub use verification_error::VerificationError; diff --git a/packages/std/src/errors/std_error.rs b/packages/std/src/errors/std_error.rs index 165ba20fb6..4993c29d5c 100644 --- a/packages/std/src/errors/std_error.rs +++ b/packages/std/src/errors/std_error.rs @@ -544,6 +544,10 @@ pub enum CheckedFromRatioError { Overflow, } +#[derive(Error, Debug, PartialEq, Eq)] +#[error("Round up operation failed because of overflow")] +pub struct RoundUpOverflowError; + #[cfg(test)] mod tests { use super::*; diff --git a/packages/std/src/math/decimal.rs b/packages/std/src/math/decimal.rs index cd00a4ccdb..3d88fc0f56 100644 --- a/packages/std/src/math/decimal.rs +++ b/packages/std/src/math/decimal.rs @@ -9,7 +9,7 @@ use thiserror::Error; use crate::errors::{ CheckedFromRatioError, CheckedMultiplyRatioError, DivideByZeroError, OverflowError, - OverflowOperation, StdError, + OverflowOperation, RoundUpOverflowError, StdError, }; use super::Fraction; @@ -184,6 +184,31 @@ impl Decimal { Self::DECIMAL_PLACES as u32 } + /// Rounds value down after decimal places. + pub fn floor(&self) -> Self { + Self((self.0 / Self::DECIMAL_FRACTIONAL) * Self::DECIMAL_FRACTIONAL) + } + + /// Rounds value up after decimal places. Panics on overflow. + pub fn ceil(&self) -> Self { + match self.checked_ceil() { + Ok(value) => value, + Err(_) => panic!("attempt to ceil with overflow"), + } + } + + /// Rounds value up after decimal places. Returns OverflowError on overflow. + pub fn checked_ceil(&self) -> Result { + let floor = self.floor(); + if &floor == self { + Ok(floor) + } else { + floor + .checked_add(Decimal::one()) + .map_err(|_| RoundUpOverflowError) + } + } + pub fn checked_add(self, other: Self) -> Result { self.0 .checked_add(other.0) @@ -1872,4 +1897,37 @@ mod tests { ); assert_eq!(Decimal::MAX.saturating_pow(2u32), Decimal::MAX); } + + #[test] + fn decimal_rounding() { + assert_eq!(Decimal::one().floor(), Decimal::one()); + assert_eq!(Decimal::percent(150).floor(), Decimal::one()); + assert_eq!(Decimal::percent(199).floor(), Decimal::one()); + assert_eq!(Decimal::percent(200).floor(), Decimal::percent(200)); + assert_eq!(Decimal::percent(99).floor(), Decimal::zero()); + + assert_eq!(Decimal::one().ceil(), Decimal::one()); + assert_eq!(Decimal::percent(150).ceil(), Decimal::percent(200)); + assert_eq!(Decimal::percent(199).ceil(), Decimal::percent(200)); + assert_eq!(Decimal::percent(99).ceil(), Decimal::one()); + assert_eq!(Decimal(Uint128::from(1u128)).ceil(), Decimal::one()); + } + + #[test] + #[should_panic(expected = "attempt to ceil with overflow")] + fn decimal_ceil_panics() { + let _ = Decimal::MAX.ceil(); + } + + #[test] + fn decimal_checked_ceil() { + assert_eq!( + Decimal::percent(199).checked_ceil(), + Ok(Decimal::percent(200)) + ); + assert!(matches!( + Decimal::MAX.checked_ceil(), + Err(RoundUpOverflowError { .. }) + )); + } } diff --git a/packages/std/src/math/decimal256.rs b/packages/std/src/math/decimal256.rs index 508191c6fa..ede9a0a969 100644 --- a/packages/std/src/math/decimal256.rs +++ b/packages/std/src/math/decimal256.rs @@ -9,7 +9,7 @@ use thiserror::Error; use crate::errors::{ CheckedFromRatioError, CheckedMultiplyRatioError, DivideByZeroError, OverflowError, - OverflowOperation, StdError, + OverflowOperation, RoundUpOverflowError, StdError, }; use crate::{Decimal, Uint512}; @@ -197,6 +197,31 @@ impl Decimal256 { Self::DECIMAL_PLACES as u32 } + /// Rounds value down after decimal places. + pub fn floor(&self) -> Self { + Self((self.0 / Self::DECIMAL_FRACTIONAL) * Self::DECIMAL_FRACTIONAL) + } + + /// Rounds value up after decimal places. Panics on overflow. + pub fn ceil(&self) -> Self { + match self.checked_ceil() { + Ok(value) => value, + Err(_) => panic!("attempt to ceil with overflow"), + } + } + + /// Rounds value up after decimal places. Returns OverflowError on overflow. + pub fn checked_ceil(&self) -> Result { + let floor = self.floor(); + if &floor == self { + Ok(floor) + } else { + floor + .checked_add(Decimal256::one()) + .map_err(|_| RoundUpOverflowError) + } + } + pub fn checked_add(self, other: Self) -> Result { self.0 .checked_add(other.0) @@ -2022,4 +2047,34 @@ mod tests { ); assert_eq!(Decimal256::MAX.saturating_pow(2u32), Decimal256::MAX); } + + #[test] + fn decimal256_rounding() { + assert_eq!(Decimal256::one().floor(), Decimal256::one()); + assert_eq!(Decimal256::percent(150).floor(), Decimal256::one()); + assert_eq!(Decimal256::percent(199).floor(), Decimal256::one()); + assert_eq!(Decimal256::percent(200).floor(), Decimal256::percent(200)); + assert_eq!(Decimal256::percent(99).floor(), Decimal256::zero()); + + assert_eq!(Decimal256::one().ceil(), Decimal256::one()); + assert_eq!(Decimal256::percent(150).ceil(), Decimal256::percent(200)); + assert_eq!(Decimal256::percent(199).ceil(), Decimal256::percent(200)); + assert_eq!(Decimal256::percent(99).ceil(), Decimal256::one()); + assert_eq!(Decimal256(Uint256::from(1u128)).ceil(), Decimal256::one()); + } + + #[test] + #[should_panic(expected = "attempt to ceil with overflow")] + fn decimal256_ceil_panics() { + let _ = Decimal256::MAX.ceil(); + } + + #[test] + fn decimal256_checked_ceil() { + assert_eq!( + Decimal256::percent(199).checked_ceil(), + Ok(Decimal256::percent(200)) + ); + assert_eq!(Decimal256::MAX.checked_ceil(), Err(RoundUpOverflowError)); + } }