use crate::version::EegGesetz;
use rust_decimal::Decimal;
use time::OffsetDateTime;
#[derive(Debug, Clone, Copy)]
pub struct NegativpreisInterval {
pub start: OffsetDateTime,
pub feed_in_kwh: Decimal,
pub price_negative: bool,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct NegativpreisResult {
pub kwh_during_negative: Decimal,
pub negative_quarter_hours: u64,
}
#[must_use]
pub fn derive_negativpreis(
intervals: &[NegativpreisInterval],
version: EegGesetz,
) -> NegativpreisResult {
if version.negativpreis_stunden_schwelle().is_none() {
return NegativpreisResult::default();
}
let min_run_qh: usize = match version.negativpreis_stunden_schwelle() {
Some(_) if matches!(version, EegGesetz::Eeg2023) => 1,
Some(h) => (h as usize) * 4,
None => return NegativpreisResult::default(),
};
let mut result = NegativpreisResult::default();
let mut i = 0;
while i < intervals.len() {
if !intervals[i].price_negative {
i += 1;
continue;
}
let run_start = i;
let mut j = i + 1;
while j < intervals.len()
&& intervals[j].price_negative
&& intervals[j].start == intervals[j - 1].start + time::Duration::minutes(15)
{
j += 1;
}
if j - run_start >= min_run_qh {
for iv in &intervals[run_start..j] {
result.kwh_during_negative += iv.feed_in_kwh.max(Decimal::ZERO);
result.negative_quarter_hours += 1;
}
}
i = j;
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal::dec;
use time::macros::datetime;
fn qh(n: i64, kwh: &str, neg: bool) -> NegativpreisInterval {
NegativpreisInterval {
start: datetime!(2026-06-01 00:00 UTC) + time::Duration::minutes(15 * n),
feed_in_kwh: dec!(0) + kwh.parse::<Decimal>().unwrap(),
price_negative: neg,
}
}
#[test]
fn eeg2023_any_negative_quarter_hour_qualifies() {
let ivs = [qh(0, "10", false), qh(1, "12", true), qh(2, "8", false)];
let r = derive_negativpreis(&ivs, EegGesetz::Eeg2023);
assert_eq!(r.kwh_during_negative, dec!(12));
assert_eq!(r.negative_quarter_hours, 1);
}
#[test]
fn eeg2021_requires_four_consecutive_hours() {
let short: Vec<_> = (0..8).map(|n| qh(n, "5", true)).collect();
assert_eq!(
derive_negativpreis(&short, EegGesetz::Eeg2021),
NegativpreisResult::default()
);
let long: Vec<_> = (0..16).map(|n| qh(n, "5", true)).collect();
let r = derive_negativpreis(&long, EegGesetz::Eeg2021);
assert_eq!(r.kwh_during_negative, dec!(80));
assert_eq!(r.negative_quarter_hours, 16);
assert_eq!(
derive_negativpreis(&long, EegGesetz::Eeg2023).negative_quarter_hours,
16
);
}
#[test]
fn a_gap_breaks_the_consecutive_run() {
let mut ivs: Vec<_> = (0..15).map(|n| qh(n, "5", true)).collect();
ivs.push(qh(15, "5", false));
ivs.push(qh(16, "5", true));
assert_eq!(
derive_negativpreis(&ivs, EegGesetz::Eeg2021),
NegativpreisResult::default()
);
}
#[test]
fn negative_feed_in_is_floored() {
let ivs = [qh(0, "-3", true)];
assert_eq!(
derive_negativpreis(&ivs, EegGesetz::Eeg2023).kwh_during_negative,
dec!(0)
);
}
}