use rust_decimal::Decimal;
use rust_decimal::dec;
use time::Date;
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct WindStandort {
pub guetegrad: Decimal,
pub korrekturfaktor: Decimal,
pub grundverguetungsperiode_aktiv: bool,
pub standortklasse: WindStandortklasse,
}
impl WindStandort {
#[must_use]
pub fn effective_aw(&self, base_aw_ct_kwh: Decimal) -> Decimal {
(base_aw_ct_kwh * self.korrekturfaktor).round_dp(5)
}
#[must_use]
pub fn approximate_from_guetegrad(guetegrad: Decimal) -> Self {
let korrekturfaktor = if guetegrad < dec!(0.80) {
dec!(0.0) } else if guetegrad < Decimal::ONE {
(dec!(1.25) - dec!(0.25) * guetegrad).round_dp(4)
} else if guetegrad <= dec!(1.50) {
(dec!(0.95) - dec!(0.10) * (guetegrad - Decimal::ONE)).round_dp(4)
} else {
dec!(0.70) };
let standortklasse = WindStandortklasse::from_guetegrad(guetegrad);
let grundverguetungsperiode_aktiv = guetegrad < Decimal::ONE;
Self {
guetegrad,
korrekturfaktor,
grundverguetungsperiode_aktiv,
standortklasse,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GuetefaktorReeval {
pub wirksam_ab_jahr: u8,
pub guetefaktor: Decimal,
}
#[must_use]
pub fn korrekturfaktor_fuer_periode(
inbetriebnahme: Date,
billing_date: Date,
initial_korrekturfaktor: Decimal,
reevals: &[GuetefaktorReeval],
) -> Decimal {
reevals
.iter()
.filter(|r| jahr_erreicht(inbetriebnahme, billing_date, r.wirksam_ab_jahr))
.max_by_key(|r| r.wirksam_ab_jahr)
.map_or(initial_korrekturfaktor, |r| {
WindStandort::approximate_from_guetegrad(r.guetefaktor).korrekturfaktor
})
}
fn jahr_erreicht(inbetriebnahme: Date, billing_date: Date, n: u8) -> bool {
let target_year = inbetriebnahme.year() + i32::from(n) - 1;
match inbetriebnahme.replace_year(target_year) {
Ok(start) => billing_date >= start,
Err(_) => billing_date.year() >= target_year,
}
}
#[must_use]
pub fn reevaluation_requires_reconciliation(
previous_guetefaktor: Decimal,
new_guetefaktor: Decimal,
) -> bool {
(new_guetefaktor - previous_guetefaktor).abs() > dec!(0.02)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
pub enum WindStandortklasse {
Excellent,
AboveReference,
Reference,
BelowReference,
Marginal,
}
impl WindStandortklasse {
#[must_use]
pub fn from_guetegrad(guetegrad: Decimal) -> Self {
if guetegrad >= dec!(1.50) {
Self::Excellent
} else if guetegrad >= dec!(1.10) {
Self::AboveReference
} else if guetegrad >= dec!(0.90) {
Self::Reference
} else if guetegrad >= dec!(0.80) {
Self::BelowReference
} else {
Self::Marginal
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use time::macros::date;
#[test]
fn sect36h_abs2_korrekturfaktor_steps_at_year_6_and_11() {
let ibn = date!(2024 - 07 - 01);
let initial = dec!(1.06);
let reevals = [
GuetefaktorReeval {
wirksam_ab_jahr: 6,
guetefaktor: dec!(0.90),
},
GuetefaktorReeval {
wirksam_ab_jahr: 11,
guetefaktor: dec!(0.95),
},
];
assert_eq!(
korrekturfaktor_fuer_periode(ibn, date!(2028 - 12 - 01), initial, &reevals),
initial
);
let kf_y6 = WindStandort::approximate_from_guetegrad(dec!(0.90)).korrekturfaktor;
assert_eq!(
korrekturfaktor_fuer_periode(ibn, date!(2029 - 07 - 01), initial, &reevals),
kf_y6
);
let kf_y11 = WindStandort::approximate_from_guetegrad(dec!(0.95)).korrekturfaktor;
assert_eq!(
korrekturfaktor_fuer_periode(ibn, date!(2034 - 08 - 01), initial, &reevals),
kf_y11
);
}
#[test]
fn sect36h_abs2_reconciliation_threshold_is_two_points() {
assert!(!reevaluation_requires_reconciliation(
dec!(1.00),
dec!(1.02)
));
assert!(reevaluation_requires_reconciliation(
dec!(1.00),
dec!(1.021)
));
assert!(reevaluation_requires_reconciliation(dec!(1.05), dec!(1.00)));
}
#[test]
fn standortklasse_from_guetegrad() {
assert_eq!(
WindStandortklasse::from_guetegrad(dec!(1.60)),
WindStandortklasse::Excellent
);
assert_eq!(
WindStandortklasse::from_guetegrad(dec!(1.20)),
WindStandortklasse::AboveReference
);
assert_eq!(
WindStandortklasse::from_guetegrad(dec!(1.00)),
WindStandortklasse::Reference
);
assert_eq!(
WindStandortklasse::from_guetegrad(dec!(0.85)),
WindStandortklasse::BelowReference
);
assert_eq!(
WindStandortklasse::from_guetegrad(dec!(0.70)),
WindStandortklasse::Marginal
);
}
#[test]
fn effective_aw_applies_korrekturfaktor() {
let standort = WindStandort {
guetegrad: dec!(0.85),
korrekturfaktor: dec!(1.08),
grundverguetungsperiode_aktiv: true,
standortklasse: WindStandortklasse::BelowReference,
};
let effective = standort.effective_aw(dec!(7.35));
assert_eq!(effective, dec!(7.938));
}
#[test]
fn reference_site_korrekturfaktor_near_one() {
let standort = WindStandort::approximate_from_guetegrad(dec!(1.00));
assert!(standort.korrekturfaktor > dec!(0.90) && standort.korrekturfaktor < dec!(1.10));
assert!(!standort.grundverguetungsperiode_aktiv);
}
#[test]
fn below_reference_triggers_grundverguetungsperiode() {
let standort = WindStandort::approximate_from_guetegrad(dec!(0.85));
assert!(standort.grundverguetungsperiode_aktiv);
assert!(
standort.korrekturfaktor > Decimal::ONE,
"below-reference => higher AW"
);
}
}