use rust_decimal::{Decimal, dec};
use time::{Date, Month, OffsetDateTime};
use super::{Einspeisung, Gate, Pauschalfall};
use crate::eeg::{EegError, add, align, mul, share, sub, sum};
use crate::series::Series;
use crate::series::interval::QualityFlag;
use crate::time::calendar::DayBoundary;
pub const FOERDERGRENZE_KWH_JE_KWP: Decimal = dec!(500);
pub const INDIFFERENZ_SPEICHER_KWH_JE_KW: Decimal = dec!(0.1);
pub const INDIFFERENZ_LADEPUNKT: Decimal = dec!(0.2);
pub const SOMMERTAGE: u16 = 183;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Pauschalbasis {
Stromspeicher {
kapazitaet_kwh: Decimal,
},
Ladepunkt,
StromspeicherUndLadepunkt {
kapazitaet_kwh: Decimal,
},
}
impl Pauschalbasis {
#[must_use]
pub const fn fall(&self) -> Pauschalfall {
match self {
Self::Stromspeicher { .. } => Pauschalfall::P1,
Self::Ladepunkt => Pauschalfall::P2,
Self::StromspeicherUndLadepunkt { .. } => Pauschalfall::P3,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Pauschalgrenzen {
pub pinst: Decimal,
pub p1: Decimal,
pub p2: Decimal,
pub p3: Decimal,
pub p4: Decimal,
p3_num: Decimal,
p3_den: Decimal,
}
impl Pauschalgrenzen {
pub fn new(basis: Pauschalbasis, pinst_kwp: Decimal) -> Result<Self, EegError> {
if pinst_kwp <= Decimal::ZERO {
return Err(EegError::ZeroWeight {
formula: "(P1)",
what: "Pinst",
});
}
let p1 = mul(pinst_kwp, FOERDERGRENZE_KWH_JE_KWP)?;
let storage = |sk: Decimal| -> Result<(Decimal, Decimal), EegError> {
if sk <= Decimal::ZERO {
return Err(EegError::ZeroWeight {
formula: "(P2)P1",
what: "SKinst",
});
}
Ok((mul(INDIFFERENZ_SPEICHER_KWH_JE_KW, pinst_kwp)?, sk))
};
let (p2_num, p2_den) = match basis {
Pauschalbasis::Stromspeicher { kapazitaet_kwh } => storage(kapazitaet_kwh)?,
Pauschalbasis::Ladepunkt => (INDIFFERENZ_LADEPUNKT, Decimal::ONE),
Pauschalbasis::StromspeicherUndLadepunkt { kapazitaet_kwh } => {
let (n, d) = storage(kapazitaet_kwh)?;
if n <= mul(INDIFFERENZ_LADEPUNKT, d)? {
(n, d)
} else {
(INDIFFERENZ_LADEPUNKT, Decimal::ONE)
}
}
};
let p2 = share(p2_num, Decimal::ONE, p2_den)?.ok_or(EegError::Overflow)?;
let p3_num = mul(p2_num, p1)?;
let p3 = share(p3_num, Decimal::ONE, p2_den)?.ok_or(EegError::Overflow)?;
Ok(Self {
pinst: pinst_kwp,
p1,
p2,
p3,
p4: add(p1, p3)?,
p3_num,
p3_den: p2_den,
})
}
pub fn rumpfjahr(&self, erster_tag: Date, letzter_tag: Date) -> Result<Rumpfjahr, EegError> {
let year = erster_tag.year();
if letzter_tag < erster_tag || letzter_tag.year() != year {
return Err(EegError::Calendar);
}
let days = |a: Date, b: Date| u16::try_from((b - a).whole_days() + 1).ok();
let tr = days(erster_tag, letzter_tag).ok_or(EegError::Calendar)?;
let april =
Date::from_calendar_date(year, Month::April, 1).map_err(|_| EegError::Calendar)?;
let september =
Date::from_calendar_date(year, Month::September, 30).map_err(|_| EegError::Calendar)?;
let (lo, hi) = (erster_tag.max(april), letzter_tag.min(september));
let trs = if lo <= hi {
days(lo, hi).ok_or(EegError::Calendar)?
} else {
0
};
let tk = time::util::days_in_year(year);
let p1 = share(Decimal::from(trs), self.p1, Decimal::from(SOMMERTAGE))?
.ok_or(EegError::Overflow)?;
let p3 = share(
Decimal::from(tr),
self.p3_num,
mul(self.p3_den, Decimal::from(tk))?,
)?
.ok_or(EegError::Overflow)?;
Ok(Rumpfjahr {
erster_tag,
letzter_tag,
tr,
trs,
tk,
p1,
p3,
p4: add(p1, p3)?,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Rumpfjahr {
pub erster_tag: Date,
pub letzter_tag: Date,
pub tr: u16,
pub trs: u16,
pub tk: u16,
pub p1: Decimal,
pub p3: Decimal,
pub p4: Decimal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Zeitraum {
Kalenderjahr(i32),
Rumpfjahr {
erster_tag: Date,
letzter_tag: Date,
},
}
#[derive(Debug, Clone, Copy)]
pub struct SolarAnlage<'a> {
pub kwp: Decimal,
pub aw: Gate<'a>,
}
#[derive(Debug, Clone)]
pub struct Pauschaloption<'a> {
pub z1nb: &'a Series,
pub einspeisung: Einspeisung<'a>,
pub sp: Gate<'a>,
pub basis: Pauschalbasis,
pub anlagen: Vec<SolarAnlage<'a>>,
pub zeitraum: Zeitraum,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct PauschalViertelstunde {
pub from: OffsetDateTime,
pub to: OffsetDateTime,
pub z1nb: Decimal,
pub einspeisung: Decimal,
pub sp: bool,
pub aw: Vec<bool>,
pub quality: QualityFlag,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct SolarAnlageJahr {
pub p14: Decimal,
pub p15: Decimal,
pub p16: Decimal,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Pauschaljahr {
pub faelle: Vec<Pauschalfall>,
pub grenzen: Pauschalgrenzen,
pub rumpfjahr: Option<Rumpfjahr>,
pub p7: Decimal,
pub p8: Decimal,
pub p9: Decimal,
pub p10: Decimal,
pub p11: Decimal,
pub anlagen: Vec<SolarAnlageJahr>,
pub viertelstunden: Vec<PauschalViertelstunde>,
}
impl Pauschaljahr {
pub fn foerderfaehig(&self) -> Result<Decimal, EegError> {
sum(self.anlagen.iter().map(|a| a.p16))
}
}
impl Pauschaloption<'_> {
pub fn jahr(&self) -> Result<Pauschaljahr, EegError> {
if self.anlagen.is_empty() {
return Err(EegError::Inconsistent {
what: "the Pauschaloption needs at least one solar plant",
});
}
if self.anlagen.iter().any(|a| a.kwp <= Decimal::ZERO) {
return Err(EegError::ZeroWeight {
formula: "(ZF)",
what: "installed power",
});
}
let pinst = sum(self.anlagen.iter().map(|a| a.kwp))?;
let grenzen = Pauschalgrenzen::new(self.basis, pinst)?;
let (start, end, rumpfjahr) = match self.zeitraum {
Zeitraum::Kalenderjahr(y) => {
let p = DayBoundary::Strom.year(y).ok_or(EegError::Calendar)?;
(p.start(), p.end(), None)
}
Zeitraum::Rumpfjahr {
erster_tag,
letzter_tag,
} => {
let r = grenzen.rumpfjahr(erster_tag, letzter_tag)?;
let a = DayBoundary::Strom
.day(erster_tag)
.ok_or(EegError::Calendar)?;
let b = DayBoundary::Strom
.day(letzter_tag)
.ok_or(EegError::Calendar)?;
(a.start(), b.end(), Some(r))
}
};
let (p1, p4) = rumpfjahr.map_or((grenzen.p1, grenzen.p4), |r| (r.p1, r.p4));
let rows = align(&[
("Z1NB", self.z1nb),
(self.einspeisung.name(), self.einspeisung.series()),
])?;
if let Some(out) = rows.iter().find(|r| r.from < start || r.to > end) {
return Err(EegError::OutsidePeriod { at: out.from });
}
let viertelstunden: Vec<PauschalViertelstunde> = rows
.iter()
.map(|r| PauschalViertelstunde {
from: r.from,
to: r.to,
z1nb: r.values[0],
einspeisung: r.values[1],
sp: self.sp.passes(r.from),
aw: self.anlagen.iter().map(|a| a.aw.passes(r.from)).collect(),
quality: r.quality,
})
.collect();
let p7 = sum(viertelstunden
.iter()
.filter(|q| q.sp)
.map(|q| q.einspeisung))?;
let p8 = sub(p7, p4)?.max(Decimal::ZERO);
let p9 = sum(viertelstunden.iter().map(|q| q.z1nb))?;
let p10 = p8.min(p9);
let p11 = sub(p9, p10)?;
let anlagen = self
.anlagen
.iter()
.enumerate()
.map(|(k, a)| {
let p14 = sum(viertelstunden
.iter()
.filter(|q| q.aw[k])
.map(|q| q.einspeisung))?;
let p15 = p14.min(p1);
let p16 = share(a.kwp, p15, pinst)?.ok_or(EegError::Overflow)?;
Ok(SolarAnlageJahr { p14, p15, p16 })
})
.collect::<Result<Vec<_>, EegError>>()?;
let mut faelle = vec![self.basis.fall()];
if self.anlagen.len() > 1 {
faelle.push(Pauschalfall::P4);
}
if matches!(self.einspeisung, Einspeisung::Zw(_)) {
faelle.push(Pauschalfall::P5);
}
Ok(Pauschaljahr {
faelle,
grenzen,
rumpfjahr,
p7,
p8,
p9,
p10,
p11,
anlagen,
viertelstunden,
})
}
}