use super::checksum::{compute_numeric_id_from_base, validate_numeric_id};
use crate::error::{IdentifierError, LengthExpectation};
const LEN: usize = 13;
fn validate_format(s: &str) -> Result<(), IdentifierError> {
if s.len() != LEN {
return Err(IdentifierError::InvalidLength {
expected: LengthExpectation::Exact(LEN),
actual: s.len(),
});
}
for (i, c) in s.char_indices() {
if !c.is_ascii_digit() {
return Err(IdentifierError::InvalidCharacter {
position: i,
character: c,
});
}
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MpIdAuthority {
Bdew,
Dvgw,
Gs1Gln,
}
impl MpIdAuthority {
#[must_use]
pub fn nad_agency_code(self) -> &'static str {
match self {
Self::Bdew => "293",
Self::Dvgw => "332",
Self::Gs1Gln => "9",
}
}
#[must_use]
pub fn unb_agency_code(self) -> &'static str {
match self {
Self::Bdew => "500",
Self::Dvgw => "502",
Self::Gs1Gln => "14",
}
}
}
impl std::fmt::Display for MpIdAuthority {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Bdew => "BDEW",
Self::Dvgw => "DVGW",
Self::Gs1Gln => "GS1 GLN",
})
}
}
#[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::marktpartner_id_schema")
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "utoipa", schema(
value_type = String,
pattern = r"^[0-9]{13}$",
example = "9900357000003",
description = "13-stellige Marktpartner-ID: BDEW-Codenummer Strom (Prefix 99), DVGW-Codenummer Gas (Prefix 98) oder GS1 GLN"
))]
pub struct MarktpartnerId(
#[cfg_attr(feature = "validate", garde(custom(check_marktpartner_id)))] Box<str>,
);
#[cfg(feature = "validate")]
fn check_marktpartner_id(value: &str, _: &()) -> Result<(), garde::Error> {
validate_format(value).map_err(garde::Error::from)
}
fn gln_check_digit(base: &[u8]) -> u8 {
let sum: u32 = base
.iter()
.enumerate()
.map(|(i, &b)| u32::from(b - b'0') * if i % 2 == 0 { 1 } else { 3 })
.sum();
((10 - (sum % 10)) % 10) as u8
}
impl MarktpartnerId {
#[must_use = "the validated identifier is returned; ignoring it discards the result"]
pub fn new(s: &str) -> Result<Self, IdentifierError> {
validate_format(s)?;
Ok(Self(Box::from(s)))
}
pub fn new_checked(s: &str) -> Result<Self, IdentifierError> {
let id = Self::new(s)?;
if id.has_valid_bdew_check_digit() || id.has_valid_gln_check_digit() {
Ok(id)
} else {
Err(IdentifierError::InvalidChecksum)
}
}
pub fn from_base(base: &str) -> Result<Self, IdentifierError> {
let full = compute_numeric_id_from_base(base, LEN, 0)?;
Ok(Self(full.into_boxed_str()))
}
pub fn check_digit(base: &str) -> Result<u8, IdentifierError> {
let full = compute_numeric_id_from_base(base, LEN, 0)?;
Ok(full.as_bytes()[LEN - 1] - b'0')
}
#[must_use]
pub fn base(&self) -> &str {
&self.0[..LEN - 1]
}
#[must_use]
pub fn has_valid_bdew_check_digit(&self) -> bool {
validate_numeric_id(&self.0, LEN, 0).is_ok()
}
#[must_use]
pub fn has_valid_gln_check_digit(&self) -> bool {
let bytes = self.0.as_bytes();
bytes[LEN - 1] - b'0' == gln_check_digit(&bytes[..LEN - 1])
}
#[must_use]
pub fn authority(&self) -> MpIdAuthority {
match &self.0[..2] {
"99" => MpIdAuthority::Bdew,
"98" => MpIdAuthority::Dvgw,
_ => MpIdAuthority::Gs1Gln,
}
}
#[must_use]
pub fn is_bdew(&self) -> bool {
self.authority() == MpIdAuthority::Bdew
}
#[must_use]
pub fn is_dvgw(&self) -> bool {
self.authority() == MpIdAuthority::Dvgw
}
#[must_use]
pub fn is_gln(&self) -> bool {
self.authority() == MpIdAuthority::Gs1Gln
}
#[must_use]
pub fn nad_agency_code(&self) -> &'static str {
self.authority().nad_agency_code()
}
#[must_use]
pub fn unb_agency_code(&self) -> &'static str {
self.authority().unb_agency_code()
}
#[must_use]
pub fn to_i64(&self) -> i64 {
self.0
.parse::<i64>()
.expect("MarktpartnerId is validated as 13 ASCII digits; parse to i64 cannot fail")
}
}
impl_identifier_traits!(
MarktpartnerId,
"a 13-digit Marktpartner-ID (BDEW-Codenummer, DVGW-Codenummer oder GS1 GLN)"
);
#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
pub mod serde_as_i64 {
use super::MarktpartnerId;
use crate::error::IdentifierError;
pub fn serialize<S: serde::Serializer>(id: &MarktpartnerId, s: S) -> Result<S::Ok, S::Error> {
s.serialize_i64(id.to_i64())
}
pub fn deserialize<'de, D: serde::Deserializer<'de>>(d: D) -> Result<MarktpartnerId, D::Error> {
struct Visitor;
impl serde::de::Visitor<'_> for Visitor {
type Value = MarktpartnerId;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("a 13-digit Marktpartner-ID as an integer or string")
}
fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<MarktpartnerId, E> {
let v = u64::try_from(v).map_err(|_| {
E::custom(IdentifierError::InvalidFormat {
description: "MarktpartnerId cannot be negative".into(),
})
})?;
self.visit_u64(v)
}
fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<MarktpartnerId, E> {
MarktpartnerId::new(&format!("{v:013}")).map_err(E::custom)
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<MarktpartnerId, E> {
MarktpartnerId::new(v).map_err(E::custom)
}
}
d.deserialize_any(Visitor)
}
}
#[cfg(test)]
mod tests {
use super::*;
const REAL_MP_IDS: &[&str] = &[
"1900100300012",
"1900100300020",
"1900100301002",
"1900100400010",
"1900100500000",
"1900100551582",
];
#[test]
fn real_world_ids_satisfy_bdew_check_digit() {
for id in REAL_MP_IDS {
let mp = MarktpartnerId::new(id).unwrap();
assert!(
mp.has_valid_bdew_check_digit(),
"{id} is a published MP-ID and must satisfy BDEW §8.1"
);
assert!(MarktpartnerId::new_checked(id).is_ok());
}
}
#[test]
fn from_base_matches_bdew_check_digit() {
for id in REAL_MP_IDS {
let base = &id[..12];
assert_eq!(MarktpartnerId::from_base(base).unwrap().as_ref(), *id);
assert_eq!(
MarktpartnerId::check_digit(base).unwrap(),
id.as_bytes()[12] - b'0'
);
}
}
#[test]
fn gln_check_digit_is_the_ean13_procedure() {
assert_eq!(gln_check_digit(b"400638133393"), 1);
let gln = MarktpartnerId::new("4006381333931").unwrap();
assert!(gln.has_valid_gln_check_digit());
assert_eq!(gln.authority(), MpIdAuthority::Gs1Gln);
}
#[test]
fn new_is_permissive_but_new_checked_is_not() {
let bogus = "9900357000000";
assert!(MarktpartnerId::new(bogus).is_ok());
assert!(matches!(
MarktpartnerId::new_checked(bogus),
Err(IdentifierError::InvalidChecksum)
));
}
#[test]
fn authority_and_edifact_codes() {
let cases = [
("9900357000003", MpIdAuthority::Bdew, "293", "500"),
("9812345000004", MpIdAuthority::Dvgw, "332", "502"),
("4006381333931", MpIdAuthority::Gs1Gln, "9", "14"),
];
for (raw, authority, nad, unb) in cases {
let id = MarktpartnerId::new(raw).unwrap();
assert_eq!(id.authority(), authority);
assert_eq!(id.nad_agency_code(), nad);
assert_eq!(id.unb_agency_code(), unb);
assert_eq!(id.is_bdew(), authority == MpIdAuthority::Bdew);
assert_eq!(id.is_dvgw(), authority == MpIdAuthority::Dvgw);
assert_eq!(id.is_gln(), authority == MpIdAuthority::Gs1Gln);
}
}
#[test]
fn format_errors() {
assert!(matches!(
MarktpartnerId::new("123456789012").unwrap_err(),
IdentifierError::InvalidLength {
expected: LengthExpectation::Exact(13),
actual: 12
}
));
assert!(matches!(
MarktpartnerId::new("12345678901234").unwrap_err(),
IdentifierError::InvalidLength { actual: 14, .. }
));
assert!(matches!(
MarktpartnerId::new("123456789012A").unwrap_err(),
IdentifierError::InvalidCharacter {
position: 12,
character: 'A'
}
));
}
#[test]
fn to_i64_conversion() {
assert_eq!(
MarktpartnerId::new("9900357000003").unwrap().to_i64(),
9_900_357_000_003_i64
);
assert_eq!(MarktpartnerId::new("0000000000000").unwrap().to_i64(), 0);
assert_eq!(
MarktpartnerId::new("9999999999999").unwrap().to_i64(),
9_999_999_999_999_i64
);
}
#[cfg(feature = "serde")]
#[test]
fn serde_as_i64_round_trip() {
#[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)]
struct Wrapper {
#[serde(with = "super::serde_as_i64")]
id: MarktpartnerId,
}
let w = Wrapper {
id: MarktpartnerId::new("0900357000009").unwrap(),
};
let json = serde_json::to_string(&w).unwrap();
assert_eq!(json, r#"{"id":900357000009}"#);
assert_eq!(serde_json::from_str::<Wrapper>(&json).unwrap(), w);
assert_eq!(
serde_json::from_str::<Wrapper>(r#"{"id":"0900357000009"}"#).unwrap(),
w
);
assert!(serde_json::from_str::<Wrapper>(r#"{"id":-1}"#).is_err());
}
}