ocpi-tariffs 0.43.0

OCPI tariff calculations
Documentation
#![allow(
    clippy::arithmetic_side_effects,
    reason = "tests are allowed have arithmetic_side_effects"
)]

use chrono::TimeDelta;

use crate::test::ApproxEq;

impl ApproxEq for TimeDelta {
    type Tolerance = i64;

    fn default_tolerance() -> Self::Tolerance {
        3
    }

    fn approx_eq_tolerance(&self, other: &Self, tolerance: i64) -> bool {
        let diff = self.num_seconds() - other.num_seconds();
        diff.abs() <= tolerance
    }
}

/// Convert a `str` in `[hour]::[minute]::[second]` format to a duration type.
pub(crate) trait FromHms {
    #[track_caller]
    fn from_hms(hms: &str) -> Self;
}

impl FromHms for TimeDelta {
    fn from_hms(hms: &str) -> Self {
        const SEPARATOR: char = ':';
        const SECS_IN_HOUR: i64 = 3600;

        let (hour, min, sec) = {
            let (hour, rest) = hms.split_once(SEPARATOR).unwrap_or_else(|| {
                panic!("Unable to parse time in HMS format. Hour separator is not present. `{hms}`")
            });

            let (min, sec) = rest.split_once(SEPARATOR).unwrap_or_else(|| {
                panic!(
                    "Unable to parse time in HMS format. Minute separator is not present. `{hms}`"
                )
            });
            (hour, min, sec)
        };

        let hour = hour.parse::<i64>().unwrap_or_else(|_| {
            panic!("Unable to parse time in HMS format. The hour is not an integer. `{hms}`")
        });

        let min = min.parse::<i64>().unwrap_or_else(|_| {
            panic!("Unable to parse time in HMS format. The minute is not an integer. `{hms}`")
        });

        assert!(
            (0..60).contains(&min),
            "Unable to parse time in HMS format. The minute is not in range `[0-60)`. `{hms}`"
        );

        let sec = sec.parse::<i64>().unwrap_or_else(|_| {
            panic!("Unable to parse time in HMS format. The second is not an integer. `{hms}`")
        });

        assert!(
            (0..60).contains(&sec),
            "Unable to parse time in HMS format. The second is not in range `[0-60)`. `{hms}`"
        );

        let secs_total = {
            // `hour` can be large, so be careful
            let hours_as_secs = hour.checked_mul(SECS_IN_HOUR).unwrap_or_else(|| {
                panic!("Unable to parse time in HMS format. The hour range is too large. `{hms}`")
            });
            hours_as_secs
                // This can not overflow, we checked that `min` and `sec` are small
                .checked_add(min * super::SECS_IN_MIN + sec)
                .unwrap_or_else(|| {
                    panic!(
                        "Unable to parse time in HMS format. The hour range is too large. `{hms}`"
                    )
                })
        };

        TimeDelta::seconds(secs_total)
    }
}

#[test]
#[should_panic(expected = "Unable to parse time in HMS format. Hour separator is not present. ``")]
fn should_panic_on_empty() {
    TimeDelta::from_hms("");
}

#[test]
#[should_panic(
    expected = "Unable to parse time in HMS format. Minute separator is not present. `:`"
)]
fn should_panic_on_single_separator() {
    TimeDelta::from_hms(":");
}

#[test]
#[should_panic(expected = "Unable to parse time in HMS format. The hour is not an integer. `::`")]
fn should_panic_on_empty_separator() {
    TimeDelta::from_hms("::");
}

#[test]
#[should_panic(
    expected = "Unable to parse time in HMS format. Minute separator is not present. `01:02`"
)]
fn should_panic_on_missing_component() {
    TimeDelta::from_hms("01:02");
}

#[test]
#[should_panic(
    expected = "Unable to parse time in HMS format. The hour is not an integer. ` 01:02:03 `"
)]
fn should_panic_on_extraneous_whitespace() {
    TimeDelta::from_hms(" 01:02:03 ");
}

#[test]
#[should_panic(
    expected = "Unable to parse time in HMS format. The hour is not an integer. ` 01: 02:03`"
)]
fn should_panic_malformed_whitespace() {
    TimeDelta::from_hms(" 01: 02:03");
}

#[test]
#[should_panic(
    expected = "Unable to parse time in HMS format. The minute is not in range `[0-60)`. `01:60:03`"
)]
fn should_panic_minutes_out_of_range() {
    TimeDelta::from_hms("01:60:03");
}

#[test]
#[should_panic(
    expected = "Unable to parse time in HMS format. The second is not in range `[0-60)`. `01:02:60`"
)]
fn should_panic_seconds_out_of_range() {
    TimeDelta::from_hms("01:02:60");
}

#[test]
fn should_parse_hms() {
    assert_eq!(TimeDelta::from_hms("01:02:03"), TimeDelta::seconds(3723));

    assert_eq!(TimeDelta::from_hms("1:2:3"), TimeDelta::seconds(3723));

    assert_eq!(
        TimeDelta::from_hms("36:52:33"),
        TimeDelta::seconds(36 * 3600 + 52 * 60 + 33)
    );

    assert_eq!(
        TimeDelta::from_hms("120:52:33"),
        TimeDelta::seconds(120 * 3600 + 52 * 60 + 33)
    );
}