mod codelist;
#[cfg(feature = "versioned")]
mod bo4e;
pub use codelist::{OBJEKTROLLEN, STRUKTUREN};
#[cfg(feature = "versioned")]
#[cfg_attr(docsrs, doc(cfg(feature = "versioned")))]
pub use bo4e::{
Befund, Buendelaudit, Lokationsbuendel, LokationsbuendelExt, LokationsbuendelObjekt,
};
use crate::identifiers::{LokationsbuendelObjektcode, Lokationsbuendelcode};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Objekttyp {
Marktlokation,
Messlokation,
Netzlokation,
TechnischeRessource,
}
impl Objekttyp {
#[must_use]
pub const fn abbreviation(self) -> &'static str {
match self {
Self::Marktlokation => "MaLo",
Self::Messlokation => "MeLo",
Self::Netzlokation => "NeLo",
Self::TechnischeRessource => "TR",
}
}
}
impl std::fmt::Display for Objekttyp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.abbreviation())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Flussrichtung {
Verbrauch,
Erzeugung,
VerbrauchUndErzeugung,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum Objektfunktion {
Netzuebergabe,
Hinterschaltung,
Differenzmessung,
Speicher,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Flexibilitaet {
Starr,
Flexibel,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Objektrolle {
pub code: &'static str,
pub objekttyp: Objekttyp,
pub richtung: Option<Flussrichtung>,
pub ebene: u8,
pub funktion: Option<Objektfunktion>,
}
impl Objektrolle {
#[must_use]
pub fn from_code(code: &LokationsbuendelObjektcode) -> Option<&'static Self> {
Self::from_wire(code.as_str())
}
#[must_use]
pub fn from_wire(code: &str) -> Option<&'static Self> {
OBJEKTROLLEN
.binary_search_by_key(&code, |r| r.code)
.ok()
.map(|i| &OBJEKTROLLEN[i])
}
#[must_use]
pub fn as_objektcode(&self) -> LokationsbuendelObjektcode {
LokationsbuendelObjektcode::new(self.code)
.expect("catalogued object codes carry a valid BDEW check digit")
}
#[must_use]
pub const fn is_verbrauchs_tr(&self) -> bool {
matches!(self.objekttyp, Objekttyp::TechnischeRessource)
&& matches!(self.richtung, Some(Flussrichtung::Verbrauch))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct StrukturObjekt {
pub code: &'static str,
pub flexibilitaet: Flexibilitaet,
pub min: u32,
pub max: Option<u32>,
pub referenz_messlokation: &'static [&'static str],
pub referenz_netzlokation: &'static [&'static str],
}
impl StrukturObjekt {
#[must_use]
pub fn rolle(&self) -> &'static Objektrolle {
Objektrolle::from_wire(self.code)
.expect("every structure row names a catalogued object code")
}
#[must_use]
pub const fn is_mandatory(&self) -> bool {
self.min > 0
}
#[must_use]
pub const fn permits(&self, count: u32) -> bool {
count >= self.min
&& match self.max {
Some(max) => count <= max,
None => true,
}
}
#[must_use]
pub fn cardinality(&self) -> String {
match (self.min, self.max) {
(min, Some(max)) if min == max => min.to_string(),
(min, Some(max)) => format!("{min}-{max}"),
(0, None) => "0-N".to_string(),
(min, None) => format!("≥{min}"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Lokationsbuendelstruktur {
pub code: &'static str,
pub bezeichnung: &'static str,
pub objekte: &'static [StrukturObjekt],
}
impl Lokationsbuendelstruktur {
#[must_use]
pub fn from_code(code: &Lokationsbuendelcode) -> Option<&'static Self> {
Self::from_wire(code.as_str())
}
#[must_use]
pub fn from_wire(code: &str) -> Option<&'static Self> {
STRUKTUREN
.binary_search_by_key(&code, |s| s.code)
.ok()
.map(|i| &STRUKTUREN[i])
}
#[must_use]
pub fn as_code(&self) -> Lokationsbuendelcode {
Lokationsbuendelcode::new(self.code)
.expect("catalogued structure codes carry a valid BDEW check digit")
}
#[must_use]
pub fn objekt(&self, code: &str) -> Option<&'static StrukturObjekt> {
self.objekte.iter().find(|o| o.code == code)
}
pub fn objekte_of(&self, typ: Objekttyp) -> impl Iterator<Item = &'static StrukturObjekt> {
self.objekte
.iter()
.filter(move |o| o.rolle().objekttyp == typ)
}
#[must_use]
pub fn max_ebene(&self) -> u8 {
self.objekte
.iter()
.map(|o| o.rolle().ebene)
.max()
.unwrap_or(1)
}
}
impl std::fmt::Display for Lokationsbuendelstruktur {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} ({})", self.bezeichnung, self.code)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn catalogues_are_sorted_for_binary_search() {
assert!(OBJEKTROLLEN.windows(2).all(|w| w[0].code < w[1].code));
assert!(STRUKTUREN.windows(2).all(|w| w[0].code < w[1].code));
}
#[test]
fn the_codelist_ships_complete() {
assert_eq!(
STRUKTUREN.len(),
15,
"codelist v1.0 publishes 15 structures"
);
assert_eq!(
OBJEKTROLLEN.len(),
27,
"codelist v1.0 publishes 27 object codes"
);
}
#[test]
fn structures_and_object_codes_agree() {
let mut used = std::collections::BTreeSet::new();
for s in STRUKTUREN {
for o in s.objekte {
assert!(
Objektrolle::from_wire(o.code).is_some(),
"{} references uncatalogued object code {}",
s.code,
o.code
);
used.insert(o.code);
}
}
for r in OBJEKTROLLEN {
assert!(
used.contains(&r.code),
"{} is catalogued but unused",
r.code
);
}
}
#[test]
fn references_stay_inside_their_structure() {
for s in STRUKTUREN {
for o in s.objekte {
for r in o
.referenz_messlokation
.iter()
.chain(o.referenz_netzlokation)
{
let target = s.objekt(r).unwrap_or_else(|| {
panic!(
"{}: {} references {r}, which is not in the structure",
s.code, o.code
)
});
let expected = if o.referenz_messlokation.contains(r) {
Objekttyp::Messlokation
} else {
Objekttyp::Netzlokation
};
assert_eq!(target.rolle().objekttyp, expected, "{}: {r}", s.code);
}
}
}
}
#[test]
fn resolves_the_standard_structure() {
let code = Lokationsbuendelcode::new("9992000000026").unwrap();
let s = Lokationsbuendelstruktur::from_code(&code).unwrap();
assert_eq!(s.bezeichnung, "Verbrauch mit einer Messlokation (Standard)");
assert_eq!(s.max_ebene(), 1);
let melo = s.objekt("9992000001032").unwrap();
assert_eq!(melo.flexibilitaet, Flexibilitaet::Starr);
assert_eq!(melo.cardinality(), "1");
assert_eq!(melo.rolle().funktion, Some(Objektfunktion::Netzuebergabe));
let nelo = s.objekt("9992000001256").unwrap();
assert_eq!(nelo.cardinality(), "0-1");
assert_eq!(nelo.rolle().richtung, None);
let tr = s.objekt("9992000001024").unwrap();
assert_eq!(tr.cardinality(), "0-N");
assert!(tr.rolle().is_verbrauchs_tr());
assert!(!tr.is_mandatory());
assert!(tr.permits(0) && tr.permits(9_999));
}
#[test]
fn cascade_structure_reaches_level_three() {
let s = Lokationsbuendelstruktur::from_wire("9992000000183").unwrap();
assert_eq!(s.max_ebene(), 3);
assert_eq!(
s.objekte_of(Objekttyp::Marktlokation).count(),
4,
"levels 1, 2 and 3 (consumption) plus level 3 generation"
);
}
#[test]
fn unpublished_codes_resolve_to_none() {
let code = Lokationsbuendelcode::from_base("999200000999").unwrap();
assert!(Lokationsbuendelstruktur::from_code(&code).is_none());
}
#[test]
fn cardinality_spellings_match_the_codelist() {
let s = Lokationsbuendelstruktur::from_wire("9992000000109").unwrap();
assert_eq!(s.objekt("9992000001032").unwrap().cardinality(), "≥2");
assert_eq!(
s.objekt("9992000001032").unwrap().referenz_netzlokation,
["9992000001256"]
);
}
}