#[cfg(all(feature = "versioned", feature = "decimal"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "versioned", feature = "decimal"))))]
pub trait BetragExt {
fn wert_decimal(&self) -> Option<rust_decimal::Decimal>;
}
#[cfg(all(feature = "versioned", feature = "decimal"))]
impl BetragExt for Option<crate::generated::v202607::Betrag> {
fn wert_decimal(&self) -> Option<rust_decimal::Decimal> {
self.as_ref().and_then(|b| b.wert)
}
}
#[cfg(all(feature = "versioned", feature = "decimal"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "versioned", feature = "decimal"))))]
pub trait MengeExt {
fn wert_decimal(&self) -> Option<rust_decimal::Decimal>;
}
#[cfg(all(feature = "versioned", feature = "decimal"))]
impl MengeExt for Option<crate::generated::v202607::Menge> {
fn wert_decimal(&self) -> Option<rust_decimal::Decimal> {
self.as_ref().and_then(|m| m.wert)
}
}
#[cfg(all(feature = "versioned", feature = "decimal"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "versioned", feature = "decimal"))))]
pub trait PreisExt {
fn wert_decimal(&self) -> Option<rust_decimal::Decimal>;
}
#[cfg(all(feature = "versioned", feature = "decimal"))]
impl PreisExt for Option<crate::generated::v202607::Preis> {
fn wert_decimal(&self) -> Option<rust_decimal::Decimal> {
self.as_ref().and_then(|p| p.wert)
}
}
#[cfg(all(feature = "versioned", feature = "time"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "versioned", feature = "time"))))]
pub type OffsetTimeResult =
Result<(time::Time, Option<time::UtcOffset>), crate::offset_time::OffsetTimeError>;
#[cfg(all(feature = "versioned", feature = "time"))]
mod zeitraum_impl {
use super::{OffsetTimeResult, ZeitpunktError};
use crate::generated::v202607::Zeitraum;
use std::ops::{Range, RangeInclusive};
use time::{Date, OffsetDateTime};
impl Zeitraum {
#[must_use]
pub fn as_inclusive_range(&self) -> Option<RangeInclusive<Date>> {
Some(self.startdatum?..=self.enddatum?)
}
#[must_use]
pub fn bounds(&self) -> (Option<Date>, Option<Date>) {
(self.startdatum, self.enddatum)
}
#[must_use]
pub fn contains(&self, date: Date) -> bool {
let start_ok = self.startdatum.is_none_or(|d| date >= d);
let end_ok = self.enddatum.is_none_or(|d| date <= d);
start_ok && end_ok
}
#[must_use]
pub fn whole_days(&self) -> Option<i64> {
let (start, end) = (self.startdatum?, self.enddatum?);
Some(((end - start).whole_days() + 1).max(0))
}
#[must_use]
pub fn duration(
&self,
) -> Option<Result<time::Duration, crate::iso8601_duration::Iso8601DurationError>> {
Some(crate::iso8601_duration::parse(self.dauer.as_deref()?))
}
#[must_use]
pub fn startuhrzeit_parsed(&self) -> Option<OffsetTimeResult> {
Some(crate::offset_time::parse(self.startuhrzeit.as_deref()?))
}
#[must_use]
pub fn enduhrzeit_parsed(&self) -> Option<OffsetTimeResult> {
Some(crate::offset_time::parse(self.enduhrzeit.as_deref()?))
}
#[must_use]
pub fn start_instant(&self) -> Option<Result<OffsetDateTime, ZeitpunktError>> {
Some(combine(self.startdatum?, self.startuhrzeit.as_deref()?))
}
#[must_use]
pub fn end_instant(&self) -> Option<Result<OffsetDateTime, ZeitpunktError>> {
Some(combine(self.enddatum?, self.enduhrzeit.as_deref()?))
}
#[must_use]
pub fn as_instant_range(&self) -> Option<Result<Range<OffsetDateTime>, ZeitpunktError>> {
let start = self.start_instant()?;
let end = self.end_instant()?;
Some(match (start, end) {
(Err(e), _) | (Ok(_), Err(e)) => Err(e),
(Ok(s), Ok(e)) => Ok(s..e),
})
}
#[must_use]
pub fn is_instant_range(&self) -> bool {
self.startdatum.is_some()
&& self.startuhrzeit.is_some()
&& self.enddatum.is_some()
&& self.enduhrzeit.is_some()
}
#[must_use]
pub fn instant_duration(&self) -> Option<Result<time::Duration, ZeitpunktError>> {
Some(self.as_instant_range()?.map(|r| r.end - r.start))
}
#[must_use]
pub fn contains_instant(&self, at: OffsetDateTime) -> bool {
let after_start = match self.start_instant() {
Some(Ok(s)) => at >= s,
Some(Err(_)) => return false,
None => true,
};
let before_end = match self.end_instant() {
Some(Ok(e)) => at < e,
Some(Err(_)) => return false,
None => true,
};
after_start && before_end
}
#[must_use]
pub fn from_instants(start: OffsetDateTime, end: OffsetDateTime) -> Zeitraum {
Zeitraum {
startdatum: Some(start.date()),
startuhrzeit: Some(crate::offset_time::format(
start.time(),
Some(start.offset()),
)),
enddatum: Some(end.date()),
enduhrzeit: Some(crate::offset_time::format(end.time(), Some(end.offset()))),
..Default::default()
}
}
}
fn combine(date: Date, uhrzeit: &str) -> Result<OffsetDateTime, ZeitpunktError> {
let (time, offset) = crate::offset_time::parse(uhrzeit).map_err(ZeitpunktError::Time)?;
let offset = offset.ok_or(ZeitpunktError::MissingOffset)?;
Ok(date.with_time(time).assume_offset(offset))
}
}
#[cfg(all(feature = "versioned", feature = "time"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "versioned", feature = "time"))))]
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum ZeitpunktError {
#[error("{0}")]
Time(#[from] crate::offset_time::OffsetTimeError),
#[error("time of day carries no UTC offset, so it does not name an instant")]
MissingOffset,
}
#[cfg(feature = "versioned")]
mod messwertstatus_impl {
use crate::generated::v202607::Messwertstatus;
impl Messwertstatus {
#[must_use]
pub const fn is_usable(self) -> bool {
!matches!(
self,
Messwertstatus::Fehlt | Messwertstatus::NichtVerwendbar | Messwertstatus::Unknown
)
}
#[must_use]
pub const fn is_measured(self) -> bool {
matches!(self, Messwertstatus::Abgelesen)
}
#[must_use]
pub const fn is_substitute(self) -> bool {
matches!(
self,
Messwertstatus::Ersatzwert
| Messwertstatus::Vorschlagswert
| Messwertstatus::Prognosewert
| Messwertstatus::Vorlaeufigerwert
| Messwertstatus::Energiemengesummiert
| Messwertstatus::AngabeFuerLieferschein
)
}
}
#[cfg(test)]
mod tests {
use super::Messwertstatus;
#[test]
fn the_predicates_partition_every_variant() {
for status in Messwertstatus::VARIANTS {
let flags = [
status.is_measured(),
status.is_substitute(),
!status.is_usable(),
];
assert_eq!(
flags.iter().filter(|f| **f).count(),
1,
"{} is in {} of the three classes",
status.as_wire(),
flags.iter().filter(|f| **f).count(),
);
}
}
#[test]
fn the_catch_all_is_not_treated_as_a_clean_reading() {
assert!(!Messwertstatus::Unknown.is_usable());
assert!(!Messwertstatus::Unknown.is_measured());
assert!(!Messwertstatus::Unknown.is_substitute());
}
}
}
#[cfg(all(feature = "versioned", feature = "time"))]
mod umschaltzeit_impl {
use super::OffsetTimeResult;
use crate::generated::v202607::Umschaltzeit;
impl Umschaltzeit {
#[must_use]
pub fn umschaltzeit_parsed(&self) -> Option<OffsetTimeResult> {
Some(crate::offset_time::parse(self.umschaltzeit.as_deref()?))
}
}
}
#[cfg(all(feature = "versioned", feature = "time"))]
mod rechnung_impl {
use crate::generated::v202607::Rechnung;
use time::Date;
impl Rechnung {
#[must_use]
pub fn billing_period(&self) -> Option<std::ops::RangeInclusive<Date>> {
self.rechnungsperiode.as_ref()?.as_inclusive_range()
}
#[must_use]
pub fn period_start(&self) -> Option<Date> {
self.rechnungsperiode.as_ref()?.startdatum
}
#[must_use]
pub fn period_end(&self) -> Option<Date> {
self.rechnungsperiode.as_ref()?.enddatum
}
#[must_use]
pub fn rechnungsdatum_date(&self) -> Option<Date> {
self.rechnungsdatum.map(|dt| dt.date())
}
#[must_use]
pub fn faelligkeitsdatum_date(&self) -> Option<Date> {
self.faelligkeitsdatum.map(|dt| dt.date())
}
}
}
#[cfg(all(feature = "versioned", feature = "decimal"))]
mod rechnung_decimal_impl {
use crate::generated::v202607::Rechnung;
impl Rechnung {
#[must_use]
pub fn gesamtnetto_decimal(&self) -> Option<rust_decimal::Decimal> {
self.gesamtnetto.as_ref()?.wert
}
#[must_use]
pub fn gesamtbrutto_decimal(&self) -> Option<rust_decimal::Decimal> {
self.gesamtbrutto.as_ref()?.wert
}
#[must_use]
pub fn gesamtsteuer_decimal(&self) -> Option<rust_decimal::Decimal> {
self.gesamtsteuer.as_ref()?.wert
}
#[must_use]
pub fn zu_zahlen_decimal(&self) -> Option<rust_decimal::Decimal> {
self.zu_zahlen.as_ref()?.wert
}
#[must_use]
pub fn rabatt_netto_decimal(&self) -> Option<rust_decimal::Decimal> {
self.rabatt_netto.as_ref()?.wert
}
#[must_use]
pub fn zukuenftiger_abschlag_decimal(&self) -> Option<rust_decimal::Decimal> {
self.zukuenftiger_abschlag.as_ref()?.wert
}
#[must_use]
pub fn vorauszahlungen_summe(&self) -> Option<rust_decimal::Decimal> {
let payments = self.vorauszahlungen.as_deref()?;
if payments.is_empty() {
return None;
}
payments
.iter()
.filter_map(|p| p.betrag.as_ref().and_then(|b| b.wert))
.try_fold(rust_decimal::Decimal::ZERO, |acc, v| acc.checked_add(v))
}
}
}
#[cfg(feature = "versioned")]
mod rechnung_versioned_impl {
use crate::generated::v202607::{Rechnung, Rechnungsposition};
impl Rechnung {
pub fn positions(&self) -> impl Iterator<Item = &Rechnungsposition> {
self.rechnungspositionen.as_deref().into_iter().flatten()
}
#[must_use]
pub fn is_storno(&self) -> bool {
self.ist_storno.unwrap_or(false)
}
#[must_use]
pub fn is_original(&self) -> bool {
self.ist_original.unwrap_or(false)
}
}
}
#[cfg(all(feature = "versioned", feature = "time"))]
mod rechnungsposition_time_impl {
use crate::generated::v202607::Rechnungsposition;
use time::Date;
impl Rechnungsposition {
#[must_use]
pub fn lieferung_von_date(&self) -> Option<Date> {
self.lieferungszeitraum.as_ref()?.startdatum
}
#[must_use]
pub fn lieferung_bis_date(&self) -> Option<Date> {
self.lieferungszeitraum.as_ref()?.enddatum
}
#[must_use]
pub fn lieferungszeitraum_contains(&self, date: Date) -> bool {
self.lieferungszeitraum
.as_ref()
.is_some_and(|z| z.contains(date))
}
}
}
#[cfg(feature = "versioned")]
mod zahlungsinformation_impl {
use crate::error::IdentifierError;
use crate::generated::v202607::Zahlungsinformation;
use crate::identifiers::{Bic, Iban};
impl Zahlungsinformation {
#[must_use]
pub fn iban_checked(&self) -> Option<Result<Iban, IdentifierError>> {
Some(Iban::new(self.iban.as_deref()?))
}
#[must_use]
pub fn bic_checked(&self) -> Option<Result<Bic, IdentifierError>> {
Some(Bic::new(self.bic.as_deref()?))
}
}
}
#[cfg(feature = "versioned")]
pub use marktregeln_impl::Aggregationszustaendigkeit;
#[cfg(feature = "versioned")]
mod marktregeln_impl {
use crate::error::IdentifierError;
use crate::generated::v202607::{
Abwicklungsmodell, Aggregationsverantwortung, Bilanzierung, EMobilitaetsart, Marktlokation,
TechnischeRessource, TechnischeRessourceVerbrauchsart,
};
use crate::identifiers::BilanzierungsgebietId;
impl Marktlokation {
#[must_use]
pub fn bilanzierungsgebiet_checked(
&self,
) -> Option<Result<BilanzierungsgebietId, IdentifierError>> {
Some(BilanzierungsgebietId::new(
self.bilanzierungsgebiet.as_deref()?,
))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum Aggregationszustaendigkeit {
Uebertragungsnetzbetreiber,
Verteilnetzbetreiber,
Ruhend,
Unbekannt,
}
impl Bilanzierung {
#[must_use]
pub fn is_modell_2(&self) -> bool {
self.abwicklungsmodell == Some(Abwicklungsmodell::Modell2)
}
#[must_use]
pub fn aggregationszustaendigkeit(&self) -> Aggregationszustaendigkeit {
match self.aggregationsverantwortung {
Some(Aggregationsverantwortung::Uenb) => {
Aggregationszustaendigkeit::Uebertragungsnetzbetreiber
}
Some(Aggregationsverantwortung::Vnb) => {
Aggregationszustaendigkeit::Verteilnetzbetreiber
}
Some(Aggregationsverantwortung::Unknown) => Aggregationszustaendigkeit::Unbekannt,
None if self.is_modell_2() => Aggregationszustaendigkeit::Ruhend,
None => Aggregationszustaendigkeit::Unbekannt,
}
}
#[must_use]
pub fn aggregation_ruht(&self) -> bool {
self.aggregationszustaendigkeit() == Aggregationszustaendigkeit::Ruhend
}
}
impl TechnischeRessource {
#[must_use]
pub fn is_emobilitaet(&self) -> bool {
self.emobilitaetsart
.is_some_and(|a| crate::Bo4eEnum::is_known(&a))
|| self.technische_ressource_verbrauchsart
== Some(TechnischeRessourceVerbrauchsart::EMobilitaet)
}
#[must_use]
pub fn is_emobilitaetsladesaeule(&self) -> bool {
self.emobilitaetsart == Some(EMobilitaetsart::EMobilitaetsladesaeule)
}
}
}
#[cfg(all(feature = "versioned", feature = "decimal"))]
pub use preisstaffel_impl::PreisstaffelSliceExt;
#[cfg(all(feature = "versioned", feature = "decimal"))]
mod preisstaffel_impl {
use crate::generated::v202607::Preisstaffel;
use rust_decimal::Decimal;
impl Preisstaffel {
#[must_use]
pub fn contains(&self, value: Decimal) -> bool {
self.staffelgrenze_von.is_none_or(|von| value >= von)
&& self.staffelgrenze_bis.is_none_or(|bis| value <= bis)
}
}
#[cfg_attr(docsrs, doc(cfg(all(feature = "versioned", feature = "decimal"))))]
pub trait PreisstaffelSliceExt {
fn select_for(&self, value: Decimal) -> Option<&Preisstaffel>;
}
impl PreisstaffelSliceExt for [Preisstaffel] {
fn select_for(&self, value: Decimal) -> Option<&Preisstaffel> {
let below_floor = self
.iter()
.all(|s| s.staffelgrenze_von.is_some_and(|von| value < von));
if self.is_empty() || below_floor {
return None;
}
self.iter()
.filter(|s| s.staffelgrenze_bis.is_none_or(|bis| value <= bis))
.min_by(|a, b| match (a.staffelgrenze_bis, b.staffelgrenze_bis) {
(Some(x), Some(y)) => x.cmp(&y),
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => std::cmp::Ordering::Equal,
})
}
}
}
#[cfg(all(feature = "versioned", feature = "decimal"))]
mod rechnungsposition_decimal_impl {
use crate::generated::v202607::Rechnungsposition;
impl Rechnungsposition {
#[must_use]
pub fn gesamtpreis_decimal(&self) -> Option<rust_decimal::Decimal> {
self.gesamtpreis.as_ref()?.wert
}
#[must_use]
pub fn einzelpreis_decimal(&self) -> Option<rust_decimal::Decimal> {
self.einzelpreis.as_ref()?.wert
}
#[must_use]
pub fn positions_menge_decimal(&self) -> Option<rust_decimal::Decimal> {
self.positions_menge.as_ref()?.wert
}
#[must_use]
pub fn zeitbezogene_menge_decimal(&self) -> Option<rust_decimal::Decimal> {
self.zeitbezogene_menge.as_ref()?.wert
}
}
}
#[cfg(all(feature = "versioned", feature = "time"))]
mod preisblatt_netznutzung_impl {
use crate::generated::v202607::{PreisblattNetznutzung, Zeitraum};
use time::Date;
impl PreisblattNetznutzung {
#[must_use]
pub fn validity(&self) -> (Option<Date>, Option<Date>) {
self.gueltigkeit
.as_ref()
.map_or((None, None), Zeitraum::bounds)
}
#[must_use]
pub fn is_valid_at(&self, date: Date) -> bool {
self.gueltigkeit.as_ref().is_some_and(|z| z.contains(date))
}
}
}