ocpi-tariffs 0.20.0

OCPI tariff calculations
Documentation
use std::fmt;

use chrono::Duration;
use rust_decimal::Decimal;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

use crate::Number;

const SECS_IN_MIN: i64 = 60;
const MINS_IN_HOUR: i64 = 60;
const MILLIS_IN_SEC: i64 = 1000;

/// Possible errors when pricing a charge session.
#[derive(Debug)]
pub enum Error {
    /// A numeric overflow occurred while creating a duration.
    DurationOverflow,
}

impl From<rust_decimal::Error> for Error {
    fn from(_: rust_decimal::Error) -> Self {
        Self::DurationOverflow
    }
}

impl std::error::Error for Error {}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::DurationOverflow => {
                f.write_str("A numeric overflow occurred while creating a duration")
            }
        }
    }
}

/// A generic duration type that converts from and to a decimal amount of hours.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct HoursDecimal(Duration);

impl<'de> Deserialize<'de> for HoursDecimal {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        use serde::de::Error;

        let hours = Number::deserialize(deserializer)?;
        let duration = Self::from_hours_number(hours).map_err(|_e| D::Error::custom("overflow"))?;
        Ok(duration)
    }
}

impl Serialize for HoursDecimal {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        let hours = self.as_num_hours_number();
        hours.serialize(serializer)
    }
}

impl fmt::Display for HoursDecimal {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let duration = self.0;
        let seconds = duration.num_seconds() % SECS_IN_MIN;
        let minutes = (duration.num_seconds() / SECS_IN_MIN) % MINS_IN_HOUR;
        let hours = duration.num_seconds() / (SECS_IN_MIN * MINS_IN_HOUR);

        write!(f, "{hours:0>2}:{minutes:0>2}:{seconds:0>2}")
    }
}

impl From<HoursDecimal> for Duration {
    fn from(value: HoursDecimal) -> Self {
        value.0
    }
}

impl From<Duration> for HoursDecimal {
    fn from(value: Duration) -> Self {
        Self(value)
    }
}

impl HoursDecimal {
    pub fn zero() -> Self {
        Self(Duration::zero())
    }

    pub fn as_num_seconds_number(&self) -> Number {
        Number::from(self.0.num_milliseconds())
            .checked_div(Number::from(MILLIS_IN_SEC))
            .expect("Can't overflow; See test `as_num_seconds_number_should_not_overflow`")
    }

    /// Convert into decimal representation.
    pub fn as_num_hours_decimal(&self) -> Decimal {
        self.as_num_hours_number().into()
    }

    #[must_use]
    pub fn as_num_hours_number(&self) -> Number {
        let div = Decimal::from(MILLIS_IN_SEC * SECS_IN_MIN * MINS_IN_HOUR);
        let num = Decimal::from(self.0.num_milliseconds());
        Number::from(num.checked_div(div).unwrap_or(Decimal::MAX))
    }

    pub fn from_seconds_number(seconds: Number) -> Result<Self, Error> {
        let millis = seconds.saturating_mul(Number::from(MILLIS_IN_SEC));

        Ok(Self(
            Duration::try_milliseconds(millis.try_into()?).ok_or(Error::DurationOverflow)?,
        ))
    }

    pub fn from_hours_number(hours: Number) -> Result<Self, Error> {
        let millis = hours.saturating_mul(Number::from(MILLIS_IN_SEC * SECS_IN_MIN * MINS_IN_HOUR));

        Ok(Self(
            Duration::try_milliseconds(millis.try_into()?).ok_or(Error::DurationOverflow)?,
        ))
    }

    #[must_use]
    pub fn saturating_sub(self, other: Self) -> Self {
        Self(self.0.checked_sub(&other.0).unwrap_or_else(Duration::zero))
    }

    #[must_use]
    pub fn saturating_add(self, other: Self) -> Self {
        Self(self.0.checked_add(&other.0).unwrap_or(Duration::MAX))
    }
}

impl Default for HoursDecimal {
    fn default() -> Self {
        Self::zero()
    }
}

/// A generic duration type that converts from and to a integer amount of seconds.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct SecondsRound(Duration);

impl<'de> Deserialize<'de> for SecondsRound {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        use serde::de::Error as DeError;

        let seconds: i64 = u64::deserialize(deserializer)?
            .try_into()
            .map_err(|_err| DeError::custom(Error::DurationOverflow))?;

        let duration = Duration::try_seconds(seconds)
            .ok_or_else(|| DeError::custom(Error::DurationOverflow))?;

        Ok(Self(duration))
    }
}

impl Serialize for SecondsRound {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        let seconds = self.0.num_seconds();
        serializer.serialize_i64(seconds)
    }
}

impl From<SecondsRound> for Duration {
    fn from(value: SecondsRound) -> Self {
        value.0
    }
}

#[cfg(test)]
mod test {
    use super::Error;

    #[test]
    const fn error_should_be_send_and_sync() {
        const fn f<T: Send + Sync>() {}

        f::<Error>();
    }
}

#[cfg(test)]
mod hour_decimal_tests {
    use chrono::Duration;
    use rust_decimal_macros::dec;

    use super::{HoursDecimal, Number, MILLIS_IN_SEC};

    #[test]
    fn zero_minutes_should_be_zero_hours() {
        let hours: HoursDecimal = Duration::try_minutes(0).unwrap().into();
        let number = hours.as_num_hours_number();
        assert_eq!(number, Number::from(dec!(0.0)));
    }

    #[test]
    fn thirty_minutes_should_be_fraction_of_hour() {
        let hours: HoursDecimal = Duration::try_minutes(30).unwrap().into();
        let number = hours.as_num_hours_number();
        assert_eq!(number, Number::from(dec!(0.5)));
    }

    #[test]
    fn sixty_minutes_should_be_fraction_of_hour() {
        let hours: HoursDecimal = Duration::try_minutes(60).unwrap().into();
        let number = hours.as_num_hours_number();
        assert_eq!(number, Number::from(dec!(1.0)));
    }

    #[test]
    fn ninety_minutes_should_be_fraction_of_hour() {
        let hours: HoursDecimal = Duration::try_minutes(90).unwrap().into();
        let number = hours.as_num_hours_number();
        assert_eq!(number, Number::from(dec!(1.5)));
    }

    #[test]
    fn as_num_seconds_number_should_not_overflow() {
        let number = Number::from(i64::MAX).checked_div(Number::from(MILLIS_IN_SEC));
        assert!(number.is_some(), "should not overflow");
    }
}