Skip to main content

paft_decimal/
constrained.rs

1//! Decimal newtypes for invariants that hold across providers.
2
3use std::fmt;
4
5use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
6
7use crate::{Decimal, one, serde::canonical_str, to_canonical_string, zero};
8
9/// Error returned when a decimal does not satisfy a constrained decimal type.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct DecimalConstraintError {
12    type_name: &'static str,
13    expected: &'static str,
14    value: Decimal,
15}
16
17impl DecimalConstraintError {
18    #[cfg(not(feature = "bigdecimal"))]
19    const fn new(type_name: &'static str, expected: &'static str, value: &Decimal) -> Self {
20        Self {
21            type_name,
22            expected,
23            value: *value,
24        }
25    }
26
27    #[cfg(feature = "bigdecimal")]
28    fn new(type_name: &'static str, expected: &'static str, value: &Decimal) -> Self {
29        Self {
30            type_name,
31            expected,
32            value: value.clone(),
33        }
34    }
35
36    /// Name of the constrained decimal type that rejected the value.
37    #[must_use]
38    pub const fn type_name(&self) -> &'static str {
39        self.type_name
40    }
41
42    /// Human-readable description of the accepted range.
43    #[must_use]
44    pub const fn expected(&self) -> &'static str {
45        self.expected
46    }
47
48    /// Rejected decimal value.
49    #[must_use]
50    pub const fn value(&self) -> &Decimal {
51        &self.value
52    }
53}
54
55impl fmt::Display for DecimalConstraintError {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        write!(
58            f,
59            "{} expected {}, got {}",
60            self.type_name, self.expected, self.value
61        )
62    }
63}
64
65impl std::error::Error for DecimalConstraintError {}
66
67fn is_non_negative(value: &Decimal) -> bool {
68    let zero = zero();
69    value >= &zero
70}
71
72fn is_positive(value: &Decimal) -> bool {
73    let zero = zero();
74    value > &zero
75}
76
77fn is_ratio(value: &Decimal) -> bool {
78    let zero = zero();
79    let one = one();
80    value >= &zero && value <= &one
81}
82
83/// Decimal constrained to `x >= 0`.
84#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
85#[cfg_attr(not(feature = "bigdecimal"), derive(Copy))]
86pub struct NonNegativeDecimal(Decimal);
87
88impl NonNegativeDecimal {
89    const EXPECTED: &'static str = "a decimal greater than or equal to 0";
90
91    /// Builds a non-negative decimal.
92    ///
93    /// # Errors
94    /// Returns [`DecimalConstraintError`] when `value < 0`.
95    pub fn new(value: Decimal) -> Result<Self, DecimalConstraintError> {
96        if is_non_negative(&value) {
97            Ok(Self(value))
98        } else {
99            Err(DecimalConstraintError::new(
100                "NonNegativeDecimal",
101                Self::EXPECTED,
102                &value,
103            ))
104        }
105    }
106
107    /// Returns the wrapped decimal by reference.
108    #[must_use]
109    pub const fn as_decimal(&self) -> &Decimal {
110        &self.0
111    }
112
113    /// Returns the wrapped decimal.
114    #[must_use]
115    #[cfg(not(feature = "bigdecimal"))]
116    pub const fn into_inner(self) -> Decimal {
117        self.0
118    }
119
120    /// Returns the wrapped decimal.
121    #[must_use]
122    #[cfg(feature = "bigdecimal")]
123    pub fn into_inner(self) -> Decimal {
124        self.0
125    }
126}
127
128impl AsRef<Decimal> for NonNegativeDecimal {
129    fn as_ref(&self) -> &Decimal {
130        self.as_decimal()
131    }
132}
133
134impl TryFrom<Decimal> for NonNegativeDecimal {
135    type Error = DecimalConstraintError;
136
137    fn try_from(value: Decimal) -> Result<Self, Self::Error> {
138        Self::new(value)
139    }
140}
141
142impl From<NonNegativeDecimal> for Decimal {
143    fn from(value: NonNegativeDecimal) -> Self {
144        value.into_inner()
145    }
146}
147
148impl fmt::Display for NonNegativeDecimal {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        f.write_str(&to_canonical_string(&self.0))
151    }
152}
153
154impl Serialize for NonNegativeDecimal {
155    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
156    where
157        S: Serializer,
158    {
159        canonical_str::serialize(&self.0, serializer)
160    }
161}
162
163impl<'de> Deserialize<'de> for NonNegativeDecimal {
164    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
165    where
166        D: Deserializer<'de>,
167    {
168        let value = canonical_str::deserialize(deserializer)?;
169        Self::new(value).map_err(de::Error::custom)
170    }
171}
172
173/// Decimal constrained to `x > 0`.
174#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
175#[cfg_attr(not(feature = "bigdecimal"), derive(Copy))]
176pub struct PositiveDecimal(Decimal);
177
178impl PositiveDecimal {
179    const EXPECTED: &'static str = "a decimal greater than 0";
180
181    /// Builds a positive decimal.
182    ///
183    /// # Errors
184    /// Returns [`DecimalConstraintError`] when `value <= 0`.
185    pub fn new(value: Decimal) -> Result<Self, DecimalConstraintError> {
186        if is_positive(&value) {
187            Ok(Self(value))
188        } else {
189            Err(DecimalConstraintError::new(
190                "PositiveDecimal",
191                Self::EXPECTED,
192                &value,
193            ))
194        }
195    }
196
197    /// Returns the wrapped decimal by reference.
198    #[must_use]
199    pub const fn as_decimal(&self) -> &Decimal {
200        &self.0
201    }
202
203    /// Returns the wrapped decimal.
204    #[must_use]
205    #[cfg(not(feature = "bigdecimal"))]
206    pub const fn into_inner(self) -> Decimal {
207        self.0
208    }
209
210    /// Returns the wrapped decimal.
211    #[must_use]
212    #[cfg(feature = "bigdecimal")]
213    pub fn into_inner(self) -> Decimal {
214        self.0
215    }
216}
217
218impl AsRef<Decimal> for PositiveDecimal {
219    fn as_ref(&self) -> &Decimal {
220        self.as_decimal()
221    }
222}
223
224impl TryFrom<Decimal> for PositiveDecimal {
225    type Error = DecimalConstraintError;
226
227    fn try_from(value: Decimal) -> Result<Self, Self::Error> {
228        Self::new(value)
229    }
230}
231
232impl From<PositiveDecimal> for Decimal {
233    fn from(value: PositiveDecimal) -> Self {
234        value.into_inner()
235    }
236}
237
238impl From<PositiveDecimal> for NonNegativeDecimal {
239    fn from(value: PositiveDecimal) -> Self {
240        Self(value.into_inner())
241    }
242}
243
244impl fmt::Display for PositiveDecimal {
245    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246        f.write_str(&to_canonical_string(&self.0))
247    }
248}
249
250impl Serialize for PositiveDecimal {
251    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
252    where
253        S: Serializer,
254    {
255        canonical_str::serialize(&self.0, serializer)
256    }
257}
258
259impl<'de> Deserialize<'de> for PositiveDecimal {
260    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
261    where
262        D: Deserializer<'de>,
263    {
264        let value = canonical_str::deserialize(deserializer)?;
265        Self::new(value).map_err(de::Error::custom)
266    }
267}
268
269/// Decimal constrained to `0 <= x <= 1`.
270#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
271#[cfg_attr(not(feature = "bigdecimal"), derive(Copy))]
272pub struct Ratio(Decimal);
273
274impl Ratio {
275    const EXPECTED: &'static str = "a decimal between 0 and 1 inclusive";
276
277    /// Builds a ratio.
278    ///
279    /// # Errors
280    /// Returns [`DecimalConstraintError`] when `value < 0` or `value > 1`.
281    pub fn new(value: Decimal) -> Result<Self, DecimalConstraintError> {
282        if is_ratio(&value) {
283            Ok(Self(value))
284        } else {
285            Err(DecimalConstraintError::new("Ratio", Self::EXPECTED, &value))
286        }
287    }
288
289    /// Returns the wrapped decimal by reference.
290    #[must_use]
291    pub const fn as_decimal(&self) -> &Decimal {
292        &self.0
293    }
294
295    /// Returns the wrapped decimal.
296    #[must_use]
297    #[cfg(not(feature = "bigdecimal"))]
298    pub const fn into_inner(self) -> Decimal {
299        self.0
300    }
301
302    /// Returns the wrapped decimal.
303    #[must_use]
304    #[cfg(feature = "bigdecimal")]
305    pub fn into_inner(self) -> Decimal {
306        self.0
307    }
308}
309
310impl AsRef<Decimal> for Ratio {
311    fn as_ref(&self) -> &Decimal {
312        self.as_decimal()
313    }
314}
315
316impl TryFrom<Decimal> for Ratio {
317    type Error = DecimalConstraintError;
318
319    fn try_from(value: Decimal) -> Result<Self, Self::Error> {
320        Self::new(value)
321    }
322}
323
324impl From<Ratio> for Decimal {
325    fn from(value: Ratio) -> Self {
326        value.into_inner()
327    }
328}
329
330impl From<Ratio> for NonNegativeDecimal {
331    fn from(value: Ratio) -> Self {
332        Self(value.into_inner())
333    }
334}
335
336impl fmt::Display for Ratio {
337    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
338        f.write_str(&to_canonical_string(&self.0))
339    }
340}
341
342impl Serialize for Ratio {
343    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
344    where
345        S: Serializer,
346    {
347        canonical_str::serialize(&self.0, serializer)
348    }
349}
350
351impl<'de> Deserialize<'de> for Ratio {
352    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
353    where
354        D: Deserializer<'de>,
355    {
356        let value = canonical_str::deserialize(deserializer)?;
357        Self::new(value).map_err(de::Error::custom)
358    }
359}