use rust_decimal::{Decimal, dec};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub const RATIO_HALF: Decimal = dec!(0.5);
pub const RATIO_COS_PHI_0_9: Decimal = dec!(0.4843);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ReactiveLimit {
#[cfg_attr(feature = "serde", serde(with = "crate::wire::decimal"))]
pub ratio: Decimal,
}
impl ReactiveLimit {
#[must_use]
pub const fn new(ratio: Decimal) -> Self {
Self { ratio }
}
#[must_use]
pub const fn half() -> Self {
Self::new(RATIO_HALF)
}
#[must_use]
pub const fn cos_phi_0_9() -> Self {
Self::new(RATIO_COS_PHI_0_9)
}
}
impl Default for ReactiveLimit {
fn default() -> Self {
Self::half()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ReactiveBalance {
#[cfg_attr(feature = "serde", serde(with = "crate::wire::decimal"))]
pub wirkarbeit_kwh: Decimal,
#[cfg_attr(feature = "serde", serde(with = "crate::wire::decimal"))]
pub blindarbeit_kvarh: Decimal,
#[cfg_attr(feature = "serde", serde(with = "crate::wire::decimal"))]
pub ratio: Decimal,
#[cfg_attr(feature = "serde", serde(with = "crate::wire::decimal"))]
pub freigrenze_kvarh: Decimal,
#[cfg_attr(feature = "serde", serde(with = "crate::wire::decimal"))]
pub blindmehrarbeit_kvarh: Decimal,
}
impl ReactiveBalance {
#[must_use]
pub fn is_chargeable(&self) -> bool {
self.blindmehrarbeit_kvarh > Decimal::ZERO
}
#[must_use]
pub fn headroom_kvarh(&self) -> Decimal {
(self.freigrenze_kvarh - self.blindarbeit_kvarh).max(Decimal::ZERO)
}
}
#[must_use]
pub fn blindmehrarbeit(
wirkarbeit_kwh: Decimal,
blindarbeit_kvarh: Decimal,
limit: ReactiveLimit,
) -> ReactiveBalance {
let ratio = limit.ratio.max(Decimal::ZERO);
let freigrenze_kvarh = (ratio * wirkarbeit_kwh).max(Decimal::ZERO);
ReactiveBalance {
wirkarbeit_kwh,
blindarbeit_kvarh,
ratio,
freigrenze_kvarh,
blindmehrarbeit_kvarh: (blindarbeit_kvarh - freigrenze_kvarh).max(Decimal::ZERO),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_excess_is_what_the_freigrenze_does_not_cover() {
let b = blindmehrarbeit(dec!(1000), dec!(600), ReactiveLimit::half());
assert_eq!(b.freigrenze_kvarh, dec!(500.0));
assert_eq!(b.blindmehrarbeit_kvarh, dec!(100.0));
assert!(b.is_chargeable());
assert_eq!(b.headroom_kvarh(), Decimal::ZERO);
}
#[test]
fn a_compensated_load_pays_nothing_and_keeps_headroom() {
let b = blindmehrarbeit(dec!(1000), dec!(200), ReactiveLimit::half());
assert_eq!(b.blindmehrarbeit_kvarh, Decimal::ZERO);
assert!(!b.is_chargeable());
assert_eq!(b.headroom_kvarh(), dec!(300.0));
}
#[test]
fn cos_phi_zero_nine_is_stricter_than_the_half_rule() {
let half = blindmehrarbeit(dec!(1000), dec!(600), ReactiveLimit::half());
let strict = blindmehrarbeit(dec!(1000), dec!(600), ReactiveLimit::cos_phi_0_9());
assert!(strict.blindmehrarbeit_kvarh > half.blindmehrarbeit_kvarh);
assert_eq!(strict.freigrenze_kvarh, dec!(484.3000));
}
#[test]
fn the_balance_is_exact() {
let b = blindmehrarbeit(
dec!(1234.567),
dec!(1000.001),
ReactiveLimit::new(dec!(0.3333)),
);
assert_eq!(b.freigrenze_kvarh, dec!(1234.567) * dec!(0.3333));
assert_eq!(
b.blindmehrarbeit_kvarh + b.freigrenze_kvarh,
b.blindarbeit_kvarh
);
}
#[test]
fn a_negative_ratio_admits_nothing_rather_than_charging_extra() {
let b = blindmehrarbeit(dec!(1000), dec!(600), ReactiveLimit::new(dec!(-0.5)));
assert_eq!(b.ratio, Decimal::ZERO);
assert_eq!(b.freigrenze_kvarh, Decimal::ZERO);
assert_eq!(b.blindmehrarbeit_kvarh, dec!(600));
}
#[test]
fn the_cos_phi_constant_is_the_stated_rounding() {
assert_eq!(RATIO_COS_PHI_0_9, dec!(0.4843));
assert_eq!(RATIO_HALF, dec!(0.5));
assert_eq!(ReactiveLimit::default(), ReactiveLimit::half());
}
}