use crate::error::PositiveError;
use crate::positive::{
Positive, dec_add, overflow_panic, precision_panic, unwrap_or_panic, validate_precision,
};
use approx::{AbsDiffEq, RelativeEq};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::borrow::Borrow;
use std::cmp::Ordering;
use std::fmt;
use std::ops::{Add, AddAssign, Div, Mul, MulAssign, Sub};
use std::str::FromStr;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct StrictlyPositive(Positive);
#[cold]
#[inline(never)]
fn strict_invariant_panic(op: &'static str) -> ! {
panic!("StrictlyPositive invariant broken in {op}: result would be zero or negative")
}
#[cold]
#[inline(never)]
fn strict_inverted_range_panic() -> ! {
panic!("StrictlyPositive clamp range is inverted: min is greater than max")
}
#[cold]
#[inline(never)]
fn strict_out_of_bounds(value: Decimal) -> PositiveError {
PositiveError::out_of_bounds(value, StrictlyPositive::MIN.to_dec(), Decimal::MAX)
}
#[inline]
fn strict_unwrap_or_panic(
result: Result<StrictlyPositive, PositiveError>,
op: &'static str,
) -> StrictlyPositive {
match result {
Ok(value) => value,
Err(PositiveError::OutOfBounds { .. }) => strict_invariant_panic(op),
Err(PositiveError::ArithmeticError { .. }) => overflow_panic(op),
Err(PositiveError::InvalidPrecision { precision, .. }) => precision_panic(precision),
Err(
error @ (PositiveError::InvalidValue { .. } | PositiveError::ConversionError { .. }),
) => panic!("StrictlyPositive {op} failed: {error}"),
}
}
impl StrictlyPositive {
pub const MIN: StrictlyPositive = StrictlyPositive::from_decimal_const(dec!(1e-28));
pub const MAX: StrictlyPositive = StrictlyPositive::from_decimal_const(Decimal::MAX);
pub const ONE: StrictlyPositive = StrictlyPositive::from_decimal_const(Decimal::ONE);
pub const TWO: StrictlyPositive = StrictlyPositive::from_decimal_const(Decimal::TWO);
pub const THREE: StrictlyPositive = StrictlyPositive::from_decimal_const(dec!(3));
pub const FOUR: StrictlyPositive = StrictlyPositive::from_decimal_const(dec!(4));
pub const FIVE: StrictlyPositive = StrictlyPositive::from_decimal_const(dec!(5));
pub const SIX: StrictlyPositive = StrictlyPositive::from_decimal_const(dec!(6));
pub const SEVEN: StrictlyPositive = StrictlyPositive::from_decimal_const(dec!(7));
pub const EIGHT: StrictlyPositive = StrictlyPositive::from_decimal_const(dec!(8));
pub const NINE: StrictlyPositive = StrictlyPositive::from_decimal_const(dec!(9));
pub const TEN: StrictlyPositive = StrictlyPositive::from_decimal_const(Decimal::TEN);
pub const HUNDRED: StrictlyPositive = StrictlyPositive::from_decimal_const(dec!(100));
pub const THOUSAND: StrictlyPositive = StrictlyPositive::from_decimal_const(dec!(1000));
pub const PI: StrictlyPositive = StrictlyPositive::from_decimal_const(Decimal::PI);
pub const E: StrictlyPositive = StrictlyPositive::from_decimal_const(Decimal::E);
#[inline]
#[must_use]
const fn from_decimal_const(value: Decimal) -> Self {
StrictlyPositive(Positive::from_decimal_const(value))
}
#[inline]
fn from_positive_checked(value: Positive) -> Result<Self, PositiveError> {
if value.is_zero() {
Err(strict_out_of_bounds(value.to_dec()))
} else {
Ok(StrictlyPositive(value))
}
}
#[inline]
fn lift(result: Result<Positive, PositiveError>) -> Result<Self, PositiveError> {
match result {
Ok(value) => Self::from_positive_checked(value),
Err(PositiveError::OutOfBounds { value, .. }) => Err(strict_out_of_bounds(value)),
Err(error) => Err(error),
}
}
#[must_use = "constructor returns a Result; ignoring it discards a validated invariant"]
pub fn new(value: f64) -> Result<Self, PositiveError> {
Self::lift(Positive::new(value))
}
#[must_use = "constructor returns a Result; ignoring it discards a validated invariant"]
pub fn new_decimal(value: Decimal) -> Result<Self, PositiveError> {
if value > Decimal::ZERO {
Ok(StrictlyPositive::from_decimal_const(value))
} else {
Err(strict_out_of_bounds(value))
}
}
#[inline]
#[must_use]
pub fn value(&self) -> Decimal {
self.0.to_dec()
}
#[inline]
#[must_use]
pub fn to_dec(&self) -> Decimal {
self.0.to_dec()
}
#[inline]
#[must_use]
pub fn to_positive(&self) -> Positive {
self.0
}
#[inline]
#[must_use]
pub fn to_f64(&self) -> f64 {
self.0.to_f64()
}
#[inline]
#[must_use]
pub fn to_i64_checked(&self) -> Option<i64> {
self.0.to_i64_checked()
}
#[inline]
#[must_use]
pub fn to_u64_checked(&self) -> Option<u64> {
self.0.to_u64_checked()
}
#[inline]
#[must_use]
pub fn to_usize_checked(&self) -> Option<usize> {
self.0.to_usize_checked()
}
#[inline]
#[must_use = "checked arithmetic returns a Result; ignoring it silences the overflow error"]
pub fn checked_add(&self, rhs: &Self) -> Result<Self, PositiveError> {
Self::lift(self.0.checked_add(&rhs.0))
}
#[inline]
#[must_use = "checked arithmetic returns a Result; ignoring it silences the overflow error"]
pub fn checked_add_positive(&self, rhs: &Positive) -> Result<Self, PositiveError> {
Self::lift(self.0.checked_add(rhs))
}
#[inline]
#[must_use = "checked arithmetic returns a Result; ignoring it silences the invariant error"]
pub fn checked_sub(&self, rhs: &Self) -> Result<Self, PositiveError> {
Self::lift(self.0.checked_sub(&rhs.0))
}
#[inline]
#[must_use = "checked arithmetic returns a Result; ignoring it silences the overflow error"]
pub fn checked_mul(&self, rhs: &Self) -> Result<Self, PositiveError> {
Self::lift(self.0.checked_mul(&rhs.0))
}
#[inline]
#[must_use = "checked arithmetic returns a Result; ignoring it silences the overflow error"]
pub fn checked_mul_positive(&self, rhs: &Positive) -> Result<Positive, PositiveError> {
self.0.checked_mul(rhs)
}
#[inline]
#[must_use = "checked arithmetic returns a Result; ignoring it silences the underflow error"]
pub fn checked_div(&self, rhs: &Self) -> Result<Self, PositiveError> {
Self::lift(self.0.checked_div(&rhs.0))
}
#[inline]
#[must_use = "checked arithmetic returns a Result; ignoring it silences the division-by-zero error"]
pub fn checked_div_positive(&self, rhs: &Positive) -> Result<Positive, PositiveError> {
self.0.checked_div(rhs)
}
#[must_use = "checked aggregation returns a Result; ignoring it silences the error"]
pub fn checked_sum<I, T>(iter: I) -> Result<StrictlyPositive, PositiveError>
where
I: IntoIterator<Item = T>,
T: Borrow<StrictlyPositive>,
{
let mut iter = iter.into_iter();
let first = iter.next().ok_or_else(|| {
PositiveError::arithmetic_error("sum", "an empty iterator has no strictly positive sum")
})?;
let mut total = first.borrow().to_dec();
for value in iter {
total = dec_add(total, value.borrow().to_dec(), "sum")?;
}
StrictlyPositive::new_decimal(total)
}
#[must_use]
pub fn sqrt(&self) -> Self {
strict_unwrap_or_panic(self.checked_sqrt(), "sqrt")
}
#[inline]
#[must_use = "checked mathematics returns a Result; ignoring it silences the error"]
pub fn checked_sqrt(&self) -> Result<Self, PositiveError> {
Self::lift(self.0.checked_sqrt())
}
#[inline]
#[must_use]
pub fn ln(&self) -> Decimal {
self.0.ln()
}
#[inline]
#[must_use]
pub fn log10(&self) -> Decimal {
self.0.log10()
}
#[must_use]
pub fn exp(&self) -> Self {
strict_unwrap_or_panic(self.checked_exp(), "exp")
}
#[inline]
#[must_use = "checked mathematics returns a Result; ignoring it silences the overflow error"]
pub fn checked_exp(&self) -> Result<Self, PositiveError> {
Self::lift(self.0.checked_exp())
}
#[must_use]
pub fn powi(&self, n: i64) -> Self {
strict_unwrap_or_panic(self.checked_powi(n), "powi")
}
#[inline]
#[must_use = "checked arithmetic returns a Result; ignoring it silences the error"]
pub fn checked_powi(&self, n: i64) -> Result<Self, PositiveError> {
Self::lift(self.0.checked_powi(n))
}
#[must_use]
pub fn powu(&self, n: u64) -> Self {
strict_unwrap_or_panic(self.checked_powu(n), "powu")
}
#[inline]
#[must_use = "checked arithmetic returns a Result; ignoring it silences the error"]
pub fn checked_powu(&self, n: u64) -> Result<Self, PositiveError> {
Self::lift(self.0.checked_powu(n))
}
#[must_use]
pub fn pow(&self, n: Positive) -> Self {
strict_unwrap_or_panic(self.checked_pow(n), "pow")
}
#[inline]
#[must_use = "checked arithmetic returns a Result; ignoring it silences the error"]
pub fn checked_pow(&self, n: Positive) -> Result<Self, PositiveError> {
Self::lift(self.0.checked_pow(n))
}
#[must_use]
pub fn floor(&self) -> Self {
strict_unwrap_or_panic(self.checked_floor(), "floor")
}
#[inline]
#[must_use = "checked arithmetic returns a Result; ignoring it silences the invariant error"]
pub fn checked_floor(&self) -> Result<Self, PositiveError> {
Self::lift(self.0.checked_floor())
}
#[must_use]
pub fn ceiling(&self) -> Self {
strict_unwrap_or_panic(self.checked_ceiling(), "ceiling")
}
#[inline]
#[must_use = "checked arithmetic returns a Result; ignoring it silences the error"]
pub fn checked_ceiling(&self) -> Result<Self, PositiveError> {
Self::lift(self.0.checked_ceiling())
}
#[must_use]
pub fn round(&self) -> Self {
strict_unwrap_or_panic(self.checked_round(), "round")
}
#[inline]
#[must_use = "checked arithmetic returns a Result; ignoring it silences the invariant error"]
pub fn checked_round(&self) -> Result<Self, PositiveError> {
Self::lift(self.0.checked_round())
}
#[must_use]
pub fn round_to(&self, decimal_places: u32) -> Self {
strict_unwrap_or_panic(self.checked_round_to(decimal_places), "round_to")
}
#[inline]
#[must_use = "checked arithmetic returns a Result; ignoring it silences the invariant error"]
pub fn checked_round_to(&self, decimal_places: u32) -> Result<Self, PositiveError> {
let decimal_places = validate_precision(decimal_places)?;
Self::lift(self.0.checked_round_to(decimal_places))
}
#[inline]
#[must_use]
pub fn max(self, other: Self) -> Self {
if self >= other { self } else { other }
}
#[inline]
#[must_use]
pub fn min(self, other: Self) -> Self {
if self <= other { self } else { other }
}
#[must_use]
pub fn clamp(self, min: Self, max: Self) -> Self {
match self.checked_clamp(min, max) {
Ok(clamped) => clamped,
Err(_) => strict_inverted_range_panic(),
}
}
#[must_use = "checked clamping returns a Result; ignoring it silences the inverted-range error"]
pub fn checked_clamp(self, min: Self, max: Self) -> Result<Self, PositiveError> {
if min > max {
return Err(PositiveError::out_of_bounds(
min.to_dec(),
StrictlyPositive::MIN.to_dec(),
max.to_dec(),
));
}
Ok(if self < min {
min
} else if self > max {
max
} else {
self
})
}
}
impl fmt::Display for StrictlyPositive {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl fmt::Debug for StrictlyPositive {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
}
}
impl Serialize for StrictlyPositive {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
Serialize::serialize(&self.0, serializer)
}
}
impl<'de> Deserialize<'de> for StrictlyPositive {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = Positive::deserialize(deserializer)?;
StrictlyPositive::from_positive_checked(value).map_err(serde::de::Error::custom)
}
}
#[cfg(feature = "utoipa")]
impl utoipa::PartialSchema for StrictlyPositive {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
utoipa::openapi::ObjectBuilder::new()
.schema_type(utoipa::openapi::schema::Type::String)
.description(Some(
"A decimal value that is guaranteed to be strictly greater than zero, \
serialised as an exact decimal string.",
))
.into()
}
}
#[cfg(feature = "utoipa")]
impl utoipa::ToSchema for StrictlyPositive {
fn name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("StrictlyPositive")
}
}
impl FromStr for StrictlyPositive {
type Err = PositiveError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::lift(Positive::from_str(s))
}
}
impl From<StrictlyPositive> for Positive {
#[inline]
fn from(value: StrictlyPositive) -> Self {
value.0
}
}
impl From<StrictlyPositive> for Decimal {
#[inline]
fn from(value: StrictlyPositive) -> Self {
value.to_dec()
}
}
impl From<StrictlyPositive> for f64 {
#[inline]
fn from(value: StrictlyPositive) -> Self {
value.to_f64()
}
}
impl AsRef<Positive> for StrictlyPositive {
#[inline]
fn as_ref(&self) -> &Positive {
&self.0
}
}
impl TryFrom<Positive> for StrictlyPositive {
type Error = PositiveError;
#[inline]
fn try_from(value: Positive) -> Result<Self, Self::Error> {
StrictlyPositive::from_positive_checked(value)
}
}
impl TryFrom<Decimal> for StrictlyPositive {
type Error = PositiveError;
#[inline]
fn try_from(value: Decimal) -> Result<Self, Self::Error> {
StrictlyPositive::new_decimal(value)
}
}
impl TryFrom<&Decimal> for StrictlyPositive {
type Error = PositiveError;
#[inline]
fn try_from(value: &Decimal) -> Result<Self, Self::Error> {
StrictlyPositive::new_decimal(*value)
}
}
impl TryFrom<f64> for StrictlyPositive {
type Error = PositiveError;
#[inline]
fn try_from(value: f64) -> Result<Self, Self::Error> {
StrictlyPositive::new(value)
}
}
impl TryFrom<i64> for StrictlyPositive {
type Error = PositiveError;
#[inline]
fn try_from(value: i64) -> Result<Self, Self::Error> {
StrictlyPositive::new_decimal(Decimal::from(value))
}
}
impl TryFrom<u64> for StrictlyPositive {
type Error = PositiveError;
#[inline]
fn try_from(value: u64) -> Result<Self, Self::Error> {
StrictlyPositive::new_decimal(Decimal::from(value))
}
}
impl TryFrom<usize> for StrictlyPositive {
type Error = PositiveError;
#[inline]
fn try_from(value: usize) -> Result<Self, Self::Error> {
StrictlyPositive::new_decimal(Decimal::from(value))
}
}
impl TryFrom<StrictlyPositive> for u64 {
type Error = PositiveError;
#[inline]
fn try_from(value: StrictlyPositive) -> Result<Self, Self::Error> {
u64::try_from(value.0)
}
}
impl TryFrom<StrictlyPositive> for i64 {
type Error = PositiveError;
#[inline]
fn try_from(value: StrictlyPositive) -> Result<Self, Self::Error> {
i64::try_from(value.0)
}
}
impl TryFrom<StrictlyPositive> for usize {
type Error = PositiveError;
#[inline]
fn try_from(value: StrictlyPositive) -> Result<Self, Self::Error> {
usize::try_from(value.0)
}
}
impl PartialEq<Positive> for StrictlyPositive {
#[inline]
fn eq(&self, other: &Positive) -> bool {
self.0 == *other
}
}
impl PartialEq<StrictlyPositive> for Positive {
#[inline]
fn eq(&self, other: &StrictlyPositive) -> bool {
*self == other.0
}
}
impl PartialOrd<Positive> for StrictlyPositive {
#[inline]
fn partial_cmp(&self, other: &Positive) -> Option<Ordering> {
self.0.partial_cmp(other)
}
}
impl PartialOrd<StrictlyPositive> for Positive {
#[inline]
fn partial_cmp(&self, other: &StrictlyPositive) -> Option<Ordering> {
self.partial_cmp(&other.0)
}
}
impl PartialEq<Decimal> for StrictlyPositive {
#[inline]
fn eq(&self, other: &Decimal) -> bool {
self.0 == *other
}
}
impl PartialEq<StrictlyPositive> for Decimal {
#[inline]
fn eq(&self, other: &StrictlyPositive) -> bool {
*self == other.0
}
}
impl PartialOrd<Decimal> for StrictlyPositive {
#[inline]
fn partial_cmp(&self, other: &Decimal) -> Option<Ordering> {
self.0.partial_cmp(other)
}
}
impl PartialOrd<StrictlyPositive> for Decimal {
#[inline]
fn partial_cmp(&self, other: &StrictlyPositive) -> Option<Ordering> {
self.partial_cmp(&other.0)
}
}
impl PartialEq<f64> for StrictlyPositive {
#[inline]
fn eq(&self, other: &f64) -> bool {
self.0 == *other
}
}
impl PartialEq<StrictlyPositive> for f64 {
#[inline]
fn eq(&self, other: &StrictlyPositive) -> bool {
*self == other.0
}
}
impl PartialOrd<f64> for StrictlyPositive {
#[inline]
fn partial_cmp(&self, other: &f64) -> Option<Ordering> {
self.0.partial_cmp(other)
}
}
impl PartialOrd<StrictlyPositive> for f64 {
#[inline]
fn partial_cmp(&self, other: &StrictlyPositive) -> Option<Ordering> {
self.partial_cmp(&other.0)
}
}
impl AbsDiffEq for StrictlyPositive {
type Epsilon = Decimal;
fn default_epsilon() -> Self::Epsilon {
Positive::default_epsilon()
}
fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
self.0.abs_diff_eq(&other.0, epsilon)
}
}
impl RelativeEq for StrictlyPositive {
fn default_max_relative() -> Self::Epsilon {
Positive::default_max_relative()
}
fn relative_eq(
&self,
other: &Self,
epsilon: Self::Epsilon,
max_relative: Self::Epsilon,
) -> bool {
self.0.relative_eq(&other.0, epsilon, max_relative)
}
}
impl Add for StrictlyPositive {
type Output = StrictlyPositive;
#[inline]
fn add(self, rhs: StrictlyPositive) -> StrictlyPositive {
strict_unwrap_or_panic(self.checked_add(&rhs), "add")
}
}
impl Add<Positive> for StrictlyPositive {
type Output = StrictlyPositive;
#[inline]
fn add(self, rhs: Positive) -> StrictlyPositive {
strict_unwrap_or_panic(self.checked_add_positive(&rhs), "add")
}
}
impl Add<StrictlyPositive> for Positive {
type Output = StrictlyPositive;
#[inline]
fn add(self, rhs: StrictlyPositive) -> StrictlyPositive {
strict_unwrap_or_panic(rhs.checked_add_positive(&self), "add")
}
}
impl AddAssign for StrictlyPositive {
#[inline]
fn add_assign(&mut self, rhs: StrictlyPositive) {
*self = strict_unwrap_or_panic(self.checked_add(&rhs), "add_assign");
}
}
impl AddAssign<Positive> for StrictlyPositive {
#[inline]
fn add_assign(&mut self, rhs: Positive) {
*self = strict_unwrap_or_panic(self.checked_add_positive(&rhs), "add_assign");
}
}
impl Sub for StrictlyPositive {
type Output = Positive;
#[inline]
fn sub(self, rhs: StrictlyPositive) -> Positive {
unwrap_or_panic(self.0.checked_sub(&rhs.0), "sub")
}
}
impl Mul for StrictlyPositive {
type Output = StrictlyPositive;
#[inline]
fn mul(self, rhs: StrictlyPositive) -> StrictlyPositive {
strict_unwrap_or_panic(self.checked_mul(&rhs), "mul")
}
}
impl Mul<Positive> for StrictlyPositive {
type Output = Positive;
#[inline]
fn mul(self, rhs: Positive) -> Positive {
self.0 * rhs
}
}
impl Mul<StrictlyPositive> for Positive {
type Output = Positive;
#[inline]
fn mul(self, rhs: StrictlyPositive) -> Positive {
self * rhs.0
}
}
impl MulAssign for StrictlyPositive {
#[inline]
fn mul_assign(&mut self, rhs: StrictlyPositive) {
*self = strict_unwrap_or_panic(self.checked_mul(&rhs), "mul_assign");
}
}
impl Div for StrictlyPositive {
type Output = StrictlyPositive;
#[inline]
fn div(self, rhs: StrictlyPositive) -> StrictlyPositive {
strict_unwrap_or_panic(self.checked_div(&rhs), "div")
}
}
impl Div<Positive> for StrictlyPositive {
type Output = Positive;
#[inline]
fn div(self, rhs: Positive) -> Positive {
self.0 / rhs
}
}
impl Div<StrictlyPositive> for Positive {
type Output = Positive;
#[inline]
fn div(self, rhs: StrictlyPositive) -> Positive {
self / rhs.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strictly_positive_repr_is_transparent_over_decimal() {
assert_eq!(
std::mem::size_of::<StrictlyPositive>(),
std::mem::size_of::<Decimal>()
);
}
#[test]
fn test_strictly_positive_constants_are_strictly_positive() {
let constants = [
StrictlyPositive::MIN,
StrictlyPositive::MAX,
StrictlyPositive::ONE,
StrictlyPositive::TWO,
StrictlyPositive::THREE,
StrictlyPositive::FOUR,
StrictlyPositive::FIVE,
StrictlyPositive::SIX,
StrictlyPositive::SEVEN,
StrictlyPositive::EIGHT,
StrictlyPositive::NINE,
StrictlyPositive::TEN,
StrictlyPositive::HUNDRED,
StrictlyPositive::THOUSAND,
StrictlyPositive::PI,
StrictlyPositive::E,
];
for constant in constants {
assert!(constant.to_dec() > Decimal::ZERO);
assert_eq!(
StrictlyPositive::new_decimal(constant.to_dec()),
Ok(constant)
);
}
}
#[test]
fn test_strictly_positive_min_is_smallest_decimal_step() {
assert_eq!(StrictlyPositive::MIN.to_dec(), Decimal::new(1, 28));
}
#[test]
fn test_strictly_positive_lift_relabels_bounds() {
let err = StrictlyPositive::lift(Positive::new_decimal(dec!(-1)));
assert_eq!(
err,
Err(PositiveError::OutOfBounds {
value: dec!(-1),
min: Decimal::new(1, 28),
max: Decimal::MAX,
})
);
}
}