use std::fmt;
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
#[serde(rename_all = "UPPERCASE")]
pub enum Kategorie {
A,
B,
C,
}
impl Kategorie {
#[must_use]
pub fn verantwortlich(self) -> Rolle {
match self {
Self::A => Rolle::Nb,
Self::B | Self::C => Rolle::Uenb,
}
}
#[must_use]
pub fn bezugszeitraum(self) -> Bezugszeitraum {
match self {
Self::A | Self::B => Bezugszeitraum::Monat,
Self::C => Bezugszeitraum::Tag,
}
}
}
impl fmt::Display for Kategorie {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::A => "A",
Self::B => "B",
Self::C => "C",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Bezugszeitraum {
Monat,
Tag,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
#[serde(rename_all = "UPPERCASE")]
pub enum Rolle {
Biko,
Bkv,
Lf,
Nb,
Uenb,
}
impl fmt::Display for Rolle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Biko => "BIKO",
Self::Bkv => "BKV",
Self::Lf => "LF",
Self::Nb => "NB",
Self::Uenb => "ÜNB",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Familie {
BgSzr,
BkSzr,
LfSzr,
Dzue,
Nzr,
Abrechnungssummenzeitreihe,
Aauez,
TaeglicheAauez,
LfAaszr,
}
impl Familie {
#[must_use]
pub fn hat_kategorie(self) -> bool {
matches!(self, Self::BgSzr | Self::BkSzr | Self::LfSzr)
}
#[must_use]
pub fn kategorien(self) -> &'static [Kategorie] {
match self {
Self::BgSzr => &[Kategorie::B, Kategorie::C],
Self::BkSzr => &[Kategorie::A, Kategorie::B, Kategorie::C],
Self::LfSzr => &[Kategorie::A, Kategorie::B],
_ => &[],
}
}
#[must_use]
pub fn kuerzel(self) -> &'static str {
match self {
Self::BgSzr => "BG-SZR",
Self::BkSzr => "BK-SZR",
Self::LfSzr => "LF-SZR",
Self::Dzue => "DZÜ",
Self::Nzr => "NZR",
Self::Abrechnungssummenzeitreihe => "Abrechnungssummenzeitreihe",
Self::Aauez => "AAÜZ",
Self::TaeglicheAauez => "tägliche AAÜZ",
Self::LfAaszr => "LF-AASZR",
}
}
#[must_use]
pub fn stammt_aus_kapitel_17(self) -> bool {
matches!(self, Self::Aauez | Self::TaeglicheAauez | Self::LfAaszr)
}
#[must_use]
pub fn endet_am(self) -> Option<time::Date> {
match self {
Self::TaeglicheAauez => Some(KAPITEL_17_2_ENDE),
_ => None,
}
}
}
pub const KAPITEL_17_2_ENDE: time::Date = time::macros::date!(2026 - 09 - 30);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct Zeitreihe {
familie: Familie,
kategorie: Option<Kategorie>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("{familie} hat keine Kategorie {kategorie:?} (BK6-24-174 Anlage 3 Tabelle 1)")]
pub struct UnbekannteKategorie {
pub familie: &'static str,
pub kategorie: Option<Kategorie>,
}
impl Zeitreihe {
#[must_use]
pub fn pruef_ebd(self) -> Option<&'static str> {
use mako_pruefung::mabis::codes as ebd;
match (self.familie, self.kategorie) {
(Familie::LfSzr, Some(Kategorie::A)) => Some(ebd::EBD_LF_SZR_A),
(Familie::LfSzr, Some(Kategorie::B)) => Some(ebd::EBD_LF_SZR_B),
(Familie::LfAaszr, _) => Some(ebd::EBD_LF_AASZR),
(Familie::Nzr, _) => Some(ebd::EBD_NZR),
(Familie::BgSzr, Some(Kategorie::B)) => Some(ebd::EBD_BG_SZR_B),
(Familie::BkSzr, Some(Kategorie::A)) => Some(ebd::EBD_BK_SZR_A),
(Familie::BkSzr, Some(Kategorie::B)) => Some(ebd::EBD_BK_SZR_B),
(Familie::Dzue, _) => Some(ebd::EBD_DZUE),
_ => None,
}
}
pub fn new(
familie: Familie,
kategorie: Option<Kategorie>,
) -> Result<Self, UnbekannteKategorie> {
let ok = match (familie.hat_kategorie(), kategorie) {
(true, Some(k)) => familie.kategorien().contains(&k),
(false, None) => true,
_ => false,
};
if ok {
Ok(Self { familie, kategorie })
} else {
Err(UnbekannteKategorie {
familie: familie.kuerzel(),
kategorie,
})
}
}
#[must_use]
pub fn familie(self) -> Familie {
self.familie
}
#[must_use]
pub fn kategorie(self) -> Option<Kategorie> {
self.kategorie
}
#[must_use]
pub fn verantwortlich(self) -> Rolle {
match (self.familie, self.kategorie) {
(_, Some(k)) => k.verantwortlich(),
(Familie::Dzue, _) => Rolle::Uenb,
(Familie::Nzr, _) => Rolle::Nb,
(Familie::Abrechnungssummenzeitreihe, _) => Rolle::Biko,
(Familie::Aauez | Familie::TaeglicheAauez | Familie::LfAaszr, _) => Rolle::Nb,
(Familie::BgSzr | Familie::BkSzr | Familie::LfSzr, None) => Rolle::Uenb,
}
}
#[must_use]
pub fn empfaenger(self) -> &'static [Rolle] {
use Familie as F;
use Kategorie as K;
match (self.familie, self.kategorie) {
(F::BgSzr, Some(K::B)) => &[Rolle::Nb, Rolle::Biko],
(F::BgSzr, _) => &[Rolle::Nb],
(F::BkSzr, Some(K::A | K::B)) => &[Rolle::Bkv, Rolle::Biko],
(F::BkSzr, _) => &[Rolle::Bkv],
(F::LfSzr, _) => &[Rolle::Lf],
(F::Dzue, _) => &[Rolle::Nb, Rolle::Biko],
(F::Nzr, _) => &[Rolle::Nb, Rolle::Biko],
(F::Abrechnungssummenzeitreihe, _) => &[Rolle::Uenb, Rolle::Bkv, Rolle::Nb],
(F::Aauez, _) => &[Rolle::Biko, Rolle::Bkv],
(F::TaeglicheAauez, _) => &[Rolle::Uenb],
(F::LfAaszr, _) => &[Rolle::Lf],
}
}
#[must_use]
pub fn bezugszeitraum(self) -> Bezugszeitraum {
self.kategorie
.map_or(Bezugszeitraum::Monat, Kategorie::bezugszeitraum)
}
#[must_use]
pub fn abrechnungsrelevant(self) -> bool {
use Familie as F;
match self.familie {
F::LfSzr => false,
F::BgSzr | F::BkSzr => self.kategorie != Some(Kategorie::C),
F::Dzue | F::Nzr | F::Abrechnungssummenzeitreihe | F::Aauez => true,
F::TaeglicheAauez | F::LfAaszr => false,
}
}
#[must_use]
pub fn hat_pruefmitteilung_und_datenstatus(self) -> bool {
!matches!(
(self.familie, self.kategorie),
(Familie::BgSzr | Familie::BkSzr, Some(Kategorie::C))
) && self.familie != Familie::TaeglicheAauez
}
#[must_use]
pub fn endet_am(self) -> Option<time::Date> {
self.familie.endet_am()
}
#[must_use]
pub fn gilt_am(self, date: time::Date) -> bool {
self.endet_am().is_none_or(|ende| date <= ende)
}
#[must_use]
pub fn aggregation(self) -> &'static [&'static str] {
use Familie as F;
use Kategorie as K;
match (self.familie, self.kategorie) {
(F::BgSzr, _) => &["BG", "Spannungsebene", "ZRT"],
(F::BkSzr, Some(K::A)) => &["BG", "BK", "ZRT"],
(F::BkSzr, Some(K::B)) => &["BG/RZ", "BK", "ZRT"],
(F::BkSzr, _) => &["RZ", "BK", "ZRT"],
(F::LfSzr, Some(K::A)) => &["BG", "BK", "LF", "ZRT"],
(F::LfSzr, _) => &["BG/RZ", "BK", "LF", "ZRT"],
(F::Dzue, _) => &["BG"],
(F::Aauez, _) => &["BK"],
(F::TaeglicheAauez, _) => &["BG"],
(F::LfAaszr, _) => &["BG", "BK", "LF"],
(F::Nzr | F::Abrechnungssummenzeitreihe, _) => &[],
}
}
#[must_use]
pub fn hat_waehlbare_aggregationsebene(self) -> bool {
self.familie == Familie::BkSzr && self.kategorie == Some(Kategorie::B)
}
}
impl fmt::Display for Zeitreihe {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.kategorie {
Some(k) => write!(f, "{} (Kategorie {k})", self.familie.kuerzel()),
None => f.write_str(self.familie.kuerzel()),
}
}
}
#[must_use]
pub fn alle() -> Vec<Zeitreihe> {
let mut out = Vec::new();
for familie in [
Familie::BgSzr,
Familie::BkSzr,
Familie::LfSzr,
Familie::Dzue,
Familie::Nzr,
Familie::Abrechnungssummenzeitreihe,
Familie::Aauez,
Familie::TaeglicheAauez,
Familie::LfAaszr,
] {
if familie.hat_kategorie() {
for &k in familie.kategorien() {
out.push(Zeitreihe::new(familie, Some(k)).expect("row from Familie::kategorien"));
}
} else {
out.push(Zeitreihe::new(familie, None).expect("row from Familie::kategorien"));
}
}
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Aggregationsebene {
Bilanzierungsgebiet,
Regelzone,
}
#[must_use]
pub fn zeitreihe_aus_cav(code: &str) -> Option<(Zeitreihe, Option<Aggregationsebene>)> {
use Aggregationsebene as E;
use Familie as F;
use Kategorie as K;
let (familie, kategorie, ebene) = match code {
"Z95" => (F::BgSzr, Some(K::B), None),
"Z96" => (F::BgSzr, Some(K::C), None),
"Z97" => (F::BkSzr, Some(K::A), None),
"Z98" => (F::BkSzr, Some(K::B), Some(E::Regelzone)),
"Z99" => (F::BkSzr, Some(K::B), Some(E::Bilanzierungsgebiet)),
"ZA0" => (F::BkSzr, Some(K::C), None),
"ZA1" => (F::LfSzr, Some(K::A), None),
"ZA2" => (F::LfSzr, Some(K::B), Some(E::Regelzone)),
"ZA3" => (F::LfSzr, Some(K::B), Some(E::Bilanzierungsgebiet)),
"ZA4" => (F::Dzue, None, None),
"ZA5" => (F::Nzr, None, None),
"ZA6" => (F::Abrechnungssummenzeitreihe, None, None),
_ => return None,
};
Zeitreihe::new(familie, kategorie).ok().map(|z| (z, ebene))
}
#[must_use]
pub fn cav_aus_zeitreihe(
zeitreihe: Zeitreihe,
ebene: Option<Aggregationsebene>,
) -> Option<&'static str> {
use Aggregationsebene as E;
use Familie as F;
use Kategorie as K;
Some(match (zeitreihe.familie(), zeitreihe.kategorie(), ebene) {
(F::BgSzr, Some(K::B), _) => "Z95",
(F::BgSzr, Some(K::C), _) => "Z96",
(F::BkSzr, Some(K::A), _) => "Z97",
(F::BkSzr, Some(K::B), Some(E::Regelzone)) => "Z98",
(F::BkSzr, Some(K::B), Some(E::Bilanzierungsgebiet)) => "Z99",
(F::BkSzr, Some(K::C), _) => "ZA0",
(F::LfSzr, Some(K::A), _) => "ZA1",
(F::LfSzr, Some(K::B), Some(E::Regelzone)) => "ZA2",
(F::LfSzr, Some(K::B), Some(E::Bilanzierungsgebiet)) => "ZA3",
(F::Dzue, _, _) => "ZA4",
(F::Nzr, _, _) => "ZA5",
(F::Abrechnungssummenzeitreihe, _, _) => "ZA6",
_ => return None,
})
}
pub const CCI_BEZEICHNUNG_SUMMENZEITREIHE: &str = "ZB4";
pub const CCI_KLASSENTYP_VERANTWORTLICHER: &str = "6";
#[must_use]
pub fn rolle_aus_cci(code: &str) -> Option<Rolle> {
Some(match code {
"ZA8" => Rolle::Nb,
"ZA9" => Rolle::Uenb,
"ZB7" => Rolle::Biko,
_ => return None,
})
}
#[must_use]
pub fn cci_aus_rolle(rolle: Rolle) -> Option<&'static str> {
Some(match rolle {
Rolle::Nb => "ZA8",
Rolle::Uenb => "ZA9",
Rolle::Biko => "ZB7",
Rolle::Bkv | Rolle::Lf => return None,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Messtechnik {
NurIms,
NurMme,
NurKme,
Gemischt,
Pauschal,
}
#[must_use]
pub fn aggregationsverantwortung(
messtechnik: Messtechnik,
viertelstunden_bilanziert: bool,
an_uenb_uebertragen: bool,
) -> Rolle {
if messtechnik == Messtechnik::NurIms && viertelstunden_bilanziert && an_uenb_uebertragen {
Rolle::Uenb
} else {
Rolle::Nb
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_catalogue_has_thirteen_rows() {
assert_eq!(alle().len(), 13);
assert_eq!(
alle()
.iter()
.filter(|z| z.familie().stammt_aus_kapitel_17())
.count(),
3
);
}
#[test]
fn only_the_taegliche_aauez_expires() {
for z in alle() {
let expected = z.familie() == Familie::TaeglicheAauez;
assert_eq!(z.endet_am().is_some(), expected, "{z}");
}
assert!(
Zeitreihe::new(Familie::TaeglicheAauez, None)
.unwrap()
.gilt_am(KAPITEL_17_2_ENDE)
);
assert!(
!Zeitreihe::new(Familie::TaeglicheAauez, None)
.unwrap()
.gilt_am(KAPITEL_17_2_ENDE.next_day().unwrap())
);
assert!(
Zeitreihe::new(Familie::Aauez, None)
.unwrap()
.gilt_am(KAPITEL_17_2_ENDE.next_day().unwrap())
);
}
#[test]
fn the_taegliche_aauez_carries_neither_pruefmitteilung_nor_datenstatus() {
assert!(
!Zeitreihe::new(Familie::TaeglicheAauez, None)
.unwrap()
.hat_pruefmitteilung_und_datenstatus()
);
assert!(
Zeitreihe::new(Familie::Aauez, None)
.unwrap()
.hat_pruefmitteilung_und_datenstatus()
);
}
#[test]
fn bg_szr_has_no_kategorie_a() {
assert!(Zeitreihe::new(Familie::BgSzr, Some(Kategorie::A)).is_err());
assert!(Zeitreihe::new(Familie::BgSzr, Some(Kategorie::B)).is_ok());
}
#[test]
fn lf_szr_has_no_kategorie_c() {
assert!(Zeitreihe::new(Familie::LfSzr, Some(Kategorie::C)).is_err());
}
#[test]
fn kategorielose_familien_refuse_a_kategorie() {
assert!(Zeitreihe::new(Familie::Nzr, Some(Kategorie::A)).is_err());
assert!(Zeitreihe::new(Familie::Nzr, None).is_ok());
}
#[test]
fn kategorie_szr_requires_a_kategorie() {
assert!(Zeitreihe::new(Familie::BkSzr, None).is_err());
}
#[test]
fn kategorie_c_is_daily_and_not_settlement_relevant() {
for familie in [Familie::BgSzr, Familie::BkSzr] {
let z = Zeitreihe::new(familie, Some(Kategorie::C)).unwrap();
assert_eq!(z.bezugszeitraum(), Bezugszeitraum::Tag);
assert!(!z.abrechnungsrelevant());
assert!(!z.hat_pruefmitteilung_und_datenstatus());
}
}
#[test]
fn lf_szr_is_never_settlement_relevant() {
for &k in Familie::LfSzr.kategorien() {
assert!(
!Zeitreihe::new(Familie::LfSzr, Some(k))
.unwrap()
.abrechnungsrelevant()
);
}
}
#[test]
fn verantwortlich_follows_the_kategorie() {
let a = Zeitreihe::new(Familie::BkSzr, Some(Kategorie::A)).unwrap();
let b = Zeitreihe::new(Familie::BkSzr, Some(Kategorie::B)).unwrap();
assert_eq!(a.verantwortlich(), Rolle::Nb);
assert_eq!(b.verantwortlich(), Rolle::Uenb);
}
#[test]
fn only_bk_szr_kategorie_b_has_a_choosable_aggregationsebene() {
for z in alle() {
let expected = z.familie() == Familie::BkSzr && z.kategorie() == Some(Kategorie::B);
assert_eq!(z.hat_waehlbare_aggregationsebene(), expected, "{z}");
}
}
#[test]
fn every_tabelle_1_row_round_trips_through_its_cav_code() {
for z in alle() {
if z.familie().stammt_aus_kapitel_17() {
continue;
}
let braucht_ebene = z.hat_waehlbare_aggregationsebene()
|| (z.familie() == Familie::LfSzr && z.kategorie() == Some(Kategorie::B));
let ebenen: &[Option<Aggregationsebene>] = if braucht_ebene {
&[
Some(Aggregationsebene::Bilanzierungsgebiet),
Some(Aggregationsebene::Regelzone),
]
} else {
&[None]
};
for &e in ebenen {
let code = cav_aus_zeitreihe(z, e).unwrap_or_else(|| panic!("{z} / {e:?}"));
assert_eq!(zeitreihe_aus_cav(code), Some((z, e)), "{code}");
}
}
}
#[test]
fn the_kategorie_b_levels_are_distinct_codes() {
let bk_b = Zeitreihe::new(Familie::BkSzr, Some(Kategorie::B)).unwrap();
assert_eq!(
cav_aus_zeitreihe(bk_b, Some(Aggregationsebene::Regelzone)),
Some("Z98")
);
assert_eq!(
cav_aus_zeitreihe(bk_b, Some(Aggregationsebene::Bilanzierungsgebiet)),
Some("Z99")
);
assert_eq!(cav_aus_zeitreihe(bk_b, None), None);
}
#[test]
fn the_emob_bk_szr_is_not_a_tabelle_1_row() {
assert_eq!(zeitreihe_aus_cav("ZG7"), None);
assert_eq!(zeitreihe_aus_cav("ZZZ"), None);
}
#[test]
fn the_verantwortlicher_codes_round_trip() {
for rolle in [Rolle::Nb, Rolle::Uenb, Rolle::Biko] {
let code = cci_aus_rolle(rolle).expect("has a code");
assert_eq!(rolle_aus_cci(code), Some(rolle));
}
assert_eq!(cci_aus_rolle(Rolle::Bkv), None);
assert_eq!(cci_aus_rolle(Rolle::Lf), None);
}
#[test]
fn uenb_aggregation_needs_all_three_conditions() {
assert_eq!(
aggregationsverantwortung(Messtechnik::NurIms, true, true),
Rolle::Uenb
);
assert_eq!(
aggregationsverantwortung(Messtechnik::NurIms, true, false),
Rolle::Nb
);
assert_eq!(
aggregationsverantwortung(Messtechnik::NurIms, false, true),
Rolle::Nb
);
for mt in [
Messtechnik::NurMme,
Messtechnik::NurKme,
Messtechnik::Gemischt,
Messtechnik::Pauschal,
] {
assert_eq!(aggregationsverantwortung(mt, true, true), Rolle::Nb);
}
}
}