use crate::rounding::RoundMoney;
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 guetefaktor: Decimal,
pub korrekturfaktor: Decimal,
pub suedregion: 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_kfm(5)
}
#[must_use]
pub fn from_guetefaktor(guetefaktor: Decimal, suedregion: bool) -> Self {
Self {
guetefaktor,
korrekturfaktor: korrekturfaktor_fuer_guetefaktor(guetefaktor, suedregion),
suedregion,
standortklasse: WindStandortklasse::from_guetefaktor(guetefaktor),
}
}
}
pub const KORREKTURFAKTOR_STUETZWERTE: [(Decimal, Decimal); 11] = [
(dec!(0.50), dec!(1.55)),
(dec!(0.60), dec!(1.42)),
(dec!(0.70), dec!(1.29)),
(dec!(0.80), dec!(1.16)),
(dec!(0.90), dec!(1.07)),
(dec!(1.00), dec!(1.00)),
(dec!(1.10), dec!(0.94)),
(dec!(1.20), dec!(0.89)),
(dec!(1.30), dec!(0.85)),
(dec!(1.40), dec!(0.81)),
(dec!(1.50), dec!(0.79)),
];
#[must_use]
pub fn korrekturfaktor_fuer_guetefaktor(guetefaktor: Decimal, suedregion: bool) -> Decimal {
let (lowest_gf, lowest_kf) = KORREKTURFAKTOR_STUETZWERTE[0];
let (highest_gf, highest_kf) =
KORREKTURFAKTOR_STUETZWERTE[KORREKTURFAKTOR_STUETZWERTE.len() - 1];
let floor_gf = if suedregion { lowest_gf } else { dec!(0.60) };
if guetefaktor <= floor_gf {
return if suedregion { lowest_kf } else { dec!(1.42) };
}
if guetefaktor >= highest_gf {
return highest_kf; }
let upper = KORREKTURFAKTOR_STUETZWERTE
.iter()
.position(|(gf, _)| *gf >= guetefaktor)
.unwrap_or(KORREKTURFAKTOR_STUETZWERTE.len() - 1);
let (gf_lo, kf_lo) = KORREKTURFAKTOR_STUETZWERTE[upper - 1];
let (gf_hi, kf_hi) = KORREKTURFAKTOR_STUETZWERTE[upper];
(kf_lo + (kf_hi - kf_lo) * (guetefaktor - gf_lo) / (gf_hi - gf_lo)).round_kfm(5)
}
#[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,
pub korrekturfaktor: 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| r.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,
Suedregion,
}
impl WindStandortklasse {
#[must_use]
pub fn from_guetefaktor(guetefaktor: Decimal) -> Self {
if guetefaktor >= dec!(1.50) {
Self::Excellent
} else if guetefaktor >= dec!(1.10) {
Self::AboveReference
} else if guetefaktor >= dec!(0.90) {
Self::Reference
} else if guetefaktor >= dec!(0.60) {
Self::BelowReference
} else {
Self::Suedregion
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use time::macros::date;
#[test]
fn sect36h_abs1_satz2_stuetzwerte() {
for (gf, kf) in [
(dec!(0.50), dec!(1.55)),
(dec!(0.60), dec!(1.42)),
(dec!(0.70), dec!(1.29)),
(dec!(0.80), dec!(1.16)),
(dec!(0.90), dec!(1.07)),
(dec!(1.00), dec!(1.00)),
(dec!(1.10), dec!(0.94)),
(dec!(1.20), dec!(0.89)),
(dec!(1.30), dec!(0.85)),
(dec!(1.40), dec!(0.81)),
(dec!(1.50), dec!(0.79)),
] {
assert_eq!(
korrekturfaktor_fuer_guetefaktor(gf, true),
kf,
"Gütefaktor {gf} (Südregion)"
);
if gf >= dec!(0.60) {
assert_eq!(korrekturfaktor_fuer_guetefaktor(gf, false), kf);
}
}
}
#[test]
fn sect36h_reference_site_is_exactly_one() {
assert_eq!(
korrekturfaktor_fuer_guetefaktor(Decimal::ONE, false),
dec!(1)
);
assert_eq!(
WindStandort::from_guetefaktor(Decimal::ONE, false).effective_aw(dec!(7.35)),
dec!(7.35)
);
}
#[test]
fn sect36h_abs1_satz3_interpolates_linearly() {
assert_eq!(
korrekturfaktor_fuer_guetefaktor(dec!(0.95), false),
dec!(1.035)
);
assert_eq!(
korrekturfaktor_fuer_guetefaktor(dec!(1.02), false),
dec!(0.988)
);
}
#[test]
fn sect36h_abs1_satz4_out_of_range_rules() {
assert_eq!(
korrekturfaktor_fuer_guetefaktor(dec!(1.80), false),
dec!(0.79)
);
assert_eq!(
korrekturfaktor_fuer_guetefaktor(dec!(0.40), true),
dec!(1.55)
);
assert_eq!(
korrekturfaktor_fuer_guetefaktor(dec!(0.55), false),
dec!(1.42)
);
assert_eq!(
korrekturfaktor_fuer_guetefaktor(dec!(0.10), false),
dec!(1.42)
);
}
#[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),
korrekturfaktor: dec!(1.07),
},
GuetefaktorReeval {
wirksam_ab_jahr: 11,
guetefaktor: dec!(0.95),
korrekturfaktor: dec!(1.035),
},
];
assert_eq!(
korrekturfaktor_fuer_periode(ibn, date!(2028 - 12 - 01), initial, &reevals),
initial
);
assert_eq!(
korrekturfaktor_fuer_periode(ibn, date!(2029 - 07 - 01), initial, &reevals),
dec!(1.07)
);
assert_eq!(
korrekturfaktor_fuer_periode(ibn, date!(2034 - 08 - 01), initial, &reevals),
dec!(1.035)
);
}
#[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_guetefaktor() {
assert_eq!(
WindStandortklasse::from_guetefaktor(dec!(1.60)),
WindStandortklasse::Excellent
);
assert_eq!(
WindStandortklasse::from_guetefaktor(dec!(1.20)),
WindStandortklasse::AboveReference
);
assert_eq!(
WindStandortklasse::from_guetefaktor(dec!(1.00)),
WindStandortklasse::Reference
);
assert_eq!(
WindStandortklasse::from_guetefaktor(dec!(0.85)),
WindStandortklasse::BelowReference
);
assert_eq!(
WindStandortklasse::from_guetefaktor(dec!(0.55)),
WindStandortklasse::Suedregion
);
}
#[test]
fn effective_aw_applies_korrekturfaktor() {
let standort = WindStandort {
guetefaktor: dec!(0.85),
korrekturfaktor: dec!(1.115),
suedregion: false,
standortklasse: WindStandortklasse::BelowReference,
};
assert_eq!(standort.effective_aw(dec!(7.35)), dec!(8.19525));
}
#[test]
fn below_reference_site_raises_the_aw() {
let standort = WindStandort::from_guetefaktor(dec!(0.85), false);
assert_eq!(standort.korrekturfaktor, dec!(1.115));
assert!(standort.korrekturfaktor > Decimal::ONE);
}
}