ocpi-tariffs 0.46.0

OCPI tariff calculations
Documentation
//! We represent the OCPI spec Number as a `Decimal` and serialize and deserialize to the precision defined in the OCPI spec.
//!
//! <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#14-number-type>
#[cfg(test)]
pub mod test;

#[cfg(test)]
mod test_approx_eq;

#[cfg(test)]
mod test_round_to_ocpi;

#[cfg(test)]
mod test_parse_string;

use std::{fmt, num::IntErrorKind};

use rust_decimal::Decimal;

use crate::{
    json,
    warning::{self, GatherWarnings, IntoCaveat},
};

/// The scale for numerical values as defined in the OCPI spec.
///
/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#14-number-type>
pub const SCALE: u32 = 4;

/// The warnings that can happen when parsing or linting a numerical value.
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum Warning {
    /// Numerical strings don't need to have escape-codes.
    ContainsEscapeCodes,

    /// Unable to convert string to a `Decimal`.
    Decimal(String),

    /// The field at the path could not be decoded.
    Decode(json::decode::Warning),

    /// The value provided exceeds `Decimal::MAX`.
    ExceedsMaximumPossibleValue,

    /// The number given has more than the four decimal precision required by the OCPI spec.
    ExcessivePrecision,

    /// The JSON value given is not a number.
    InvalidType { type_found: json::ValueKind },

    /// The value provided is less than `Decimal::MIN`.
    LessThanMinimumPossibleValue,

    /// An underflow is when there are more fractional digits than can be represented within `Decimal`.
    Underflow,
}

impl Warning {
    fn invalid_type(elem: &json::Element<'_>) -> Self {
        Self::InvalidType {
            type_found: elem.value().kind(),
        }
    }
}

impl From<json::decode::Warning> for Warning {
    fn from(warning: json::decode::Warning) -> Self {
        Self::Decode(warning)
    }
}

impl fmt::Display for Warning {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::ContainsEscapeCodes => f.write_str("The value contains escape codes but it does not need them"),
            Self::Decimal(msg) => write!(f, "{msg}"),
            Self::Decode(warning) => fmt::Display::fmt(warning, f),
            Self::ExcessivePrecision => f.write_str("The number given has more than the four decimal precision required by the OCPI spec."),
            Self::InvalidType { type_found } => {
                write!(f, "The value should be a number but is `{type_found}`.")
            }
            Self::ExceedsMaximumPossibleValue => {
                f.write_str("The value provided exceeds `79,228,162,514,264,337,593,543,950,335`.")
            }
            Self::LessThanMinimumPossibleValue => f.write_str("The value provided is less than `-79,228,162,514,264,337,593,543,950,335`."),
            Self::Underflow => f.write_str("An underflow is when there are more than 28 fractional digits"),
        }
    }
}

impl crate::Warning for Warning {
    fn id(&self) -> warning::Id {
        match self {
            Self::ContainsEscapeCodes => warning::Id::from_static("contains_escape_codes"),
            Self::Decimal(_) => warning::Id::from_static("decimal"),
            Self::Decode(warning) => warning.id(),
            Self::ExcessivePrecision => warning::Id::from_static("excessive_precision"),
            Self::InvalidType { .. } => warning::Id::from_static("invalid_type"),
            Self::ExceedsMaximumPossibleValue => {
                warning::Id::from_static("exceeds_maximum_possible_value")
            }
            Self::LessThanMinimumPossibleValue => {
                warning::Id::from_static("less_than_minimum_possible_value")
            }
            Self::Underflow => warning::Id::from_static("underflow"),
        }
    }
}

pub(crate) fn int_error_kind_as_str(kind: IntErrorKind) -> &'static str {
    match kind {
        IntErrorKind::Empty => "empty",
        IntErrorKind::InvalidDigit => "invalid digit",
        IntErrorKind::PosOverflow => "positive overflow",
        IntErrorKind::NegOverflow => "negative overflow",
        IntErrorKind::Zero => "zero",
        _ => "unknown",
    }
}

impl json::FromJson<'_> for Decimal {
    type Warning = Warning;

    fn from_json(elem: &json::Element<'_>) -> crate::Verdict<Self, Self::Warning> {
        let mut warnings = warning::Set::new();
        let value = elem.as_value();

        // First try get the JSON element as a JSON number.
        let s = if let Some(s) = value.as_number() {
            s
        } else {
            // If the JSON element is not a JSON number, then we also accept a JSON string.
            // As long as it's a number encoded as a string.
            let Some(raw_str) = value.to_raw_str() else {
                return warnings.bail(Warning::invalid_type(elem), elem);
            };

            let pending_str = raw_str
                .has_escapes(elem)
                .gather_warnings_into(&mut warnings);

            match pending_str {
                json::decode::PendingStr::NoEscapes(s) => s,
                json::decode::PendingStr::HasEscapes(_) => {
                    return warnings.bail(Warning::ContainsEscapeCodes, elem);
                }
            }
        };

        let mut decimal = match Decimal::from_str_exact(s) {
            Ok(v) => v,
            Err(err) => {
                let kind = match err {
                    rust_decimal::Error::ExceedsMaximumPossibleValue => {
                        Warning::ExceedsMaximumPossibleValue
                    }
                    rust_decimal::Error::LessThanMinimumPossibleValue => {
                        Warning::LessThanMinimumPossibleValue
                    }
                    rust_decimal::Error::Underflow => Warning::Underflow,
                    rust_decimal::Error::ConversionTo(_) => {
                        unreachable!("This is only triggered when converting to numerical types")
                    }
                    rust_decimal::Error::ErrorString(msg) => Warning::Decimal(msg),
                    rust_decimal::Error::ScaleExceedsMaximumPrecision(_) => {
                        unreachable!("`Decimal::from_str_exact` uses a scale of zero")
                    }
                };

                return warnings.bail(kind, elem);
            }
        };

        if decimal.scale() > SCALE {
            warnings.insert(Warning::ExcessivePrecision, elem);
        }

        decimal.rescale(SCALE);
        Ok(decimal.into_caveat(warnings))
    }
}

