use std::fmt;
use std::str::FromStr;
use crate::error::ParseError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
pub enum MaloIssuer {
Dvgw,
Bdew,
}
impl MaloIssuer {
pub const ALL: [Self; 2] = [Self::Dvgw, Self::Bdew];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Dvgw => "DVGW",
Self::Bdew => "BDEW",
}
}
}
crate::codes::string_codes! {
MaloIssuer;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct MaloId {
digits: [u8; 11],
}
const MALO_FORMAT: &str =
"an 11-digit MaLo-ID with a valid check digit, first digit 1-9, e.g. 41373559241";
impl MaloId {
pub const LEN: usize = 11;
#[must_use]
pub fn compute_check_digit(digits: &str) -> Option<u8> {
let bytes = digits.as_bytes();
if bytes.len() != 10 || !bytes.iter().all(u8::is_ascii_digit) {
return None;
}
let digit = |i: usize| u32::from(bytes[i] - b'0');
let odd: u32 = (0..10).step_by(2).map(digit).sum();
let even: u32 = (1..10).step_by(2).map(digit).sum();
let total = odd + even * 2;
Some(((10 - (total % 10)) % 10) as u8)
}
#[must_use]
pub const fn check_digit(&self) -> u8 {
self.digits[10] - b'0'
}
#[must_use]
pub const fn issuer(&self) -> MaloIssuer {
match self.digits[0] {
b'1'..=b'3' => MaloIssuer::Dvgw,
_ => MaloIssuer::Bdew,
}
}
#[must_use]
pub fn as_str(&self) -> &str {
std::str::from_utf8(&self.digits).unwrap_or_default()
}
}
impl fmt::Display for MaloId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.pad(self.as_str())
}
}
impl FromStr for MaloId {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let err = || ParseError::format("MaloId", s, MALO_FORMAT);
let t = s.trim();
let bytes = t.as_bytes();
if bytes.len() != Self::LEN || !bytes.iter().all(u8::is_ascii_digit) || bytes[0] == b'0' {
return Err(err());
}
let expected = Self::compute_check_digit(&t[..10]).ok_or_else(err)?;
if bytes[10] - b'0' != expected {
return Err(err());
}
let mut digits = [0u8; Self::LEN];
digits.copy_from_slice(bytes);
Ok(Self { digits })
}
}
impl TryFrom<&str> for MaloId {
type Error = ParseError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
s.parse()
}
}
impl From<MaloId> for String {
fn from(id: MaloId) -> String {
id.as_str().to_owned()
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct MeloId {
value: String,
}
const MELO_FORMAT: &str = "a 33-character Zählpunktbezeichnung: 2 uppercase letters, \
6-digit Netzbetreiber number, then 25 alphanumeric characters (VDE-AR-N 4400)";
impl MeloId {
pub const LEN: usize = 33;
#[must_use]
pub fn as_str(&self) -> &str {
&self.value
}
#[must_use]
pub fn country(&self) -> &str {
&self.value[..2]
}
#[must_use]
pub fn netzbetreiber_nr(&self) -> &str {
&self.value[2..8]
}
}
impl fmt::Display for MeloId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.pad(&self.value)
}
}
impl FromStr for MeloId {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let err = || ParseError::format("MeloId", s, MELO_FORMAT);
let t = s.trim();
if t.len() != Self::LEN || !t.bytes().all(|b| b.is_ascii_alphanumeric()) {
return Err(err());
}
let value = t.to_ascii_uppercase();
let bytes = value.as_bytes();
if !bytes[..2].iter().all(u8::is_ascii_uppercase)
|| !bytes[2..8].iter().all(u8::is_ascii_digit)
{
return Err(err());
}
Ok(Self { value })
}
}
impl TryFrom<&str> for MeloId {
type Error = ParseError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
s.parse()
}
}
impl From<MeloId> for String {
fn from(id: MeloId) -> String {
id.value
}
}
#[cfg(feature = "serde")]
mod serde_impl {
use super::{MaloId, MeloId};
use serde::de::{self, Visitor};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
impl Serialize for MaloId {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
struct MaloIdVisitor;
impl Visitor<'_> for MaloIdVisitor {
type Value = MaloId;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("an 11-digit MaLo-ID string such as \"41373559241\"")
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<MaloId, E> {
v.parse().map_err(de::Error::custom)
}
}
impl<'de> Deserialize<'de> for MaloId {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_str(MaloIdVisitor)
}
}
impl Serialize for MeloId {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
struct MeloIdVisitor;
impl Visitor<'_> for MeloIdVisitor {
type Value = MeloId;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a 33-character Zählpunktbezeichnung")
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<MeloId, E> {
v.parse().map_err(de::Error::custom)
}
}
impl<'de> Deserialize<'de> for MeloId {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_str(MeloIdVisitor)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_anwendungshilfe_example_is_reproduced() {
assert_eq!(MaloId::compute_check_digit("4137355924"), Some(1));
let malo: MaloId = "41373559241".parse().unwrap();
assert_eq!(malo.check_digit(), 1);
assert_eq!(malo.issuer(), MaloIssuer::Bdew);
}
#[test]
fn the_scheme_is_not_luhn() {
assert_eq!(MaloId::compute_check_digit("5123869678"), Some(1));
assert!("51238696781".parse::<MaloId>().is_ok());
assert!("51238696780".parse::<MaloId>().is_err());
}
#[test]
fn a_round_total_gives_check_digit_zero() {
assert_eq!(MaloId::compute_check_digit("6200000000"), Some(0));
assert!("62000000000".parse::<MaloId>().is_ok());
assert_eq!(MaloId::compute_check_digit("1000000000"), Some(9));
assert_eq!(MaloId::compute_check_digit("8642097531"), Some(2));
}
#[test]
fn issuer_follows_the_first_digit() {
let dvgw: MaloId = {
let check = MaloId::compute_check_digit("1234567890").unwrap();
format!("1234567890{check}").parse().unwrap()
};
assert_eq!(dvgw.issuer(), MaloIssuer::Dvgw);
let bdew: MaloId = "41373559241".parse().unwrap();
assert_eq!(bdew.issuer(), MaloIssuer::Bdew);
}
#[test]
fn malformed_malo_ids_are_rejected() {
for s in [
"",
"4137355924", "413735592411", "4137355924X", "41373559240", "01373559241", "41 37355924 1", ] {
assert!(s.parse::<MaloId>().is_err(), "{s:?} must not parse");
}
assert!(" 41373559241 ".parse::<MaloId>().is_ok());
}
#[test]
fn malo_round_trips_and_orders() {
let a: MaloId = "41373559241".parse().unwrap();
assert_eq!(a.to_string().parse::<MaloId>().unwrap(), a);
assert_eq!(String::from(a), "41373559241");
assert_eq!(format!("{a:>13}"), " 41373559241", "padding is honoured");
let err = "nope".parse::<MaloId>().unwrap_err();
assert_eq!(err.type_name(), "MaloId");
assert!(err.to_string().contains("check digit"), "{err}");
}
const MELO: &str = "DE00056266802AO6G56M11SN51G21M24S";
#[test]
fn a_real_shaped_melo_id_parses() {
let melo: MeloId = MELO.parse().unwrap();
assert_eq!(melo.as_str(), MELO);
assert_eq!(melo.country(), "DE");
assert_eq!(melo.netzbetreiber_nr(), "000562");
assert_eq!(melo.to_string().parse::<MeloId>().unwrap(), melo);
}
#[test]
fn lowercase_normalises_to_uppercase() {
let lower = MELO.to_lowercase();
let melo: MeloId = lower.parse().unwrap();
assert_eq!(melo.as_str(), MELO);
}
#[test]
fn malformed_melo_ids_are_rejected() {
for s in [
"",
"DE0005626680", "DE00056266802AO6G56M11SN51G21M24S7", "D30005626680AO6G56M11SN51G21M24S5", "DEX0056266802AO6G56M11SN51G21M24S", "DE00056266802AO6G56M11SN51G21M2-S", ] {
assert!(s.parse::<MeloId>().is_err(), "{s:?} must not parse");
}
}
}