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#[cfg(test)]
5mod test;
6
7#[cfg(test)]
8mod test_round_to_ocpi;
9
10#[cfg(test)]
11mod test_parse_string;
12
13use std::{fmt, num::IntErrorKind};
14
15use rust_decimal::Decimal;
16
17use crate::{
18    into_caveat, json,
19    warning::{self, GatherWarnings, IntoCaveat},
20};
21
22/// The scale for numerical values as defined in the OCPI spec.
23///
24/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#14-number-type>
25pub const SCALE: u32 = 4;
26
27/// The warnings that can happen when parsing or linting a numerical value.
28#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
29pub enum Warning {
30    /// Numerical strings do not need to have escape codes.
31    ContainsEscapeCodes,
32
33    /// Unable to convert string to a `Decimal`.
34    Decimal(String),
35
36    /// The field at the path could not be decoded.
37    Decode(json::decode::Warning),
38
39    /// The value provided exceeds `Decimal::MAX`.
40    ExceedsMaximumPossibleValue,
41
42    /// The number given has more than the four decimal precision required by the OCPI spec.
43    ExcessivePrecision,
44
45    /// The JSON value given is not a number.
46    InvalidType { type_found: json::ValueKind },
47
48    /// The value provided is less than `Decimal::MIN`.
49    LessThanMinimumPossibleValue,
50
51    /// An underflow is when there are more fractional digits than can be represented within `Decimal`.
52    Underflow,
53}
54
55impl Warning {
56    fn invalid_type(elem: &json::Element<'_>) -> Self {
57        Self::InvalidType {
58            type_found: elem.value().kind(),
59        }
60    }
61}
62
63impl From<json::decode::Warning> for Warning {
64    fn from(warning: json::decode::Warning) -> Self {
65        Self::Decode(warning)
66    }
67}
68
69impl fmt::Display for Warning {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        match self {
72            Self::ContainsEscapeCodes => f.write_str("The value contains escape codes but it does not need them"),
73            Self::Decimal(msg) => write!(f, "{msg}"),
74            Self::Decode(warning) => fmt::Display::fmt(warning, f),
75            Self::ExcessivePrecision => f.write_str("The number given has more than the four decimal precision required by the OCPI spec."),
76            Self::InvalidType { type_found } => {
77                write!(f, "The value should be a number but is `{type_found}`.")
78            }
79            Self::ExceedsMaximumPossibleValue => {
80                f.write_str("The value provided exceeds `79,228,162,514,264,337,593,543,950,335`.")
81            }
82            Self::LessThanMinimumPossibleValue => f.write_str("The value provided is less than `-79,228,162,514,264,337,593,543,950,335`."),
83            Self::Underflow => f.write_str("An underflow is when there are more than 28 fractional digits"),
84        }
85    }
86}
87
88impl crate::Warning for Warning {
89    fn id(&self) -> warning::Id {
90        match self {
91            Self::ContainsEscapeCodes => warning::Id::from_static("contains_escape_codes"),
92            Self::Decimal(_) => warning::Id::from_static("decimal"),
93            Self::Decode(warning) => warning.id(),
94            Self::ExcessivePrecision => warning::Id::from_static("excessive_precision"),
95            Self::InvalidType { .. } => warning::Id::from_static("invalid_type"),
96            Self::ExceedsMaximumPossibleValue => {
97                warning::Id::from_static("exceeds_maximum_possible_value")
98            }
99            Self::LessThanMinimumPossibleValue => {
100                warning::Id::from_static("less_than_minimum_possible_value")
101            }
102            Self::Underflow => warning::Id::from_static("underflow"),
103        }
104    }
105}
106
107pub(crate) fn int_error_kind_as_str(kind: IntErrorKind) -> &'static str {
108    match kind {
109        IntErrorKind::Empty => "empty",
110        IntErrorKind::InvalidDigit => "invalid digit",
111        IntErrorKind::PosOverflow => "positive overflow",
112        IntErrorKind::NegOverflow => "negative overflow",
113        IntErrorKind::Zero => "zero",
114        _ => "unknown",
115    }
116}
117
118into_caveat!(Decimal);
119
120impl json::FromJson<'_, '_> for Decimal {
121    type Warning = Warning;
122
123    fn from_json(elem: &json::Element<'_>) -> crate::Verdict<Self, Self::Warning> {
124        let mut warnings = warning::Set::new();
125        let value = elem.as_value();
126
127        // First try get the JSON element as a JSON number.
128        let s = if let Some(s) = value.as_number() {
129            s
130        } else {
131            // If the JSON element is not a JSON number, then we also accept a JSON string.
132            // As long as it's a number encoded as a string.
133            let Some(raw_str) = value.as_raw_str() else {
134                return warnings.bail(Warning::invalid_type(elem), elem);
135            };
136
137            let pending_str = raw_str
138                .has_escapes(elem)
139                .gather_warnings_into(&mut warnings);
140
141            match pending_str {
142                json::decode::PendingStr::NoEscapes(s) => s,
143                json::decode::PendingStr::HasEscapes(_) => {
144                    return warnings.bail(Warning::ContainsEscapeCodes, elem);
145                }
146            }
147        };
148
149        let mut decimal = match Decimal::from_str_exact(s) {
150            Ok(v) => v,
151            Err(err) => {
152                let kind = match err {
153                    rust_decimal::Error::ExceedsMaximumPossibleValue => {
154                        Warning::ExceedsMaximumPossibleValue
155                    }
156                    rust_decimal::Error::LessThanMinimumPossibleValue => {
157                        Warning::LessThanMinimumPossibleValue
158                    }
159                    rust_decimal::Error::Underflow => Warning::Underflow,
160                    rust_decimal::Error::ConversionTo(_) => {
161                        unreachable!("This is only triggered when converting to numerical types")
162                    }
163                    rust_decimal::Error::ErrorString(msg) => Warning::Decimal(msg),
164                    rust_decimal::Error::ScaleExceedsMaximumPrecision(_) => {
165                        unreachable!("`Decimal::from_str_exact` uses a scale of zero")
166                    }
167                };
168
169                return warnings.bail(kind, elem);
170            }
171        };
172
173        if decimal.scale() > SCALE {
174            warnings.insert(Warning::ExcessivePrecision, elem);
175        }
176
177        decimal.rescale(SCALE);
178        Ok(decimal.into_caveat(warnings))
179    }
180}
181
182pub(crate) trait FromDecimal {
183    fn from_decimal(d: Decimal) -> Self;
184}
185
186/// All `Decimal`s should be rescaled to scale defined in the OCPI specs.
187///
188/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#14-number-type>
189impl FromDecimal for Decimal {
190    fn from_decimal(mut d: Decimal) -> Self {
191        d.rescale(SCALE);
192        d
193    }
194}
195
196/// Round a `Decimal` or `Decimal`-like value to the scale defined in the OCPI spec.
197///
198/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#14-number-type>
199pub trait RoundDecimal {
200    /// Round a `Decimal` or `Decimal`-like value to the scale defined in the OCPI spec.
201    ///
202    /// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#14-number-type>
203    #[must_use]
204    fn round_to_ocpi_scale(self) -> Self;
205}
206
207impl RoundDecimal for Decimal {
208    fn round_to_ocpi_scale(self) -> Self {
209        self.round_dp_with_strategy(SCALE, rust_decimal::RoundingStrategy::MidpointNearestEven)
210    }
211}
212
213impl<T: RoundDecimal> RoundDecimal for Option<T> {
214    fn round_to_ocpi_scale(self) -> Self {
215        self.map(RoundDecimal::round_to_ocpi_scale)
216    }
217}
218
219/// Impl a `Decimal` based newtype.
220///
221/// All `Decimal` newtypes impl and `serde::Deserialize` which apply the precision
222/// defined in the OCPI spec.
223///
224/// <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#14-number-type>
225#[doc(hidden)]
226#[macro_export]
227macro_rules! impl_dec_newtype {
228    ($kind:ident, $unit:literal) => {
229        impl $kind {
230            #[must_use]
231            pub fn zero() -> Self {
232                use num_traits::Zero as _;
233
234                Self(Decimal::zero())
235            }
236
237            pub fn is_zero(&self) -> bool {
238                self.0.is_zero()
239            }
240
241            /// Round this number to the OCPI specified amount of decimals.
242            #[must_use]
243            pub fn rescale(mut self) -> Self {
244                self.0.rescale(number::SCALE);
245                Self(self.0)
246            }
247
248            #[must_use]
249            pub fn round_dp(self, digits: u32) -> Self {
250                Self(self.0.round_dp(digits))
251            }
252        }
253
254        impl $crate::number::FromDecimal for $kind {
255            fn from_decimal(mut d: Decimal) -> Self {
256                d.rescale($crate::number::SCALE);
257                Self(d)
258            }
259        }
260
261        impl $crate::number::RoundDecimal for $kind {
262            fn round_to_ocpi_scale(self) -> Self {
263                Self(self.0.round_to_ocpi_scale())
264            }
265        }
266
267        impl std::fmt::Display for $kind {
268            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269                // Avoid writing needless "0.000"
270                if self.0.is_zero() {
271                    if f.alternate() {
272                        write!(f, "0")
273                    } else {
274                        write!(f, "0{}", $unit)
275                    }
276                } else {
277                    if f.alternate() {
278                        write!(f, "{:.4}", self.0)
279                    } else {
280                        write!(f, "{:.4}{}", self.0, $unit)
281                    }
282                }
283            }
284        }
285
286        /// The user can convert a `Decimal` newtype to a `Decimal` But cannot create
287        /// a `Decimal` newtype from a `Decimal`.
288        impl From<$kind> for rust_decimal::Decimal {
289            fn from(value: $kind) -> Self {
290                value.0
291            }
292        }
293
294        #[cfg(test)]
295        impl From<u64> for $kind {
296            fn from(value: u64) -> Self {
297                Self(value.into())
298            }
299        }
300
301        #[cfg(test)]
302        impl From<f64> for $kind {
303            fn from(value: f64) -> Self {
304                Self(Decimal::from_f64_retain(value).unwrap())
305            }
306        }
307
308        #[cfg(test)]
309        impl From<rust_decimal::Decimal> for $kind {
310            fn from(value: rust_decimal::Decimal) -> Self {
311                Self(value)
312            }
313        }
314
315        impl $crate::SaturatingAdd for $kind {
316            fn saturating_add(self, other: Self) -> Self {
317                Self(self.0.saturating_add(other.0))
318            }
319        }
320
321        impl $crate::SaturatingSub for $kind {
322            fn saturating_sub(self, other: Self) -> Self {
323                Self(self.0.saturating_sub(other.0))
324            }
325        }
326
327        impl $crate::json::FromJson<'_, '_> for $kind {
328            type Warning = $crate::number::Warning;
329
330            fn from_json(elem: &json::Element<'_>) -> $crate::Verdict<Self, Self::Warning> {
331                rust_decimal::Decimal::from_json(elem).map(|v| v.map(Self))
332            }
333        }
334
335        #[cfg(test)]
336        impl $crate::test::ApproxEq for $kind {
337            fn approx_eq(&self, other: &Self) -> bool {
338                const TOLERANCE: Decimal = rust_decimal_macros::dec!(0.1);
339                const PRECISION: u32 = 2;
340
341                $crate::test::approx_eq_dec(self.0, other.0, TOLERANCE, PRECISION)
342            }
343        }
344    };
345}