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