From 290e937fd43382bf52fd4d92c9294193e2f1f8ed Mon Sep 17 00:00:00 2001 From: Remo Date: Thu, 6 Jul 2023 17:24:27 +0200 Subject: [PATCH 1/2] use saturating operations --- .cargo/config.toml | 1 - ocpi-tariffs/src/pricer.rs | 5 ++++- ocpi-tariffs/src/session.rs | 13 +++++++++++-- ocpi-tariffs/src/types/number.rs | 2 +- ocpi-tariffs/src/types/time.rs | 7 +++++-- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 097e59e..823aea3 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -17,7 +17,6 @@ rustflags = [ "-Wtrivial_numeric_casts", "-Wunsafe_code", "-Wunused_import_braces", - "-Wunused_qualifications", # https://rust-lang.github.io/rust-clippy/master/index.html "-Aclippy::doc_markdown", diff --git a/ocpi-tariffs/src/pricer.rs b/ocpi-tariffs/src/pricer.rs index f8c8635..878d71b 100644 --- a/ocpi-tariffs/src/pricer.rs +++ b/ocpi-tariffs/src/pricer.rs @@ -128,7 +128,10 @@ impl Pricer { let total_time = if let Some(first) = periods.first() { let last = periods.last().unwrap(); - (last.end_date_time - first.start_date_time).into() + (last + .end_date_time + .signed_duration_since(first.start_date_time)) + .into() } else { HoursDecimal::zero() }; diff --git a/ocpi-tariffs/src/session.rs b/ocpi-tariffs/src/session.rs index c4eca0e..1012906 100644 --- a/ocpi-tariffs/src/session.rs +++ b/ocpi-tariffs/src/session.rs @@ -118,11 +118,20 @@ impl InstantData { fn next(&self, state: &PeriodData, date_time: DateTime) -> Self { let mut next = self.clone(); - next.total_duration = next.total_duration + (date_time - next.date_time); + let duration = date_time.signed_duration_since(next.date_time); + + next.total_duration = next + .total_duration + .checked_add(&duration) + .unwrap_or_else(Duration::max_value); + next.date_time = date_time; if let Some(duration) = state.charging_duration { - next.total_charging_duration = next.total_charging_duration + duration; + next.total_charging_duration = next + .total_charging_duration + .checked_add(&duration) + .unwrap_or_else(Duration::max_value); } if let Some(energy) = state.energy { diff --git a/ocpi-tariffs/src/types/number.rs b/ocpi-tariffs/src/types/number.rs index 6223a66..12a45c0 100644 --- a/ocpi-tariffs/src/types/number.rs +++ b/ocpi-tariffs/src/types/number.rs @@ -90,7 +90,7 @@ impl Div for Number { type Output = Self; fn div(self, rhs: Self) -> Self::Output { - Self(self.0 / rhs.0) + Self(self.0.checked_div(rhs.0).expect("divide by zero")) } } diff --git a/ocpi-tariffs/src/types/time.rs b/ocpi-tariffs/src/types/time.rs index a5761af..8d6621e 100644 --- a/ocpi-tariffs/src/types/time.rs +++ b/ocpi-tariffs/src/types/time.rs @@ -65,13 +65,16 @@ impl From for HoursDecimal { impl AddAssign for HoursDecimal { fn add_assign(&mut self, rhs: Self) { - self.0 = self.0 + rhs.0; + self.0 = self + .0 + .checked_add(&rhs.0) + .unwrap_or_else(Duration::max_value); } } impl SubAssign for HoursDecimal { fn sub_assign(&mut self, rhs: Self) { - self.0 = self.0 - rhs.0; + self.0 = self.0.checked_sub(&rhs.0).unwrap_or_else(Duration::zero); } } From 5e53e8da85ee12dff2d2cb346dfeb49c89edb508 Mon Sep 17 00:00:00 2001 From: Remo Date: Mon, 10 Jul 2023 15:48:24 +0200 Subject: [PATCH 2/2] use explicit non-overflowing ops everywhere --- cli/src/cli.rs | 5 +- ocpi-tariffs/src/pricer.rs | 140 ++++++++++++++++++-------- ocpi-tariffs/src/session.rs | 2 +- ocpi-tariffs/src/types/electricity.rs | 50 +++------ ocpi-tariffs/src/types/money.rs | 127 ++++++----------------- ocpi-tariffs/src/types/number.rs | 59 +++++------ ocpi-tariffs/src/types/time.rs | 64 ++++-------- ocpi-tariffs/tests/common/mod.rs | 129 ------------------------ 8 files changed, 187 insertions(+), 389 deletions(-) delete mode 100644 ocpi-tariffs/tests/common/mod.rs diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 683d877..60c1a94 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -3,7 +3,6 @@ use std::{ fmt::Display, fs::File, io::{stdin, Read}, - ops::Mul, path::PathBuf, process::exit, }; @@ -28,7 +27,7 @@ use ocpi_tariffs::{ tariff::{CompatibilityVat, OcpiTariff}, v211, }, - pricer::{DimensionReport, Pricer, Report}, + pricer::{Dimension, DimensionReport, Pricer, Report}, types::{ electricity::Kwh, money::{Money, Price}, @@ -427,7 +426,7 @@ impl PeriodTable { pub fn row(&mut self, dim: &DimensionReport, time: DateTime) where - T: Into + Mul + Copy, + T: Into + Dimension, { let cost = dim.cost(); self.rows.push(PeriodComponent { diff --git a/ocpi-tariffs/src/pricer.rs b/ocpi-tariffs/src/pricer.rs index 878d71b..9ef8613 100644 --- a/ocpi-tariffs/src/pricer.rs +++ b/ocpi-tariffs/src/pricer.rs @@ -1,5 +1,3 @@ -use std::ops::Mul; - use crate::{ ocpi::{ cdr::Cdr, @@ -81,14 +79,18 @@ impl Pricer { let dimensions = Dimensions::new(components, &period.period_data); - total_charging_time += dimensions.time.volume.unwrap_or_else(HoursDecimal::zero); + total_charging_time = total_charging_time + .saturating_add(dimensions.time.volume.unwrap_or_else(HoursDecimal::zero)); - total_energy += dimensions.energy.volume.unwrap_or_else(Kwh::zero); + total_energy = + total_energy.saturating_add(dimensions.energy.volume.unwrap_or_else(Kwh::zero)); - total_parking_time += dimensions - .parking_time - .volume - .unwrap_or_else(HoursDecimal::zero); + total_parking_time = total_parking_time.saturating_add( + dimensions + .parking_time + .volume + .unwrap_or_else(HoursDecimal::zero), + ); periods.push(PeriodReport::new(period, dimensions)); } @@ -97,32 +99,48 @@ impl Pricer { let billed_energy = step_size.apply_energy(&mut periods, total_energy); let billed_parking_time = step_size.apply_parking_time(&mut periods, total_parking_time); - let mut total_energy_cost = None; - let mut total_time_cost = None; - let mut total_parking_cost = None; - let mut total_fixed_cost = None; + let mut total_energy_cost: Option = None; + let mut total_time_cost: Option = None; + let mut total_parking_cost: Option = None; + let mut total_fixed_cost: Option = None; for period in &periods { let dimensions = &period.dimensions; total_energy_cost = match (total_energy_cost, dimensions.energy.cost()) { (None, None) => None, - (total, period) => Some(total.unwrap_or_default() + period.unwrap_or_default()), + (total, period) => Some( + total + .unwrap_or_default() + .saturating_add(period.unwrap_or_default()), + ), }; total_time_cost = match (total_time_cost, dimensions.time.cost()) { (None, None) => None, - (total, period) => Some(total.unwrap_or_default() + period.unwrap_or_default()), + (total, period) => Some( + total + .unwrap_or_default() + .saturating_add(period.unwrap_or_default()), + ), }; total_parking_cost = match (total_parking_cost, dimensions.parking_time.cost()) { (None, None) => None, - (total, period) => Some(total.unwrap_or_default() + period.unwrap_or_default()), + (total, period) => Some( + total + .unwrap_or_default() + .saturating_add(period.unwrap_or_default()), + ), }; total_fixed_cost = match (total_fixed_cost, dimensions.flat.cost()) { (None, None) => None, - (total, period) => Some(total.unwrap_or_default() + period.unwrap_or_default()), + (total, period) => Some( + total + .unwrap_or_default() + .saturating_add(period.unwrap_or_default()), + ), }; } @@ -143,9 +161,13 @@ impl Pricer { total_energy_cost, ] .into_iter() - .fold(None, |accum, next| match (accum, next) { + .fold(None, |accum: Option, next| match (accum, next) { (None, None) => None, - _ => Some(accum.unwrap_or_default() + next.unwrap_or_default()), + _ => Some( + accum + .unwrap_or_default() + .saturating_add(next.unwrap_or_default()), + ), }); let report = Report { @@ -206,23 +228,28 @@ impl StepSize { } fn duration_step_size( - total: HoursDecimal, - billed_volume: &mut HoursDecimal, + total_volume: HoursDecimal, + period_billed_volume: &mut HoursDecimal, step_size: u64, ) -> HoursDecimal { if step_size > 0 { - let total_seconds = total.as_num_seconds_decimal(); + let total_seconds = total_volume.as_num_seconds_decimal(); let step_size = Number::from(step_size); - let priced_total_seconds = (total_seconds / step_size).ceil() * step_size; - let priced_total = - HoursDecimal::from_seconds_decimal(priced_total_seconds).expect("overflow"); + let total_billed_volume = HoursDecimal::from_seconds_decimal( + total_seconds + .checked_div(step_size) + .ceil() + .saturating_mul(step_size), + ) + .expect("overflow"); - *billed_volume += priced_total - total; + let period_delta_volume = total_billed_volume.saturating_sub(total_volume); + *period_billed_volume = period_billed_volume.saturating_add(period_delta_volume); - priced_total + total_billed_volume } else { - total + total_volume } } @@ -262,29 +289,35 @@ impl StepSize { } } - fn apply_energy(&self, periods: &mut [PeriodReport], total: Kwh) -> Kwh { + fn apply_energy(&self, periods: &mut [PeriodReport], total_volume: Kwh) -> Kwh { if let Some((energy_index, price)) = &self.energy { if price.step_size > 0 { let period = &mut periods[*energy_index]; let step_size = Number::from(price.step_size); - let volume = period + let period_billed_volume = period .dimensions .energy .billed_volume .as_mut() .expect("dimension should have a volume"); - let billed = - Kwh::from_watt_hours((total.watt_hours() / step_size).ceil() * step_size); + let total_billed_volume = Kwh::from_watt_hours( + total_volume + .watt_hours() + .checked_div(step_size) + .ceil() + .saturating_mul(step_size), + ); - *volume += billed - total; + let period_delta_volume = total_billed_volume.saturating_sub(total_volume); + *period_billed_volume = period_billed_volume.saturating_add(period_delta_volume); - return billed; + return total_billed_volume; } } - total + total_volume } } @@ -357,7 +390,11 @@ impl PeriodReport { if accum.is_none() && next.is_none() { None } else { - Some(accum.unwrap_or_default() + next.unwrap_or_default()) + Some( + accum + .unwrap_or_default() + .saturating_add(next.unwrap_or_default()), + ) } }) } @@ -424,17 +461,14 @@ where } } -impl DimensionReport -where - V: Mul + Copy, -{ +impl DimensionReport { /// The total cost of this dimension during a period. pub fn cost(&self) -> Option { if let (Some(volume), Some(price)) = (self.billed_volume, self.price) { - let excl_vat = volume * price.price; + let excl_vat = volume.cost(price.price); let incl_vat = match price.vat { - CompatibilityVat::Vat(Some(vat)) => Some(excl_vat * vat), + CompatibilityVat::Vat(Some(vat)) => Some(excl_vat.apply_vat(vat)), CompatibilityVat::Vat(None) => Some(excl_vat), CompatibilityVat::Unknown => None, }; @@ -445,3 +479,27 @@ where } } } + +/// An OCPI tariff dimension +pub trait Dimension: Copy { + /// The cost of this dimension at a certain price. + fn cost(&self, price: Money) -> Money; +} + +impl Dimension for Kwh { + fn cost(&self, price: Money) -> Money { + price.kwh_cost(*self) + } +} + +impl Dimension for () { + fn cost(&self, price: Money) -> Money { + price + } +} + +impl Dimension for HoursDecimal { + fn cost(&self, price: Money) -> Money { + price.time_cost(*self) + } +} diff --git a/ocpi-tariffs/src/session.rs b/ocpi-tariffs/src/session.rs index 1012906..2b0c00f 100644 --- a/ocpi-tariffs/src/session.rs +++ b/ocpi-tariffs/src/session.rs @@ -135,7 +135,7 @@ impl InstantData { } if let Some(energy) = state.energy { - next.total_energy += energy; + next.total_energy = next.total_energy.saturating_add(energy); } next diff --git a/ocpi-tariffs/src/types/electricity.rs b/ocpi-tariffs/src/types/electricity.rs index 4efc459..8249cae 100644 --- a/ocpi-tariffs/src/types/electricity.rs +++ b/ocpi-tariffs/src/types/electricity.rs @@ -1,9 +1,5 @@ -use std::{ - fmt::Display, - ops::{Add, AddAssign, Mul, Sub}, -}; +use std::fmt::Display; -use rust_decimal_macros::dec; use serde::{Deserialize, Serialize}; use super::number::Number; @@ -18,12 +14,22 @@ impl Kwh { Self(Number::default()) } + /// Saturating addition + pub fn saturating_add(self, other: Self) -> Self { + Self(self.0.saturating_add(other.0)) + } + + /// Saturating subtraction + pub fn saturating_sub(self, other: Self) -> Self { + Self(self.0.saturating_sub(other.0)) + } + pub(crate) fn watt_hours(self) -> Number { - self.0 * Number::from(dec!(1000.0)) + self.0.saturating_mul(Number::from(1000)) } pub(crate) fn from_watt_hours(num: Number) -> Self { - Self(num / dec!(1000.0).into()) + Self(num.checked_div(Number::from(1000))) } /// Round this number to the OCPI specified amount of decimals. @@ -44,36 +50,6 @@ impl Display for Kwh { } } -impl Add for Kwh { - type Output = Self; - - fn add(self, rhs: Self) -> Self::Output { - Self(self.0 + rhs.0) - } -} - -impl AddAssign for Kwh { - fn add_assign(&mut self, rhs: Self) { - self.0 = self.0 + rhs.0; - } -} - -impl Sub for Kwh { - type Output = Self; - - fn sub(self, rhs: Self) -> Self::Output { - Self(self.0 - rhs.0) - } -} - -impl Mul for Kwh { - type Output = Self; - - fn mul(self, rhs: Number) -> Self::Output { - Self(self.0 * rhs) - } -} - /// A value of kilo watts. #[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)] #[serde(transparent)] diff --git a/ocpi-tariffs/src/types/money.rs b/ocpi-tariffs/src/types/money.rs index 587fcef..7cd70ec 100644 --- a/ocpi-tariffs/src/types/money.rs +++ b/ocpi-tariffs/src/types/money.rs @@ -1,10 +1,5 @@ -use std::{ - fmt::Display, - ops::{Add, AddAssign, Mul}, -}; - -use rust_decimal_macros::dec; use serde::{Deserialize, Serialize}; +use std::fmt::Display; use super::{electricity::Kwh, number::Number, time::HoursDecimal}; @@ -37,33 +32,21 @@ impl Price { incl_vat: self.incl_vat.map(Money::with_scale), } } -} - -impl Add for Price { - type Output = Price; - fn add(self, rhs: Self) -> Self::Output { + /// Saturating addition. + pub fn saturating_add(self, rhs: Self) -> Self { Self { - excl_vat: self.excl_vat + rhs.excl_vat, + excl_vat: self.excl_vat.saturating_add(rhs.excl_vat), incl_vat: match (self.incl_vat, rhs.incl_vat) { - (Some(lhs_incl_vat), Some(rhs_incl_vat)) => Some(lhs_incl_vat + rhs_incl_vat), + (Some(lhs_incl_vat), Some(rhs_incl_vat)) => { + Some(lhs_incl_vat.saturating_add(rhs_incl_vat)) + } _ => None, }, } } } -impl AddAssign for Price { - fn add_assign(&mut self, rhs: Self) { - self.excl_vat = self.excl_vat + rhs.excl_vat; - - self.incl_vat = match (self.incl_vat, rhs.incl_vat) { - (Some(lhs_incl_vat), Some(rhs_incl_vat)) => Some(lhs_incl_vat + rhs_incl_vat), - _ => None, - }; - } -} - impl Default for Price { fn default() -> Self { Self::zero() @@ -84,79 +67,35 @@ impl Money { pub fn with_scale(self) -> Self { Self(self.0.with_scale()) } -} - -impl Add for Money { - type Output = Self; - - fn add(self, rhs: Self) -> Self::Output { - Self(self.0 + rhs.0) - } -} - -impl Mul for Money { - type Output = Money; - - fn mul(self, rhs: Number) -> Self::Output { - Self(self.0 * rhs) - } -} - -impl Mul for Number { - type Output = Money; - - fn mul(self, rhs: Money) -> Self::Output { - Money(rhs.0 * self) - } -} - -impl Mul for Money { - type Output = Money; - fn mul(self, rhs: Kwh) -> Self::Output { - Self(self.0 * Number::from(rhs)) + /// Saturating addition + pub fn saturating_add(self, other: Self) -> Self { + Self(self.0.saturating_add(other.0)) } -} - -impl Mul for Kwh { - type Output = Money; - fn mul(self, rhs: Money) -> Self::Output { - rhs * self + /// Saturating subtraction + pub fn saturating_sub(self, other: Self) -> Self { + Self(self.0.saturating_sub(other.0)) } -} - -impl Mul for Money { - type Output = Money; - fn mul(self, rhs: HoursDecimal) -> Self::Output { - let cost = self.0 * rhs.as_num_hours_decimal(); - - Self(cost) + /// Saturating multiplication + pub fn saturating_mul(self, other: Self) -> Self { + Self(self.0.saturating_mul(other.0)) } -} -impl Mul for HoursDecimal { - type Output = Money; - - fn mul(self, rhs: Money) -> Self::Output { - rhs * self + /// Apply a VAT percentage to this monetary amount. + pub fn apply_vat(self, vat: Vat) -> Self { + Self(self.0.saturating_mul(vat.as_fraction())) } -} - -impl Mul<()> for Money { - type Output = Money; - fn mul(self, _: ()) -> Self::Output { - self + /// Cost of a certain amount of [`Kwh`] with this price. + pub fn kwh_cost(self, kwh: Kwh) -> Self { + Self(self.0.saturating_mul(kwh.into())) } -} - -impl Mul for () { - type Output = Money; - fn mul(self, rhs: Money) -> Self::Output { - rhs * self + /// Cost of a certain amount of [`HoursDecimal`] with this price. + pub fn time_cost(self, hours: HoursDecimal) -> Self { + Self(self.0.saturating_mul(hours.as_num_hours_decimal())) } } @@ -177,19 +116,9 @@ impl Display for Money { #[serde(transparent)] pub struct Vat(Number); -impl Mul for Vat { - type Output = Money; - - fn mul(self, rhs: Money) -> Self::Output { - let vat = (self.0 / Number::from(dec!(100))) + Number::from(dec!(1.0)); - Money(rhs.0 * vat) - } -} - -impl Mul for Money { - type Output = Money; - fn mul(self, rhs: Vat) -> Self::Output { - rhs * self +impl Vat { + pub(crate) fn as_fraction(self) -> Number { + self.0.checked_div(100.into()).saturating_add(1.into()) } } diff --git a/ocpi-tariffs/src/types/number.rs b/ocpi-tariffs/src/types/number.rs index 12a45c0..2f487cc 100644 --- a/ocpi-tariffs/src/types/number.rs +++ b/ocpi-tariffs/src/types/number.rs @@ -1,7 +1,4 @@ -use std::{ - fmt::Display, - ops::{Add, Div, Mul, Sub}, -}; +use std::fmt::Display; use serde::{Deserialize, Deserializer, Serialize}; @@ -17,6 +14,22 @@ impl Number { self.0.rescale(4); self } + + pub(crate) fn checked_div(self, other: Self) -> Self { + Self(self.0.checked_div(other.0).expect("divide by zero")) + } + + pub(crate) fn saturating_sub(self, other: Self) -> Self { + Self(self.0.saturating_sub(other.0)) + } + + pub(crate) fn saturating_add(self, other: Self) -> Self { + Self(self.0.saturating_add(other.0)) + } + + pub(crate) fn saturating_mul(self, other: Self) -> Self { + Self(self.0.saturating_mul(other.0)) + } } impl<'de> Deserialize<'de> for Number { @@ -62,6 +75,12 @@ impl From for Number { } } +impl From for Number { + fn from(value: i32) -> Self { + Self(value.into()) + } +} + impl TryFrom for i64 { type Error = rust_decimal::Error; @@ -70,38 +89,6 @@ impl TryFrom for i64 { } } -impl Add for Number { - type Output = Self; - - fn add(self, rhs: Self) -> Self::Output { - Self(self.0.saturating_add(rhs.0)) - } -} - -impl Mul for Number { - type Output = Self; - - fn mul(self, rhs: Self) -> Self::Output { - Self(self.0.saturating_mul(rhs.0)) - } -} - -impl Div for Number { - type Output = Self; - - fn div(self, rhs: Self) -> Self::Output { - Self(self.0.checked_div(rhs.0).expect("divide by zero")) - } -} - -impl Sub for Number { - type Output = Self; - - fn sub(self, rhs: Self) -> Self::Output { - Self(self.0.saturating_sub(rhs.0)) - } -} - impl Display for Number { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) diff --git a/ocpi-tariffs/src/types/time.rs b/ocpi-tariffs/src/types/time.rs index 8d6621e..433213f 100644 --- a/ocpi-tariffs/src/types/time.rs +++ b/ocpi-tariffs/src/types/time.rs @@ -1,7 +1,4 @@ -use std::{ - fmt::Display, - ops::{Add, AddAssign, Sub, SubAssign}, -}; +use std::fmt::Display; use chrono::Duration; use serde::{Deserialize, Serialize, Serializer}; @@ -63,64 +60,45 @@ impl From for HoursDecimal { } } -impl AddAssign for HoursDecimal { - fn add_assign(&mut self, rhs: Self) { - self.0 = self - .0 - .checked_add(&rhs.0) - .unwrap_or_else(Duration::max_value); - } -} - -impl SubAssign for HoursDecimal { - fn sub_assign(&mut self, rhs: Self) { - self.0 = self.0.checked_sub(&rhs.0).unwrap_or_else(Duration::zero); - } -} - -impl Add for HoursDecimal { - type Output = Self; - - fn add(mut self, rhs: Self) -> Self::Output { - self += rhs; - - self - } -} - -impl Sub for HoursDecimal { - type Output = Self; - - fn sub(mut self, rhs: Self) -> Self::Output { - self -= rhs; - - self - } -} - impl HoursDecimal { pub(crate) fn zero() -> Self { Self(Duration::zero()) } pub(crate) fn as_num_seconds_decimal(&self) -> Number { - Number::from(self.0.num_milliseconds()) / Number::from(MILLIS_IN_SEC) + Number::from(self.0.num_milliseconds()).checked_div(Number::from(MILLIS_IN_SEC)) } pub(crate) fn as_num_hours_decimal(&self) -> Number { Number::from(self.0.num_milliseconds()) - / Number::from(MILLIS_IN_SEC * SECS_IN_MIN * MINS_IN_HOUR) + .checked_div(Number::from(MILLIS_IN_SEC * SECS_IN_MIN * MINS_IN_HOUR)) } pub(crate) fn from_seconds_decimal(seconds: Number) -> Result { - let millis = seconds * Number::from(MILLIS_IN_SEC); + let millis = seconds.saturating_mul(Number::from(MILLIS_IN_SEC)); + Ok(Self(Duration::milliseconds(millis.try_into()?))) } pub(crate) fn from_hours_decimal(hours: Number) -> Result { - let millis = hours * Number::from(MILLIS_IN_SEC * SECS_IN_MIN * MINS_IN_HOUR); + let millis = hours.saturating_mul(Number::from(MILLIS_IN_SEC * SECS_IN_MIN * MINS_IN_HOUR)); + Ok(Self(Duration::milliseconds(millis.try_into()?))) } + + /// Saturating subtraction. + pub fn saturating_sub(self, other: Self) -> Self { + Self(self.0.checked_sub(&other.0).unwrap_or_else(Duration::zero)) + } + + /// Saturating addition. + pub fn saturating_add(self, other: Self) -> Self { + Self( + self.0 + .checked_add(&other.0) + .unwrap_or_else(Duration::max_value), + ) + } } impl Default for HoursDecimal { diff --git a/ocpi-tariffs/tests/common/mod.rs b/ocpi-tariffs/tests/common/mod.rs deleted file mode 100644 index 6f5924f..0000000 --- a/ocpi-tariffs/tests/common/mod.rs +++ /dev/null @@ -1,129 +0,0 @@ -use std::{ - fs::{read_dir, File}, - path::PathBuf, -}; - -use chrono_tz::Tz; -use ocpi_tariffs::{ - ocpi::{cdr::Cdr, tariff::OcpiTariff}, - pricer::Pricer, -}; - -pub struct JsonTest { - pub path: PathBuf, - pub tariff: OcpiTariff, - pub cdrs: Vec<(String, Cdr)>, -} - -pub fn collect_json_tests() -> Result, Box> { - let mut tests = Vec::new(); - - for test_dir in read_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/resources"))? { - let test_dir_path = test_dir?.path(); - - if !test_dir_path.is_dir() { - continue; - } - - let mut tariff = None; - let mut cdrs = Vec::new(); - - for json_file in read_dir(&test_dir_path)? { - let file_path = json_file?.path(); - - if file_path.extension().unwrap() != "json" { - continue; - } - - let file_stem = file_path.file_stem().unwrap(); - if file_stem == "tariff" { - tariff = Some(serde_json::from_reader(File::open(file_path)?)?); - } else { - cdrs.push(( - file_stem.to_string_lossy().to_string(), - serde_json::from_reader(File::open(file_path)?)?, - )); - } - } - - tests.push(JsonTest { - tariff: tariff - .unwrap_or_else(|| panic!("no tariff.json in test directory {:?}", test_dir_path)), - cdrs, - path: test_dir_path, - }); - } - - Ok(tests) -} - -#[macro_export] -macro_rules! tariff { - ($name:literal) => { - serde_json::from_str::<'_, ocpi_tariffs::ocpi::tariff::OcpiTariff>(include_str!(concat!( - "../resources/", - $name, - "/tariff.json" - ))) - .unwrap() - }; -} - -pub fn validate_cdr(cdr: Cdr, tariff: OcpiTariff) -> Result<(), ocpi_tariffs::Error> { - let pricer = Pricer::with_tariffs(&cdr, &[tariff], Tz::UTC); - let report = pricer.build_report()?; - - assert_eq!( - cdr.total_cost, - report.total_cost.unwrap_or_default().with_scale(), - "total_cost" - ); - - assert_eq!( - cdr.total_energy, - report.total_energy.with_scale(), - "total_energy" - ); - assert_eq!( - cdr.total_energy_cost.unwrap_or_default(), - report.total_energy_cost.unwrap_or_default().with_scale(), - "total_energy_cost" - ); - - assert_eq!(cdr.total_time, report.total_time, "total_time"); - - assert_eq!( - cdr.total_time_cost.unwrap_or_default(), - report.total_time_cost.unwrap_or_default().with_scale(), - "total_time_cost" - ); - - assert_eq!( - cdr.total_parking_time.unwrap_or_default(), - report.total_parking_time, - "total_parking_time" - ); - - assert_eq!( - cdr.total_parking_cost.unwrap_or_default(), - report.total_parking_cost.unwrap_or_default().with_scale(), - "total_parking_cost" - ); - - assert_eq!( - cdr.total_reservation_cost.unwrap_or_default(), - report - .total_reservation_cost - .unwrap_or_default() - .with_scale(), - "total_reservation_cost" - ); - - assert_eq!( - cdr.total_fixed_cost.unwrap_or_default(), - report.total_fixed_cost.unwrap_or_default().with_scale(), - "total_fixed_cost" - ); - - Ok(()) -}