Skip to main content

ocpi_tariffs/
number.rs

1//! We represent the OCPI spec Number as a `Decimal` and serialize and deserialize to the precision defined in the OCPI spec.
2//!
3//! <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#14-number-type>
4
5use std::{borrow::Cow, fmt};
6
7use rust_decimal::Decimal;
8
9use crate::{
10    into_caveat, json,
11    warning::{self, IntoCaveat},
12};
13
14/// The scale for numerical values as defined in the OCPI spec.
15///
16/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#14-number-type>
17pub const SCALE: u32 = 4;
18
19/// The warnings that can happen when parsing or linting a numerical value.
20#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
21pub enum WarningKind {
22    /// Numerical strings do not need to have escape codes.
23    ContainsEscapeCodes,
24
25    /// The value provided exceeds `Decimal::MAX`.
26    ExceedsMaximumPossibleValue,
27
28    /// The JSON value given is not a number.
29    InvalidType,
30
31    /// The value provided is less than `Decimal::MIN`.
32    LessThanMinimumPossibleValue,
33
34    /// An underflow is when there are more fractional digits than can be represented within `Decimal`.
35    Underflow,
36}
37
38impl fmt::Display for WarningKind {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        match self {
41            WarningKind::ContainsEscapeCodes => write!(
42                f,
43                "The value contains escape codes but it does not need them"
44            ),
45            WarningKind::InvalidType => write!(f, "The value should be a number."),
46            WarningKind::ExceedsMaximumPossibleValue => {
47                write!(
48                    f,
49                    "The value provided exceeds `79,228,162,514,264,337,593,543,950,335`."
50                )
51            }
52            WarningKind::LessThanMinimumPossibleValue => write!(
53                f,
54                "The value provided is less than `-79,228,162,514,264,337,593,543,950,335`."
55            ),
56            WarningKind::Underflow => write!(
57                f,
58                "An underflow is when there are more than 28 fractional digits"
59            ),
60        }
61    }
62}
63
64impl warning::Kind for WarningKind {
65    fn id(&self) -> Cow<'static, str> {
66        match self {
67            WarningKind::ContainsEscapeCodes => "contains_escape_codes".into(),
68            WarningKind::InvalidType => "invalid_type".into(),
69            WarningKind::ExceedsMaximumPossibleValue => "exceeds_maximum_possible_value".into(),
70            WarningKind::LessThanMinimumPossibleValue => "less_than_minimum_possible_value".into(),
71            WarningKind::Underflow => "underflow".into(),
72        }
73    }
74}
75
76into_caveat!(Decimal);
77
78impl json::FromJson<'_, '_> for Decimal {
79    type WarningKind = WarningKind;
80
81    fn from_json(elem: &json::Element<'_>) -> crate::Verdict<Self, Self::WarningKind> {
82        let warnings = warning::Set::new();
83        let value = elem.as_value();
84
85        let Some(s) = value.as_number() else {
86            return warnings.bail(WarningKind::InvalidType, elem);
87        };
88
89        let mut decimal = match Decimal::from_str_exact(s) {
90            Ok(v) => v,
91            Err(err) => {
92                let kind = match err {
93                    rust_decimal::Error::ExceedsMaximumPossibleValue => {
94                        WarningKind::ExceedsMaximumPossibleValue
95                    }
96                    rust_decimal::Error::LessThanMinimumPossibleValue => {
97                        WarningKind::LessThanMinimumPossibleValue
98                    }
99                    rust_decimal::Error::Underflow => WarningKind::Underflow,
100                    rust_decimal::Error::ConversionTo(_) => unreachable!("This is only triggered when converting to numerical types"),
101                    rust_decimal::Error::ErrorString(_) => unreachable!("rust_decimal docs state: This is a legacy/deprecated error type retained for backwards compatibility."),
102                    rust_decimal::Error::ScaleExceedsMaximumPrecision(_) => unreachable!("`Decimal::from_str_exact` uses a scale of zero")
103                };
104
105                return warnings.bail(kind, elem);
106            }
107        };
108
109        decimal.rescale(SCALE);
110        Ok(decimal.into_caveat(warnings))
111    }
112}
113
114pub(crate) trait FromDecimal {
115    fn from_decimal(d: Decimal) -> Self;
116}
117
118/// All `Decimal`s should be rescaled to scale defined in the OCPI specs.
119///
120/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#14-number-type>
121impl FromDecimal for Decimal {
122    fn from_decimal(mut d: Decimal) -> Self {
123        d.rescale(SCALE);
124        d
125    }
126}
127
128/// Truncate a `Decimal` or `Decimal`-like value to the scale defined in the OCPI spec.
129///
130/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#14-number-type>
131pub trait TruncateDecimal {
132    /// Truncate a `Decimal` or `Decimal`-like value to the scale defined in the OCPI spec.
133    ///
134    /// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#14-number-type>
135    #[must_use]
136    fn truncate_to_ocpi_scale(self) -> Self;
137}
138
139impl TruncateDecimal for Decimal {
140    fn truncate_to_ocpi_scale(self) -> Self {
141        self.trunc_with_scale(SCALE)
142    }
143}
144
145impl<T: TruncateDecimal> TruncateDecimal for Option<T> {
146    fn truncate_to_ocpi_scale(self) -> Self {
147        self.map(TruncateDecimal::truncate_to_ocpi_scale)
148    }
149}
150
151/// Impl a `Decimal` based newtype.
152///
153/// All `Decimal` newtypes impl and `serde::Deserialize` which apply the precision
154/// defined in the OCPI spec.
155///
156/// <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#14-number-type>
157#[doc(hidden)]
158#[macro_export]
159macro_rules! impl_dec_newtype {
160    ($kind:ident, $unit:literal) => {
161        impl $kind {
162            #[must_use]
163            pub fn zero() -> Self {
164                use num_traits::Zero as _;
165
166                Self(Decimal::zero())
167            }
168
169            pub fn is_zero(&self) -> bool {
170                self.0.is_zero()
171            }
172
173            /// Round this number to the OCPI specified amount of decimals.
174            #[must_use]
175            pub fn rescale(mut self) -> Self {
176                self.0.rescale(number::SCALE);
177                Self(self.0)
178            }
179
180            #[must_use]
181            pub fn round_dp(self, digits: u32) -> Self {
182                Self(self.0.round_dp(digits))
183            }
184        }
185
186        impl $crate::number::FromDecimal for $kind {
187            fn from_decimal(mut d: Decimal) -> Self {
188                d.rescale($crate::number::SCALE);
189                Self(d)
190            }
191        }
192
193        impl $crate::number::TruncateDecimal for $kind {
194            fn truncate_to_ocpi_scale(self) -> Self {
195                Self(self.0.truncate_to_ocpi_scale())
196            }
197        }
198
199        impl std::fmt::Display for $kind {
200            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201                // Avoid writing needless "0.000"
202                if self.0.is_zero() {
203                    write!(f, "0{}", $unit)
204                } else {
205                    write!(f, "{:.4}{}", self.0, $unit)
206                }
207            }
208        }
209
210        /// The user can convert a `Decimal` newtype to a `Decimal` But cannot create
211        /// a `Decimal` newtype from a `Decimal`.
212        impl From<$kind> for rust_decimal::Decimal {
213            fn from(value: $kind) -> Self {
214                value.0
215            }
216        }
217
218        #[cfg(test)]
219        impl From<u64> for $kind {
220            fn from(value: u64) -> Self {
221                Self(value.into())
222            }
223        }
224
225        #[cfg(test)]
226        impl From<f64> for $kind {
227            fn from(value: f64) -> Self {
228                Self(Decimal::from_f64_retain(value).unwrap())
229            }
230        }
231
232        #[cfg(test)]
233        impl From<rust_decimal::Decimal> for $kind {
234            fn from(value: rust_decimal::Decimal) -> Self {
235                Self(value)
236            }
237        }
238
239        impl $crate::SaturatingAdd for $kind {
240            fn saturating_add(self, other: Self) -> Self {
241                Self(self.0.saturating_add(other.0))
242            }
243        }
244
245        impl $crate::SaturatingSub for $kind {
246            fn saturating_sub(self, other: Self) -> Self {
247                Self(self.0.saturating_sub(other.0))
248            }
249        }
250
251        impl $crate::json::FromJson<'_, '_> for $kind {
252            type WarningKind = $crate::number::WarningKind;
253
254            fn from_json(elem: &json::Element<'_>) -> $crate::Verdict<Self, Self::WarningKind> {
255                rust_decimal::Decimal::from_json(elem).map(|v| v.map(Self))
256            }
257        }
258
259        #[cfg(test)]
260        impl $crate::test::ApproxEq for $kind {
261            fn approx_eq(&self, other: &Self) -> bool {
262                const TOLERANCE: Decimal = rust_decimal_macros::dec!(0.1);
263                const PRECISION: u32 = 2;
264
265                $crate::test::approx_eq_dec(self.0, other.0, TOLERANCE, PRECISION)
266            }
267        }
268    };
269}
270
271#[cfg(test)]
272mod test {
273    use rust_decimal::Decimal;
274    use rust_decimal_macros::dec;
275
276    use crate::test::{approx_eq_dec, ApproxEq};
277
278    impl ApproxEq for Decimal {
279        fn approx_eq(&self, other: &Self) -> bool {
280            const TOLERANCE: Decimal = dec!(0.1);
281            const PRECISION: u32 = 2;
282
283            approx_eq_dec(*self, *other, TOLERANCE, PRECISION)
284        }
285    }
286}
287
288#[cfg(test)]
289mod test_truncate_to_ocpi {
290    use rust_decimal_macros::dec;
291
292    use super::TruncateDecimal;
293
294    #[test]
295    fn test() {
296        let d = dec!(13.951148000000);
297        assert_eq!(d.truncate_to_ocpi_scale(), dec!(13.9511));
298    }
299}