use time::{OffsetDateTime, Weekday};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum TariffWindowDays {
WeekdaysOnly,
WeekdaysAndSaturday,
AllDays,
}
impl TariffWindowDays {
#[must_use]
pub fn is_ht_day(self, weekday: Weekday) -> bool {
match self {
Self::WeekdaysOnly => matches!(
weekday,
Weekday::Monday
| Weekday::Tuesday
| Weekday::Wednesday
| Weekday::Thursday
| Weekday::Friday
),
Self::WeekdaysAndSaturday => !matches!(weekday, Weekday::Sunday),
Self::AllDays => true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct TariffWindow {
pub hour_from: u8,
pub hour_to: u8,
pub days: TariffWindowDays,
}
impl TariffWindow {
pub const BDEW_STANDARD: Self = Self {
hour_from: 6,
hour_to: 22,
days: TariffWindowDays::WeekdaysOnly,
};
pub const MON_SAT_0600_2200: Self = Self {
hour_from: 6,
hour_to: 22,
days: TariffWindowDays::WeekdaysAndSaturday,
};
#[must_use]
pub fn is_ht(&self, ts: OffsetDateTime) -> bool {
use time_tz::{OffsetDateTimeExt, timezones};
let berlin = timezones::db::europe::BERLIN;
let local = ts.to_timezone(berlin);
let hour = local.hour();
let weekday = local.weekday();
self.days.is_ht_day(weekday) && hour >= self.hour_from && hour < self.hour_to
}
#[must_use]
pub fn register_name(&self, ts: OffsetDateTime) -> &'static str {
if self.is_ht(ts) { "HT" } else { "NT" }
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct HtNtSchedule {
pub dsop_mp_id: Option<String>,
pub windows: Vec<TariffWindow>,
}
impl HtNtSchedule {
#[must_use]
pub fn bdew_standard() -> Self {
Self {
dsop_mp_id: None,
windows: vec![TariffWindow::BDEW_STANDARD],
}
}
#[must_use]
pub fn is_ht(&self, ts: OffsetDateTime) -> bool {
self.windows.iter().any(|w| w.is_ht(ts))
}
}
#[cfg(test)]
mod tests {
use super::*;
use time::macros::datetime;
#[test]
fn bdew_standard_monday_morning_is_ht() {
let window = TariffWindow::BDEW_STANDARD;
assert!(
window.is_ht(datetime!(2026-01-05 8:00 UTC)),
"Mon 09:00 CET should be HT"
);
}
#[test]
fn bdew_standard_monday_night_is_nt() {
let window = TariffWindow::BDEW_STANDARD;
assert!(
!window.is_ht(datetime!(2026-01-05 22:00 UTC)),
"Mon 23:00 CET should be NT"
);
}
#[test]
fn bdew_standard_22_00_is_nt() {
let window = TariffWindow::BDEW_STANDARD;
assert!(
!window.is_ht(datetime!(2026-01-05 21:00 UTC)),
"Mon 22:00 CET exclusive = NT"
);
}
#[test]
fn bdew_standard_sunday_is_nt() {
let window = TariffWindow::BDEW_STANDARD;
assert!(
!window.is_ht(datetime!(2026-01-04 11:00 UTC)),
"Sunday should be NT"
);
}
#[test]
fn bdew_standard_saturday_is_nt() {
let window = TariffWindow::BDEW_STANDARD;
assert!(
!window.is_ht(datetime!(2026-01-03 9:00 UTC)),
"Saturday should be NT"
);
}
#[test]
fn mon_sat_schedule_saturday_morning_is_ht() {
let window = TariffWindow::MON_SAT_0600_2200;
assert!(
window.is_ht(datetime!(2026-01-03 9:00 UTC)),
"Sat 10:00 CET should be HT"
);
}
#[test]
fn dst_summer_time_is_handled() {
let window = TariffWindow::BDEW_STANDARD;
assert!(
window.is_ht(datetime!(2026-06-15 7:00 UTC)),
"Mon 09:00 CEST should be HT"
);
assert!(
!window.is_ht(datetime!(2026-06-15 20:00 UTC)),
"Mon 22:00 CEST should be NT"
);
}
#[test]
fn schedule_is_ht_delegates_to_windows() {
let schedule = HtNtSchedule::bdew_standard();
assert!(schedule.is_ht(datetime!(2026-01-05 8:00 UTC)));
assert!(!schedule.is_ht(datetime!(2026-01-04 11:00 UTC)));
}
#[test]
fn register_name_returns_correct_string() {
let window = TariffWindow::BDEW_STANDARD;
assert_eq!(window.register_name(datetime!(2026-01-05 8:00 UTC)), "HT");
assert_eq!(window.register_name(datetime!(2026-01-04 11:00 UTC)), "NT");
}
}