use crate::error::{IdentifierError, LengthExpectation};
pub(super) const ZAEHLPUNKT_LEN: usize = 33;
pub(super) fn validate_zaehlpunktbezeichnung(s: &str) -> Result<(), IdentifierError> {
if s.len() != ZAEHLPUNKT_LEN {
return Err(IdentifierError::InvalidLength {
expected: LengthExpectation::Exact(ZAEHLPUNKT_LEN),
actual: s.len(),
});
}
for c in s.chars().take(2) {
if !c.is_ascii_uppercase() {
return Err(IdentifierError::InvalidFormat {
description:
"first two characters must be uppercase ISO 3166-1 country code (e.g. \"DE\")"
.into(),
});
}
}
for (i, c) in s.chars().enumerate().skip(2) {
if !c.is_ascii_alphanumeric() {
return Err(IdentifierError::InvalidCharacter {
position: i,
character: c,
});
}
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "validate", derive(garde::Validate))]
#[cfg_attr(feature = "validate", garde(allow_unvalidated))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(
feature = "schemars",
schemars(schema_with = "crate::schema_helpers::zaehlpunktbezeichnung_schema")
)]
#[cfg_attr(
feature = "schemars",
schemars(description = crate::identifiers::schema::ZAEHLPUNKTBEZEICHNUNG.description)
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "utoipa", schema(
value_type = String,
pattern = r"^[A-Z]{2}[A-Za-z0-9]{31}$",
example = "DE0000000000000000000000000000042",
description = crate::identifiers::schema::ZAEHLPUNKTBEZEICHNUNG.description
))]
pub struct Zaehlpunktbezeichnung(
#[cfg_attr(feature = "validate", garde(custom(check_zaehlpunktbezeichnung)))] Box<str>,
);
#[cfg(feature = "validate")]
fn check_zaehlpunktbezeichnung(value: &str, _: &()) -> Result<(), garde::Error> {
validate_zaehlpunktbezeichnung(value).map_err(garde::Error::from)
}
impl Zaehlpunktbezeichnung {
#[must_use = "the validated Zählpunktbezeichnung is returned; ignoring it discards the result"]
pub fn new(s: &str) -> Result<Self, IdentifierError> {
validate_zaehlpunktbezeichnung(s)?;
Ok(Self(Box::from(s)))
}
#[must_use]
pub fn country_code(&self) -> &str {
&self.0[..2]
}
#[must_use]
pub fn is_german(&self) -> bool {
self.country_code() == "DE"
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_melo_id(self) -> super::MeloId {
super::MeloId::new(&self.0).expect("the two grammars are identical")
}
}
impl From<super::MeloId> for Zaehlpunktbezeichnung {
fn from(id: super::MeloId) -> Self {
Self(Box::from(id.as_ref()))
}
}
impl_identifier_traits!(
Zaehlpunktbezeichnung,
"a 33-character Zählpunktbezeichnung: ISO 3166-1 country code + 31 alphanumeric characters"
);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
#[non_exhaustive]
pub enum Zaehlpunktart {
Messlokation,
Netzuebergabe,
NetzgangzeitreiheEmob,
NetzzeitreiheEmob,
MabisZaehlpunkt,
}
impl Zaehlpunktart {
#[must_use]
pub const fn is_emobilitaet(self) -> bool {
matches!(self, Self::NetzgangzeitreiheEmob | Self::NetzzeitreiheEmob)
}
#[must_use]
pub const fn is_messlokation(self) -> bool {
matches!(self, Self::Messlokation)
}
#[must_use]
pub const fn bezeichnung(self) -> &'static str {
match self {
Self::Messlokation => "Messlokation",
Self::Netzuebergabe => "Zählpunkt (Netzübergabe)",
Self::NetzgangzeitreiheEmob => "Zählpunkt (eMob)",
Self::NetzzeitreiheEmob => "MaBiS-Zählpunkt für NZR (eMob)",
Self::MabisZaehlpunkt => "MaBiS-Zählpunkt",
}
}
}
impl std::fmt::Display for Zaehlpunktart {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.bezeichnung())
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct Zaehlpunkt {
pub art: Zaehlpunktart,
pub bezeichnung: Zaehlpunktbezeichnung,
}
impl Zaehlpunkt {
#[must_use]
pub const fn new(art: Zaehlpunktart, bezeichnung: Zaehlpunktbezeichnung) -> Self {
Self { art, bezeichnung }
}
#[must_use]
pub const fn is_emobilitaet(&self) -> bool {
self.art.is_emobilitaet()
}
#[must_use]
pub const fn is_messlokation(&self) -> bool {
self.art.is_messlokation()
}
#[must_use]
pub fn as_melo_id(&self) -> Option<super::MeloId> {
self.is_messlokation()
.then(|| self.bezeichnung.clone().into_melo_id())
}
#[must_use]
pub fn country_code(&self) -> &str {
self.bezeichnung.country_code()
}
}
impl std::fmt::Display for Zaehlpunkt {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} {}", self.art, self.bezeichnung)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identifiers::MeloId;
#[test]
fn a_zaehlpunkt_emob_is_not_a_messlokation() {
let zpb = Zaehlpunktbezeichnung::new("DE0000000000000000000000000000042").unwrap();
let emob = Zaehlpunkt::new(Zaehlpunktart::NetzgangzeitreiheEmob, zpb.clone());
assert!(emob.is_emobilitaet());
assert!(!emob.is_messlokation());
assert_eq!(emob.as_melo_id(), None);
let melo = Zaehlpunkt::new(Zaehlpunktart::Messlokation, zpb);
assert!(!melo.is_emobilitaet());
assert_eq!(
melo.as_melo_id(),
Some(MeloId::new("DE0000000000000000000000000000042").unwrap())
);
}
#[test]
fn every_zaehlpunktart_has_a_label() {
for art in [
Zaehlpunktart::Messlokation,
Zaehlpunktart::Netzuebergabe,
Zaehlpunktart::NetzgangzeitreiheEmob,
Zaehlpunktart::NetzzeitreiheEmob,
Zaehlpunktart::MabisZaehlpunkt,
] {
assert!(!art.bezeichnung().is_empty());
assert_eq!(art.is_messlokation(), art == Zaehlpunktart::Messlokation);
}
}
#[test]
fn accepts_the_same_grammar_as_a_melo_id() {
let s = "DE0000000000000000000000000000042";
assert!(Zaehlpunktbezeichnung::new(s).is_ok());
assert!(MeloId::new(s).is_ok());
}
#[test]
fn rejects_what_a_melo_id_rejects() {
for bad in [
"DE123", "de0000000000000000000000000000042", "DE000000000000000000000000000004-", ] {
assert!(Zaehlpunktbezeichnung::new(bad).is_err(), "{bad}");
assert!(MeloId::new(bad).is_err(), "{bad}");
}
}
#[test]
fn conversions_are_explicit_in_both_directions() {
let melo = MeloId::new("DE0000000000000000000000000000001").unwrap();
let zpb = Zaehlpunktbezeichnung::from(melo.clone());
assert_eq!(zpb.as_str(), melo.as_ref());
assert_eq!(zpb.into_melo_id(), melo);
}
#[test]
fn country_helpers_read_the_prefix() {
let zpb = Zaehlpunktbezeichnung::new("AT0000000000000000000000000000042").unwrap();
assert_eq!(zpb.country_code(), "AT");
assert!(!zpb.is_german());
}
}