use rust_decimal::Decimal;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub const IMBALANCE_PCT_DP: u32 = 2;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ImbalanceSaldo {
#[cfg_attr(feature = "serde", serde(with = "crate::wire::decimal"))]
pub actual_kwh: Decimal,
#[cfg_attr(feature = "serde", serde(with = "crate::wire::decimal"))]
pub contracted_kwh: Decimal,
#[cfg_attr(feature = "serde", serde(with = "crate::wire::decimal"))]
pub mehr_kwh: Decimal,
#[cfg_attr(feature = "serde", serde(with = "crate::wire::decimal"))]
pub minder_kwh: Decimal,
#[cfg_attr(feature = "serde", serde(with = "crate::wire::decimal"))]
pub delta_kwh: Decimal,
}
impl ImbalanceSaldo {
#[must_use]
pub fn is_mehr(&self) -> bool {
self.mehr_kwh > Decimal::ZERO
}
#[must_use]
pub fn is_minder(&self) -> bool {
self.minder_kwh > Decimal::ZERO
}
#[must_use]
pub fn is_balanced(&self) -> bool {
self.delta_kwh.is_zero()
}
#[must_use]
pub fn magnitude_kwh(&self) -> Decimal {
self.delta_kwh.abs()
}
#[must_use]
pub fn delta_pct(&self) -> Option<Decimal> {
if self.contracted_kwh.is_zero() {
None
} else {
Some(
(self.delta_kwh / self.contracted_kwh * Decimal::ONE_HUNDRED)
.round_dp_with_strategy(
IMBALANCE_PCT_DP,
rust_decimal::RoundingStrategy::MidpointAwayFromZero,
),
)
}
}
}
#[must_use]
pub fn compute_imbalance(actual_kwh: Decimal, contracted_kwh: Decimal) -> ImbalanceSaldo {
let delta = actual_kwh - contracted_kwh;
let mehr = (-delta).max(Decimal::ZERO);
let minder = delta.max(Decimal::ZERO);
ImbalanceSaldo {
actual_kwh,
contracted_kwh,
mehr_kwh: mehr,
minder_kwh: minder,
delta_kwh: delta,
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal::dec;
#[test]
fn over_consumption_is_a_mindermenge() {
let s = compute_imbalance(dec!(1050), dec!(1000));
assert_eq!(s.minder_kwh, dec!(50));
assert_eq!(s.mehr_kwh, Decimal::ZERO);
assert_eq!(s.delta_kwh, dec!(50));
assert!(s.is_minder());
assert!(!s.is_mehr());
}
#[test]
fn under_consumption_is_a_mehrmenge() {
let s = compute_imbalance(dec!(950), dec!(1000));
assert_eq!(s.mehr_kwh, dec!(50));
assert_eq!(s.minder_kwh, Decimal::ZERO);
assert_eq!(s.delta_kwh, dec!(-50));
assert!(s.is_mehr());
assert!(!s.is_minder());
}
#[test]
fn balanced_period() {
let s = compute_imbalance(dec!(1000), dec!(1000));
assert!(s.is_balanced());
assert_eq!(s.magnitude_kwh(), Decimal::ZERO);
assert_eq!(s.delta_pct(), Some(Decimal::ZERO));
}
#[test]
fn delta_pct_calculation() {
let s = compute_imbalance(dec!(1050), dec!(1000));
assert_eq!(s.delta_pct(), Some(dec!(5)));
}
#[test]
fn delta_pct_zero_contracted() {
let s = compute_imbalance(dec!(100), Decimal::ZERO);
assert_eq!(s.delta_pct(), None);
}
#[test]
fn mehr_and_minder_are_mutually_exclusive() {
for (actual, contracted) in [
(dec!(900), dec!(1000)),
(dec!(1100), dec!(1000)),
(dec!(1000), dec!(1000)),
] {
let s = compute_imbalance(actual, contracted);
assert!(
!(s.is_mehr() && s.is_minder()),
"mehr and minder cannot both be true"
);
}
}
}