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> {
(digits.len() == 10)
.then(|| lok_waggon_check_digit(digits))
.flatten()
}
#[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 TryFrom<String> for MaloId {
type Error = ParseError;
fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse()
}
}
impl From<MaloId> for String {
fn from(id: MaloId) -> String {
id.as_str().to_owned()
}
}
fn lok_waggon_check_digit(digits: &str) -> Option<u8> {
let bytes = digits.as_bytes();
if bytes.is_empty() || !bytes.iter().all(u8::is_ascii_digit) {
return None;
}
let digit = |i: usize| u32::from(bytes[i] - b'0');
let odd: u32 = (0..bytes.len()).step_by(2).map(digit).sum();
let even: u32 = (1..bytes.len()).step_by(2).map(digit).sum();
let total = odd + even * 2;
Some(((10 - (total % 10)) % 10) as u8)
}
#[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 CodeVergabestelle {
BdewStrom,
DvgwGas,
Gs1OrOther,
}
impl CodeVergabestelle {
pub const ALL: [Self; 3] = [Self::BdewStrom, Self::DvgwGas, Self::Gs1OrOther];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::BdewStrom => "BDEW_STROM",
Self::DvgwGas => "DVGW_GAS",
Self::Gs1OrOther => "GS1_OR_OTHER",
}
}
}
crate::codes::string_codes! {
CodeVergabestelle;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct BdewCode {
digits: [u8; 13],
}
const BDEW_CODE_FORMAT: &str =
"a 13-digit BDEW/DVGW Marktpartner-ID, e.g. 9900987654321 (99 = BDEW/Strom, 98 = DVGW/Gas)";
impl BdewCode {
pub const LEN: usize = 13;
#[must_use]
pub fn compute_check_digit(digits: &str) -> Option<u8> {
(digits.len() == 12)
.then(|| lok_waggon_check_digit(digits))
.flatten()
}
#[must_use]
pub const fn check_digit(&self) -> u8 {
self.digits[12] - b'0'
}
#[must_use]
pub fn has_bdew_check_digit(&self) -> bool {
let Some(expected) = Self::compute_check_digit(&self.as_str()[..12]) else {
return false;
};
expected == self.check_digit()
}
#[must_use]
pub const fn vergabestelle(&self) -> CodeVergabestelle {
match (self.digits[0], self.digits[1]) {
(b'9', b'9') => CodeVergabestelle::BdewStrom,
(b'9', b'8') => CodeVergabestelle::DvgwGas,
_ => CodeVergabestelle::Gs1OrOther,
}
}
#[must_use]
pub fn as_str(&self) -> &str {
std::str::from_utf8(&self.digits).unwrap_or_default()
}
}
impl fmt::Display for BdewCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.pad(self.as_str())
}
}
impl FromStr for BdewCode {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let t = s.trim();
let bytes = t.as_bytes();
if bytes.len() != Self::LEN || !bytes.iter().all(u8::is_ascii_digit) {
return Err(ParseError::format("BdewCode", s, BDEW_CODE_FORMAT));
}
let mut digits = [0u8; Self::LEN];
digits.copy_from_slice(bytes);
Ok(Self { digits })
}
}
impl TryFrom<&str> for BdewCode {
type Error = ParseError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
s.parse()
}
}
impl TryFrom<String> for BdewCode {
type Error = ParseError;
fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse()
}
}
impl From<BdewCode> for String {
fn from(id: BdewCode) -> 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 TryFrom<String> for MeloId {
type Error = ParseError;
fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse()
}
}
impl From<MeloId> for String {
fn from(id: MeloId) -> String {
id.value
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum EicType {
#[cfg_attr(feature = "serde", serde(rename = "X"))]
Party,
#[cfg_attr(feature = "serde", serde(rename = "Y"))]
Area,
#[cfg_attr(feature = "serde", serde(rename = "Z"))]
MeasurementPoint,
#[cfg_attr(feature = "serde", serde(rename = "W"))]
ResourceObject,
#[cfg_attr(feature = "serde", serde(rename = "T"))]
TieLine,
#[cfg_attr(feature = "serde", serde(rename = "V"))]
Location,
#[cfg_attr(feature = "serde", serde(rename = "A"))]
Substation,
}
impl EicType {
pub const ALL: [Self; 7] = [
Self::Party,
Self::Area,
Self::MeasurementPoint,
Self::ResourceObject,
Self::TieLine,
Self::Location,
Self::Substation,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Party => "X",
Self::Area => "Y",
Self::MeasurementPoint => "Z",
Self::ResourceObject => "W",
Self::TieLine => "T",
Self::Location => "V",
Self::Substation => "A",
}
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Party => "Party",
Self::Area => "Area or Domain",
Self::MeasurementPoint => "Measurement point",
Self::ResourceObject => "Resource object",
Self::TieLine => "Tie-line",
Self::Location => "Location",
Self::Substation => "Substation",
}
}
#[must_use]
pub const fn from_letter(letter: u8) -> Option<Self> {
match letter {
b'X' => Some(Self::Party),
b'Y' => Some(Self::Area),
b'Z' => Some(Self::MeasurementPoint),
b'W' => Some(Self::ResourceObject),
b'T' => Some(Self::TieLine),
b'V' => Some(Self::Location),
b'A' => Some(Self::Substation),
_ => None,
}
}
}
crate::codes::string_codes! {
EicType;
}
#[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"))]
pub enum Regelzone {
#[cfg_attr(feature = "serde", serde(rename = "TENNET"))]
TenneT,
Amprion,
FiftyHertz,
TransnetBw,
}
impl Regelzone {
pub const ALL: [Self; 4] = [
Self::TenneT,
Self::Amprion,
Self::FiftyHertz,
Self::TransnetBw,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::TenneT => "TENNET",
Self::Amprion => "AMPRION",
Self::FiftyHertz => "FIFTY_HERTZ",
Self::TransnetBw => "TRANSNET_BW",
}
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::TenneT => "TenneT TSO",
Self::Amprion => "Amprion",
Self::FiftyHertz => "50Hertz Transmission",
Self::TransnetBw => "TransnetBW",
}
}
#[must_use]
pub const fn eic_letter(self) -> char {
match self {
Self::TenneT => 'N',
Self::Amprion => 'R',
Self::FiftyHertz => 'V',
Self::TransnetBw => 'W',
}
}
#[must_use]
pub const fn from_eic_letter(letter: u8) -> Option<Self> {
match letter {
b'N' => Some(Self::TenneT),
b'R' => Some(Self::Amprion),
b'V' => Some(Self::FiftyHertz),
b'W' => Some(Self::TransnetBw),
_ => None,
}
}
#[must_use]
pub const fn control_area_eic(self) -> Eic {
Eic {
chars: match self {
Self::TenneT => *b"10YDE-EON------1",
Self::Amprion => *b"10YDE-RWENET---I",
Self::FiftyHertz => *b"10YDE-VE-------2",
Self::TransnetBw => *b"10YDE-ENBW-----N",
},
}
}
}
crate::codes::string_codes! {
Regelzone;
}
const EIC_FORMAT: &str = "a 16-character EIC: 2 characters of issuing office, an object-type \
letter, 12 characters of `0-9`, `A-Z` or `-`, and a check character \
(ENTSO-E EIC Reference Manual)";
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Eic {
chars: [u8; 16],
}
impl Eic {
pub const LEN: usize = 16;
#[must_use]
pub fn as_str(&self) -> &str {
std::str::from_utf8(&self.chars).unwrap_or("")
}
#[must_use]
pub fn issuing_office(&self) -> &str {
self.as_str().get(..2).unwrap_or("")
}
#[must_use]
pub const fn object_type(&self) -> Option<EicType> {
EicType::from_letter(self.chars[2])
}
#[must_use]
pub const fn check_character(&self) -> char {
self.chars[15] as char
}
pub const GERMAN_LIO: &'static str = "11";
#[must_use]
pub fn is_german(&self) -> bool {
self.issuing_office() == Self::GERMAN_LIO
}
#[must_use]
pub const fn regelzone(&self) -> Option<Regelzone> {
if self.chars[0] != b'1' || self.chars[1] != b'1' || self.chars[2] != b'Y' {
return None;
}
Regelzone::from_eic_letter(self.chars[3])
}
#[must_use]
pub fn compute_check_character(body: &str) -> Option<char> {
let bytes = body.as_bytes();
if bytes.len() != Self::LEN - 1 {
return None;
}
let mut sum: u32 = 0;
for (i, byte) in bytes.iter().enumerate() {
let value = eic_value(*byte)?;
sum += value * (16 - i as u32);
}
let check = 36 - ((sum + 36) % 37);
eic_char(check)
}
}
const fn eic_value(byte: u8) -> Option<u32> {
match byte {
b'0'..=b'9' => Some((byte - b'0') as u32),
b'A'..=b'Z' => Some((byte - b'A') as u32 + 10),
b'-' => Some(36),
_ => None,
}
}
const fn eic_char(value: u32) -> Option<char> {
match value {
0..=9 => Some((b'0' + value as u8) as char),
10..=35 => Some((b'A' + (value - 10) as u8) as char),
_ => None,
}
}
impl fmt::Display for Eic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.pad(self.as_str())
}
}
impl FromStr for Eic {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let err = || ParseError::format("Eic", s, EIC_FORMAT);
let trimmed = s.trim();
if trimmed.len() != Self::LEN {
return Err(err());
}
let upper = trimmed.to_ascii_uppercase();
let bytes = upper.as_bytes();
if !bytes.iter().all(|b| eic_value(*b).is_some()) || !bytes[2].is_ascii_uppercase() {
return Err(err());
}
if Self::compute_check_character(&upper[..Self::LEN - 1]) != Some(bytes[15] as char) {
return Err(err());
}
let mut chars = [0u8; 16];
chars.copy_from_slice(bytes);
Ok(Self { chars })
}
}
impl TryFrom<&str> for Eic {
type Error = ParseError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
s.parse()
}
}
impl TryFrom<String> for Eic {
type Error = ParseError;
fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse()
}
}
impl From<Eic> for String {
fn from(id: Eic) -> String {
id.as_str().to_owned()
}
}
#[cfg(feature = "serde")]
mod serde_impl {
use super::{BdewCode, Eic, 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 BdewCode {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
struct BdewCodeVisitor;
impl Visitor<'_> for BdewCodeVisitor {
type Value = BdewCode;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a 13-digit Marktpartner-ID such as \"9900987654321\"")
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<BdewCode, E> {
v.parse().map_err(de::Error::custom)
}
}
impl<'de> Deserialize<'de> for BdewCode {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_str(BdewCodeVisitor)
}
}
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)
}
}
impl Serialize for Eic {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
struct EicVisitor;
impl Visitor<'_> for EicVisitor {
type Value = Eic;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a 16-character EIC such as \"10YDE-VE-------2\"")
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Eic, E> {
v.parse().map_err(de::Error::custom)
}
}
impl<'de> Deserialize<'de> for Eic {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_str(EicVisitor)
}
}
}
#[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");
}
}
#[test]
fn the_published_eic_examples_are_reproduced() {
for code in [
"10X168Y4E6H0041Z", "10X---ENTSOE---L", "10YDE-VE-------2", "10YDE-ENBW-----N", "10YDE-RWENET---I", "10YDE-EON------1", "10Y1001A1001A83F", ] {
let eic: Eic = code.parse().unwrap_or_else(|e| panic!("{code}: {e}"));
assert_eq!(eic.as_str(), code);
assert_eq!(
Eic::compute_check_character(&code[..15]),
Some(code.as_bytes()[15] as char),
);
}
}
#[test]
fn the_object_type_is_the_third_character() {
let area: Eic = "10YDE-VE-------2".parse().unwrap();
assert_eq!(area.object_type(), Some(EicType::Area));
assert_eq!(area.issuing_office(), "10");
assert_eq!(area.check_character(), '2');
let party: Eic = "10X---ENTSOE---L".parse().unwrap();
assert_eq!(party.object_type(), Some(EicType::Party));
}
#[test]
fn a_corrupted_code_is_refused() {
assert!(
"10YDE-VE-------3".parse::<Eic>().is_err(),
"wrong check char"
);
assert!("10YED-VE-------2".parse::<Eic>().is_err(), "transposition");
assert!("10YDE-VE-------".parse::<Eic>().is_err(), "too short");
assert!("10YDE-VE-------22".parse::<Eic>().is_err(), "too long");
assert!("10YDE_VE-------2".parse::<Eic>().is_err(), "underscore");
}
#[test]
fn the_type_position_must_be_a_letter() {
let body = "1010DE---------";
let check = Eic::compute_check_character(body).unwrap();
assert!(format!("{body}{check}").parse::<Eic>().is_err());
}
#[test]
fn lowercase_is_canonicalised() {
let eic: Eic = " 10yde-ve-------2 ".parse().unwrap();
assert_eq!(eic.to_string(), "10YDE-VE-------2");
}
#[test]
fn a_body_computing_to_the_minus_sign_has_no_check_character() {
let body = "10X000000000002"; assert_eq!(Eic::compute_check_character(body), None);
assert!(format!("{body}-").parse::<Eic>().is_err());
}
#[test]
fn a_short_or_impure_body_has_no_check_character() {
assert_eq!(Eic::compute_check_character("10X"), None);
assert_eq!(Eic::compute_check_character("10x---entsoe---"), None);
}
#[test]
fn an_unknown_type_letter_is_not_a_parse_failure() {
let body = "10Q---FUTURE---";
let check = Eic::compute_check_character(body).unwrap();
let eic: Eic = format!("{body}{check}").parse().unwrap();
assert_eq!(eic.object_type(), None);
}
#[test]
fn every_control_area_code_is_a_valid_eic() {
for zone in Regelzone::ALL {
let eic = zone.control_area_eic();
let text = eic.to_string();
assert_eq!(
text.parse::<Eic>(),
Ok(eic),
"{}: {text} does not round-trip",
zone.as_str(),
);
assert_eq!(eic.object_type(), Some(EicType::Area));
assert_eq!(eic.issuing_office(), "10", "the CIO, not the BDEW");
assert!(!eic.is_german(), "a control area is a CIO code");
}
assert_eq!(
Regelzone::FiftyHertz.control_area_eic().to_string(),
"10YDE-VE-------2",
);
}
#[test]
fn the_regelzone_is_read_off_a_german_y_code() {
let bg: Eic = "11YR-AMPRION-BG9".parse().unwrap();
assert_eq!(bg.regelzone(), Some(Regelzone::Amprion));
assert_eq!(bg.object_type(), Some(EicType::Area));
assert!(bg.is_german());
for (code, zone) in [
("11YN-TENNET--BGQ", Regelzone::TenneT),
("11YV-50HERTZ-BGX", Regelzone::FiftyHertz),
("11YW-TNGBW---BGW", Regelzone::TransnetBw),
] {
let eic: Eic = code.parse().unwrap_or_else(|e| panic!("{code}: {e}"));
assert_eq!(eic.regelzone(), Some(zone));
assert_eq!(zone.eic_letter(), code.as_bytes()[3] as char);
}
}
#[test]
fn only_a_german_area_code_has_a_regelzone() {
let party: Eic = "11XSAP-AMPRION-B".parse().unwrap();
assert_eq!(party.object_type(), Some(EicType::Party));
assert_eq!(party.regelzone(), None, "an X code carries no Regelzone");
assert_eq!(Regelzone::Amprion.control_area_eic().regelzone(), None);
}
#[test]
fn an_owned_string_converts_like_a_borrowed_one() {
fn accepts<T: TryFrom<S>, S>(value: S) -> Result<T, T::Error> {
value.try_into()
}
assert_eq!(
accepts::<MaloId, _>("51238696781".to_owned()).expect("a valid MaLo-ID"),
"51238696781".parse::<MaloId>().expect("the same ID"),
);
assert_eq!(
accepts::<BdewCode, _>("9900987654321".to_owned()).expect("a valid code"),
"9900987654321".parse::<BdewCode>().expect("the same code"),
);
assert_eq!(
accepts::<MeloId, _>("DE0001234567800000000000000012345".to_owned())
.expect("a valid MeLo-ID"),
"DE0001234567800000000000000012345"
.parse::<MeloId>()
.expect("the same ID"),
);
assert_eq!(
accepts::<Eic, _>("10X168Y4E6H0041Z".to_owned()).expect("a valid EIC"),
"10X168Y4E6H0041Z".parse::<Eic>().expect("the same EIC"),
);
let err = accepts::<MaloId, _>("nope".to_owned()).expect_err("not an ID");
assert!(err.to_string().contains("MaloId"), "{err}");
}
}