pub(crate) trait FromDecimal {
    fn from_decimal(d: Decimal) -> Self;
}

/// All `Decimal`s should be rescaled to scale defined in the OCPI specs.
///
/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#14-number-type>
impl FromDecimal for Decimal {
    fn from_decimal(mut d: Decimal) -> Self {
        d.rescale(SCALE);
        d
    }
}

/// Round a `Decimal` or `Decimal`-like value to the scale defined in the OCPI spec.
///
/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#14-number-type>
pub trait RoundDecimal {
    /// Round a `Decimal` or `Decimal`-like value to the scale defined in the OCPI spec.
    ///
    /// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#14-number-type>
    #[must_use]
    fn round_to_ocpi_scale(self) -> Self;
}

impl RoundDecimal for Decimal {
    fn round_to_ocpi_scale(self) -> Self {
        self.round_dp_with_strategy(SCALE, rust_decimal::RoundingStrategy::MidpointNearestEven)
    }
}

impl<T: RoundDecimal> RoundDecimal for Option<T> {
    fn round_to_ocpi_scale(self) -> Self {
        self.map(RoundDecimal::round_to_ocpi_scale)
    }
}

/// Allow a `Decimal` type to define its own precision when testing for zero.
///
/// Note: the `num_traits::Zero` trait is not used as it has extra requirements that
/// the `ocpi-tariffs` `Decimal` types do not want/need to fulfill.
pub(crate) trait IsZero {
    /// Return true if the value is considered zero.
    fn is_zero(&self) -> bool;
}

/// Approximately compare two `Decimal` values.
pub(crate) fn approx_eq_dec(a: &Decimal, b: &Decimal, tolerance: Decimal) -> bool {
    // If `a` and `b` are potentially equal then `a - b` should be close to zero.
    // If the subtraction results in an overflow, then the numbers are nowhere near to being equal.
    let Some(diff) = a.checked_sub(*b) else {
        return false;
    };
    // We don't care about the sign of the difference when checking for equality.
    diff.abs() <= tolerance
}

/// Impl a `Decimal` based newtype.
///
/// All `Decimal` newtypes impl and `serde::Deserialize` which apply the precision
/// defined in the OCPI spec.
///
/// <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#14-number-type>
#[doc(hidden)]
#[macro_export]
macro_rules! impl_dec_newtype {
    ($kind:ident, $unit:literal) => {
        impl $kind {
            /// Round this number to the OCPI specified amount of decimals.
            #[must_use]
            pub fn rescale(mut self) -> Self {
                self.0.rescale(number::SCALE);
                Self(self.0)
            }

            #[must_use]
            pub fn round_dp(self, digits: u32) -> Self {
                Self(self.0.round_dp(digits))
            }
        }

        impl $crate::number::FromDecimal for $kind {
            fn from_decimal(mut d: Decimal) -> Self {
                d.rescale($crate::number::SCALE);
                Self(d)
            }
        }

        impl $crate::number::RoundDecimal for $kind {
            fn round_to_ocpi_scale(self) -> Self {
                Self(self.0.round_to_ocpi_scale())
            }
        }

        impl std::fmt::Display for $kind {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                // Avoid writing needless "0.000"
                if self.0.is_zero() {
                    if f.alternate() {
                        write!(f, "0")
                    } else {
                        write!(f, "0{}", $unit)
                    }
                } else {
                    if f.alternate() {
                        write!(f, "{:.4}", self.0)
                    } else {
                        write!(f, "{:.4}{}", self.0, $unit)
                    }
                }
            }
        }

        /// The user can convert a `Decimal` newtype to a `Decimal` But cannot create
        /// a `Decimal` newtype from a `Decimal`.
        impl From<$kind> for rust_decimal::Decimal {
            fn from(value: $kind) -> Self {
                value.0
            }
        }

        #[cfg(test)]
        impl From<u64> for $kind {
            fn from(value: u64) -> Self {
                Self(value.into())
            }
        }

        #[cfg(test)]
        impl From<f64> for $kind {
            fn from(value: f64) -> Self {
                Self(Decimal::from_f64_retain(value).unwrap())
            }
        }

        #[cfg(test)]
        impl From<rust_decimal::Decimal> for $kind {
            fn from(value: rust_decimal::Decimal) -> Self {
                Self(value)
            }
        }

        impl $crate::SaturatingAdd for $kind {
            fn saturating_add(self, other: Self) -> Self {
                Self(self.0.saturating_add(other.0))
            }
        }

        impl $crate::SaturatingSub for $kind {
            fn saturating_sub(self, other: Self) -> Self {
                Self(self.0.saturating_sub(other.0))
            }
        }

        impl $crate::json::FromJson<'_> for $kind {
            type Warning = $crate::number::Warning;

            fn from_json(elem: &json::Element<'_>) -> $crate::Verdict<Self, Self::Warning> {
                rust_decimal::Decimal::from_json(elem).map(|v| v.map(Self))
            }
        }

        #[cfg(test)]
        impl $crate::test::ApproxEq for $kind {
            type Tolerance = Decimal;

            fn default_tolerance() -> Self::Tolerance {
                rust_decimal_macros::dec!(0.1)
            }

            fn approx_eq_tolerance(&self, other: &Self, tolerance: Decimal) -> bool {
                $crate::number::approx_eq_dec(&self.0, &other.0, tolerance)
            }
        }
    };
}