use std::collections::HashSet;
use tracing::trace;
use chrono::{DateTime, Duration, NaiveDate, NaiveTime, Timelike, Utc, Weekday};
use crate::{
currency,
json::FromJson as _,
price::{self, Consumed, PeriodNormalized},
warning::VerdictExt as _,
Ampere, Kw, Kwh, Verdict, Version, Versioned as _,
};
use super::TotalsSnapshot;
pub(super) fn parse<'buf>(
tariff: &crate::tariff::Versioned<'buf>,
) -> Verdict<crate::tariff::v221::Tariff<'buf>, crate::tariff::Warning> {
match tariff.version() {
Version::V211 => {
let tariff = crate::tariff::v211::Tariff::from_json(tariff.as_element());
tariff.map_caveat(crate::tariff::v221::Tariff::from)
}
Version::V221 => crate::tariff::v221::Tariff::from_json(tariff.as_element()),
}
}
#[derive(Debug)]
pub(crate) struct Tariff {
id: String,
currency: currency::Code,
elements: Vec<Element>,
start_date_time: Option<DateTime<Utc>>,
end_date_time: Option<DateTime<Utc>>,
}
impl Tariff {
pub(super) fn from_v221(tariff: &crate::tariff::v221::Tariff<'_>) -> Self {
let crate::tariff::v221::Tariff {
id,
elements,
start_date_time,
end_date_time,
party_id: _,
currency,
min_price: _,
max_price: _,
} = tariff;
let elements = elements
.iter()
.enumerate()
.map(|(element_index, element)| Element::new(element, element_index))
.collect();
Self {
id: id.to_string(),
currency: *currency,
start_date_time: *start_date_time,
end_date_time: *end_date_time,
elements,
}
}
pub(super) fn id(&self) -> &str {
&self.id
}
pub(super) fn currency(&self) -> currency::Code {
self.currency
}
pub(super) fn active_components(&self, period: &PeriodNormalized) -> price::ComponentSet {
let mut component_set = price::ComponentSet {
energy: None,
flat: None,
duration_charging: None,
duration_parking: None,
};
for tariff_element in &self.elements {
trace!("{period:#?}");
if !tariff_element.is_active(period) {
continue;
}
if component_set.duration_charging.is_none() {
component_set
.duration_charging
.clone_from(&tariff_element.components.duration_charging);
}
if component_set.duration_parking.is_none() {
component_set
.duration_parking
.clone_from(&tariff_element.components.duration_parking);
}
if component_set.energy.is_none() {
component_set
.energy
.clone_from(&tariff_element.components.energy);
}
if component_set.flat.is_none() {
component_set
.flat
.clone_from(&tariff_element.components.flat);
}
if component_set.has_all_components() {
break;
}
}
component_set
}
fn is_active(&self, start_time: DateTime<Utc>) -> bool {
let is_after_start = self
.start_date_time
.map(|s| start_time >= s)
.unwrap_or(true);
let is_before_end = self.end_date_time.map(|s| start_time < s).unwrap_or(true);
is_after_start && is_before_end
}
}
#[derive(Debug)]
pub(super) struct Element {
components: price::ComponentSet,
restrictions: Vec<Restriction>,
}
impl Element {
fn new(element: &crate::tariff::v221::Element, element_index: usize) -> Self {
let restrictions = if let Some(restrictions) = &element.restrictions {
collect_restrictions(restrictions)
} else {
Vec::new()
};
let mut components = price::ComponentSet {
energy: None,
flat: None,
duration_charging: None,
duration_parking: None,
};
for component in &element.price_components {
let price_component = price::Component::new(component, element_index);
match component.dimension_type {
crate::tariff::v2x::DimensionType::Flat => {
components.flat.get_or_insert(price_component)
}
crate::tariff::v2x::DimensionType::Time => {
components.duration_charging.get_or_insert(price_component)
}
crate::tariff::v2x::DimensionType::ParkingTime => {
components.duration_parking.get_or_insert(price_component)
}
crate::tariff::v2x::DimensionType::Energy => {
components.energy.get_or_insert(price_component)
}
};
}
Self {
components,
restrictions,
}
}
fn is_active(&self, period: &PeriodNormalized) -> bool {
for restriction in &self.restrictions {
if !restriction.snapshot_validity_exclusive(&period.start_snapshot) {
return false;
}
if !restriction.period_validity(&period.consumed) {
return false;
}
}
true
}
#[expect(dead_code, reason = "pending use in linter")]
fn is_active_at_end(&self, period: &PeriodNormalized) -> bool {
for restriction in &self.restrictions {
if !restriction.snapshot_validity_inclusive(&period.end_snapshot) {
return false;
}
}
true
}
}
fn collect_restrictions(restriction: &crate::tariff::v221::Restrictions) -> Vec<Restriction> {
let mut collected = Vec::new();
match (restriction.start_time, restriction.end_time) {
(Some(start_time), Some(end_time)) if end_time < start_time => {
collected.push(Restriction::WrappingTime {
start_time,
end_time,
});
}
(start_time, end_time) => {
if let Some(start_time) = start_time {
collected.push(Restriction::StartTime(start_time));
}
if let Some(end_time) = end_time {
collected.push(Restriction::EndTime(end_time));
}
}
}
if let Some(start_date) = restriction.start_date {
collected.push(Restriction::StartDate(start_date));
}
if let Some(end_date) = restriction.end_date {
collected.push(Restriction::EndDate(end_date));
}
if let Some(min_kwh) = restriction.min_kwh {
collected.push(Restriction::MinKwh(min_kwh));
}
if let Some(max_kwh) = restriction.max_kwh {
collected.push(Restriction::MaxKwh(max_kwh));
}
if let Some(min_current) = restriction.min_current {
collected.push(Restriction::MinCurrent(min_current));
}
if let Some(max_current) = restriction.max_current {
collected.push(Restriction::MaxCurrent(max_current));
}
if let Some(min_power) = restriction.min_power {
collected.push(Restriction::MinPower(min_power));
}
if let Some(max_power) = restriction.max_power {
collected.push(Restriction::MaxPower(max_power));
}
if let Some(min_duration) = restriction.min_duration {
collected.push(Restriction::MinDuration(min_duration));
}
if let Some(max_duration) = restriction.max_duration {
collected.push(Restriction::MaxDuration(max_duration));
}
if let Some(day_of_week) = restriction.day_of_week.as_deref() {
if !day_of_week.is_empty() {
collected.push(Restriction::Weekday(
day_of_week.iter().copied().map(Into::into).collect(),
));
}
}
collected
}
#[derive(Debug, Clone)]
enum Restriction {
StartTime(NaiveTime),
EndTime(NaiveTime),
WrappingTime {
start_time: NaiveTime,
end_time: NaiveTime,
},
StartDate(NaiveDate),
EndDate(NaiveDate),
MinKwh(Kwh),
MaxKwh(Kwh),
MinCurrent(Ampere),
MaxCurrent(Ampere),
MinPower(Kw),
MaxPower(Kw),
MinDuration(Duration),
MaxDuration(Duration),
Weekday(HashSet<Weekday>),
}
impl Restriction {
fn snapshot_validity_exclusive(&self, snapshot: &TotalsSnapshot) -> bool {
match self {
&Self::WrappingTime {
start_time,
end_time,
} => snapshot.local_time() >= start_time || snapshot.local_time() < end_time,
&Self::StartTime(start_time) => snapshot.local_time() >= start_time,
&Self::EndTime(end_time) => snapshot.local_time() < end_time,
&Self::StartDate(start_date) => snapshot.local_date() >= start_date,
&Self::EndDate(end_date) => snapshot.local_date() < end_date,
&Self::MinKwh(min_energy) => snapshot.energy >= min_energy,
&Self::MaxKwh(max_energy) => snapshot.energy < max_energy,
&Self::MinDuration(min_duration) => snapshot.duration_total >= min_duration,
&Self::MaxDuration(max_duration) => snapshot.duration_total < max_duration,
Self::Weekday(days) => days.contains(&snapshot.local_weekday()),
Self::MinCurrent(_) | Self::MaxCurrent(_) | Self::MinPower(_) | Self::MaxPower(_) => {
true
}
}
}
fn snapshot_validity_inclusive(&self, snapshot: &TotalsSnapshot) -> bool {
match self {
&Self::WrappingTime {
start_time,
end_time,
} => snapshot.local_time() >= start_time || snapshot.local_time() < end_time,
&Self::EndTime(end_time) => snapshot.local_time() <= end_time,
&Self::EndDate(end_date) => {
let is_before_end_date = snapshot.local_date() < end_date;
let is_on_end_date = snapshot.local_date() == end_date;
let is_at_midnight = snapshot.local_time().num_seconds_from_midnight() == 0;
is_before_end_date || (is_on_end_date && is_at_midnight)
}
&Self::MinKwh(min_energy) => snapshot.energy >= min_energy,
&Self::MaxKwh(max_energy) => snapshot.energy < max_energy,
&Self::MinDuration(min_duration) => snapshot.duration_total >= min_duration,
&Self::MaxDuration(max_duration) => snapshot.duration_total < max_duration,
Self::Weekday(days) => {
let includes_weekday = days.contains(&snapshot.local_weekday());
let includes_day_before = days.contains(&snapshot.local_weekday().pred());
let is_at_midnight = snapshot.local_time().num_seconds_from_midnight() == 0;
includes_weekday || (includes_day_before && is_at_midnight)
}
_ => true,
}
}
fn period_validity(&self, consumed: &Consumed) -> bool {
match *self {
Self::MinCurrent(min_current) => consumed
.current_min
.map(|current| current >= min_current)
.unwrap_or(true),
Self::MaxCurrent(max_current) => consumed
.current_max
.map(|current| current < max_current)
.unwrap_or(true),
Self::MinPower(min_power) => consumed
.power_min
.map(|power| power >= min_power)
.unwrap_or(true),
Self::MaxPower(max_power) => consumed
.power_max
.map(|power| power < max_power)
.unwrap_or(true),
_ => true,
}
}
}
pub(super) fn find_first_active(
tariffs: Vec<Tariff>,
start_date_time: DateTime<Utc>,
) -> Option<(usize, Tariff)> {
tariffs
.into_iter()
.enumerate()
.find(|(_, t)| t.is_active(start_date_time))
}
pub(super) fn normalize_all(tariffs: &[crate::tariff::v221::Tariff<'_>]) -> Vec<Tariff> {
tariffs.iter().map(Tariff::from_v221).collect()
}