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