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
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 JSON value given is not a number.
54    InvalidType { type_found: json::ValueKind },
55
56    /// The value provided is less than `Decimal::MIN`.
57    LessThanMinimumPossibleValue,
58
59    /// An underflow is when there are more fractional digits than can be represented within `Decimal`.
60    Underflow,
61}
62
63impl Warning {
64    fn invalid_type(elem: &json::Element<'_>) -> Self {
65        Self::InvalidType {
66            type_found: elem.value().kind(),
67        }
68    }
69}
70
71impl From<json::decode::Warning> for Warning {
72    fn from(warning: json::decode::Warning) -> Self {
73        Self::Decode(warning)
74    }
75}
76
77impl fmt::Display for Warning {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        match self {
80            Self::ContainsEscapeCodes => f.write_str("The value contains escape codes but it does not need them"),
81            Self::Decimal(msg) => write!(f, "{msg}"),
82            Self::Decode(warning) => fmt::Display::fmt(warning, f),
83            Self::ExcessivePrecision => f.write_str("The number given has more than the four decimal precision required by the OCPI spec."),
84            Self::InvalidType { type_found } => {
85                write!(f, "The value should be a number but is `{type_found}`.")
86            }
87            Self::ExceedsMaximumPossibleValue => {
88                f.write_str("The value provided exceeds `79,228,162,514,264,337,593,543,950,335`.")
89            }
90            Self::LessThanMinimumPossibleValue => f.write_str("The value provided is less than `-79,228,162,514,264,337,593,543,950,335`."),
91            Self::Underflow => f.write_str("An underflow is when there are more than 28 fractional digits"),
92        }
93    }
94}
95
96impl crate::Warning for Warning {
97    fn id(&self) -> warning::Id {
98        match self {
99            Self::ContainsEscapeCodes => warning::Id::from_static("contains_escape_codes"),
100            Self::Decimal(_) => warning::Id::from_static("decimal"),
101            Self::Decode(warning) => warning.id(),
102            Self::ExcessivePrecision => warning::Id::from_static("excessive_precision"),
103            Self::InvalidType { type_found } => {
104                warning::Id::from_string(format!("invalid_type({type_found})"))
105            }
106            Self::ExceedsMaximumPossibleValue => {
107                warning::Id::from_static("exceeds_maximum_possible_value")
108            }
109            Self::LessThanMinimumPossibleValue => {
110                warning::Id::from_static("less_than_minimum_possible_value")
111            }
112            Self::Underflow => warning::Id::from_static("underflow"),
113        }
114    }
115}
116
117pub(crate) fn int_error_kind_as_str(kind: IntErrorKind) -> &'static str {
118    match kind {
119        IntErrorKind::Empty => "empty",
120        IntErrorKind::InvalidDigit => "invalid digit",
121        IntErrorKind::PosOverflow => "positive overflow",
122        IntErrorKind::NegOverflow => "negative overflow",
123        IntErrorKind::Zero => "zero",
124        _ => "unknown",
125    }
126}
127
128impl<'buf> FromSchema<'buf, schema::Number<'buf>> for Decimal {
129    type Warning = Warning;
130
131    fn from_schema(source: &schema::Number<'buf>) -> crate::Verdict<Self, Self::Warning> {
132        let mut warnings = warning::Set::new();
133
134        // The schema already proved the literal form is a valid JSON number, so its
135        // `digits` are read directly. The string-encoded form carries an unchecked
136        // string, so it must be decoded (and an escaped string is rejected) here.
137        let (elem, digits) = match source {
138            schema::Number::Number { elem, digits } => (elem, *digits),
139            schema::Number::StringEncoded { elem, value } => {
140                let pending_str = value.has_escapes(elem).gather_warnings_into(&mut warnings);
141
142                match pending_str {
143                    json::PendingStr::NoEscapes(s) => (elem, s),
144                    json::PendingStr::HasEscapes(_) => {
145                        return warnings.bail(elem, Warning::ContainsEscapeCodes);
146                    }
147                }
148            }
149        };
150
151        let decimal = match Decimal::from_str_exact(digits) {
152            Ok(v) => v,
153            Err(err) => {
154                let kind = match err {
155                    rust_decimal::Error::ExceedsMaximumPossibleValue => {
156                        Warning::ExceedsMaximumPossibleValue
157                    }
158                    rust_decimal::Error::LessThanMinimumPossibleValue => {
159                        Warning::LessThanMinimumPossibleValue
160                    }
161                    rust_decimal::Error::Underflow => Warning::Underflow,
162                    rust_decimal::Error::ConversionTo(_) => {
163                        unreachable!("This is only triggered when converting to numerical types")
164                    }
165                    rust_decimal::Error::ErrorString(msg) => Warning::Decimal(msg),
166                    rust_decimal::Error::ScaleExceedsMaximumPrecision(_) => {
167                        unreachable!("`Decimal::from_str_exact` uses a scale of zero")
168                    }
169                };
170
171                return warnings.bail(elem, kind);
172            }
173        };
174
175        if decimal.scale() > SCALE {
176            warnings.insert(elem, Warning::ExcessivePrecision);
177        }
178
179        Ok(decimal.into_caveat(warnings))
180    }
181}
182
183impl json::FromJson<'_> for Decimal {
184    type Warning = Warning;
185
186    fn from_json(elem: &json::Element<'_>) -> crate::Verdict<Self, Self::Warning> {
187        let mut warnings = warning::Set::new();
188        let value = elem.as_value();
189
190        // First try get the JSON element as a JSON number.
191        let s = if let Some(s) = value.as_number() {
192            s
193        } else {
194            // If the JSON element is not a JSON number, then we also accept a JSON string.
195            // As long as it's a number encoded as a string.
196            let Some(raw_str) = value.to_raw_str() else {
197                return warnings.bail(elem, Warning::invalid_type(elem));
198            };
199
200            let pending_str = raw_str
201                .has_escapes(elem)
202                .gather_warnings_into(&mut warnings);
203
204            match pending_str {
205                json::PendingStr::NoEscapes(s) => s,
206                json::PendingStr::HasEscapes(_) => {
207                    return warnings.bail(elem, Warning::ContainsEscapeCodes);
208                }
209            }
210        };
211
212        let decimal = match Decimal::from_str_exact(s) {
213            Ok(v) => v,
214            Err(err) => {
215                let kind = match err {
216                    rust_decimal::Error::ExceedsMaximumPossibleValue => {
217                        Warning::ExceedsMaximumPossibleValue
218                    }
219                    rust_decimal::Error::LessThanMinimumPossibleValue => {
220                        Warning::LessThanMinimumPossibleValue
221                    }
222                    rust_decimal::Error::Underflow => Warning::Underflow,
223                    rust_decimal::Error::ConversionTo(_) => {
224                        unreachable!("This is only triggered when converting to numerical types")
225                    }
226                    rust_decimal::Error::ErrorString(msg) => Warning::Decimal(msg),
227                    rust_decimal::Error::ScaleExceedsMaximumPrecision(_) => {
228                        unreachable!("`Decimal::from_str_exact` uses a scale of zero")
229                    }
230                };
231
232                return warnings.bail(elem, kind);
233            }
234        };
235
236        if decimal.scale() > SCALE {
237            warnings.insert(elem, Warning::ExcessivePrecision);
238        }
239
240        Ok(decimal.into_caveat(warnings))
241    }
242}
243
244pub(crate) trait FromDecimal {
245    fn from_decimal(d: Decimal) -> Self;
246}
247
248/// All `Decimal`s should be rescaled to scale defined in the OCPI specs.
249///
250/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#14-number-type>.
251impl FromDecimal for Decimal {
252    fn from_decimal(mut d: Decimal) -> Self {
253        d.rescale(SCALE);
254        d
255    }
256}
257
258/// Round a `Decimal` or `Decimal`-like value to the scale defined in the OCPI spec.
259///
260/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#14-number-type>.
261pub trait RoundDecimal {
262    /// Round a `Decimal` or `Decimal`-like value to the scale defined in the OCPI spec.
263    ///
264    /// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#14-number-type>.
265    #[must_use]
266    fn round_to_ocpi_scale(self) -> Self;
267}
268
269impl RoundDecimal for Decimal {
270    fn round_to_ocpi_scale(self) -> Self {
271        self.round_dp_with_strategy(SCALE, rust_decimal::RoundingStrategy::MidpointNearestEven)
272    }
273}
274
275impl<T: RoundDecimal> RoundDecimal for Option<T> {
276    fn round_to_ocpi_scale(self) -> Self {
277        self.map(RoundDecimal::round_to_ocpi_scale)
278    }
279}
280
281/// Allow a `Decimal` type to define its own precision when testing for zero.
282///
283/// Note: the `num_traits::Zero` trait is not used as it has extra requirements that
284/// the `ocpi-tariffs` `Decimal` types do not want/need to fulfill.
285pub(crate) trait IsZero {
286    /// Return true if the value is considered zero.
287    fn is_zero(&self) -> bool;
288}
289
290/// Approximately compare two `Decimal` values.
291pub(crate) fn approx_eq_dec(a: &Decimal, b: &Decimal, tolerance: Decimal) -> bool {
292    // If `a` and `b` are potentially equal then `a - b` should be close to zero.
293    // If the subtraction results in an overflow, then the numbers are nowhere near to being equal.
294    let Some(diff) = a.checked_sub(*b) else {
295        return false;
296    };
297    // We don't care about the sign of the difference when checking for equality.
298    diff.abs() <= tolerance
299}
300
301/// Impl a `Decimal` based newtype.
302///
303/// All `Decimal` newtypes impl and `serde::Deserialize` which apply the precision
304/// defined in the OCPI spec.
305///
306/// <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#14-number-type>.
307#[doc(hidden)]
308#[macro_export]
309macro_rules! impl_dec_newtype {
310    ($kind:ident, $unit:literal) => {
311        impl $kind {
312            /// Round this number to the OCPI specified amount of decimals.
313            #[must_use]
314            pub fn rescale(mut self) -> Self {
315                self.0.rescale(number::SCALE);
316                Self(self.0)
317            }
318
319            #[must_use]
320            pub fn round_dp(self, digits: u32) -> Self {
321                Self(self.0.round_dp(digits))
322            }
323        }
324
325        impl $crate::number::FromDecimal for $kind {
326            fn from_decimal(d: Decimal) -> Self {
327                Self(d)
328            }
329        }
330
331        impl $crate::number::RoundDecimal for $kind {
332            fn round_to_ocpi_scale(self) -> Self {
333                Self(self.0.round_to_ocpi_scale())
334            }
335        }
336
337        impl std::fmt::Display for $kind {
338            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
339                // Avoid writing needless "0.000"
340                if self.0.is_zero() {
341                    if f.alternate() {
342                        write!(f, "0")
343                    } else {
344                        write!(f, "0{}", $unit)
345                    }
346                } else {
347                    if f.alternate() {
348                        write!(f, "{:.4}", self.0)
349                    } else {
350                        write!(f, "{:.4}{}", self.0, $unit)
351                    }
352                }
353            }
354        }
355
356        /// The user can convert a `Decimal` newtype to a `Decimal` But cannot create
357        /// a `Decimal` newtype from a `Decimal`.
358        impl From<$kind> for rust_decimal::Decimal {
359            fn from(value: $kind) -> Self {
360                value.0
361            }
362        }
363
364        #[cfg(test)]
365        impl From<u64> for $kind {
366            fn from(value: u64) -> Self {
367                Self(value.into())
368            }
369        }
370
371        #[cfg(test)]
372        impl From<f64> for $kind {
373            fn from(value: f64) -> Self {
374                Self(Decimal::from_f64_retain(value).unwrap())
375            }
376        }
377
378        #[cfg(test)]
379        impl From<rust_decimal::Decimal> for $kind {
380            fn from(value: rust_decimal::Decimal) -> Self {
381                Self(value)
382            }
383        }
384
385        impl $crate::SaturatingAdd for $kind {
386            fn saturating_add(self, other: Self) -> Self {
387                Self(self.0.saturating_add(other.0))
388            }
389        }
390
391        impl $crate::SaturatingSub for $kind {
392            fn saturating_sub(self, other: Self) -> Self {
393                Self(self.0.saturating_sub(other.0))
394            }
395        }
396
397        impl $crate::json::FromJson<'_> for $kind {
398            type Warning = $crate::number::Warning;
399
400            fn from_json(elem: &json::Element<'_>) -> $crate::Verdict<Self, Self::Warning> {
401                rust_decimal::Decimal::from_json(elem).map(|v| v.map(Self))
402            }
403        }
404
405        impl<'buf> $crate::FromSchema<'buf, $crate::schema::Number<'buf>> for $kind {
406            type Warning = $crate::number::Warning;
407
408            fn from_schema(
409                source: &$crate::schema::Number<'buf>,
410            ) -> $crate::Verdict<Self, Self::Warning> {
411                let decimal: $crate::Verdict<rust_decimal::Decimal, $crate::number::Warning> =
412                    $crate::FromSchema::from_schema(source);
413                decimal.map(|v| v.map(Self))
414            }
415        }
416
417        #[cfg(test)]
418        impl $crate::test::ApproxEq for $kind {
419            type Tolerance = Decimal;
420
421            fn default_tolerance() -> Self::Tolerance {
422                rust_decimal_macros::dec!(0.1)
423            }
424
425            fn approx_eq_tolerance(&self, other: &Self, tolerance: Decimal) -> bool {
426                $crate::number::approx_eq_dec(&self.0, &other.0, tolerance)
427            }
428        }
429    };
430}