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