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