use crate::checkdigit;
use crate::errors::ValidationError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Lei {
bytes: [u8; Self::LENGTH],
}
impl Lei {
pub const LENGTH: usize = 20;
pub fn parse(s: &str) -> Result<Self, ValidationError> {
let found = s.chars().count();
if found != Self::LENGTH {
return Err(ValidationError::WrongLength {
expected: Self::LENGTH,
found,
});
}
for (i, ch) in s.chars().enumerate() {
let legal = if i >= Self::LENGTH - 2 {
ch.is_ascii_digit()
} else {
ch.is_ascii_digit() || ch.is_ascii_uppercase()
};
if !legal {
return Err(ValidationError::InvalidCharacter {
position: i + 1,
found: ch,
});
}
}
let mut bytes = [0u8; Self::LENGTH];
bytes.copy_from_slice(s.as_bytes());
let reserved = core::str::from_utf8(&bytes[4..6]).unwrap_or("");
if reserved != "00" {
return Err(ValidationError::Structure {
rule: "LEI positions 5-6 must be 00",
});
}
let body = core::str::from_utf8(&bytes[0..18]).unwrap_or("");
let expected = checkdigit::lei_check_digits(body)?;
for offset in 0..2 {
let want = expected[offset];
let got = char::from(bytes[18 + offset]);
if want != got {
return Err(ValidationError::BadCheckDigit {
expected: want,
found: got,
});
}
}
Ok(Self { bytes })
}
pub fn validate(s: &str) -> Result<(), ValidationError> {
Self::parse(s).map(|_| ())
}
#[must_use]
pub const fn from_bytes_unchecked(bytes: [u8; Self::LENGTH]) -> Self {
Self { bytes }
}
#[must_use]
#[inline]
pub fn as_str(&self) -> &str {
core::str::from_utf8(&self.bytes).unwrap_or("")
}
#[must_use]
#[inline]
pub fn as_bytes(&self) -> &[u8] {
&self.bytes
}
#[must_use]
#[inline]
pub fn lou_prefix(&self) -> &str {
core::str::from_utf8(&self.bytes[0..4]).unwrap_or("")
}
#[must_use]
#[inline]
pub fn entity_id(&self) -> &str {
core::str::from_utf8(&self.bytes[6..18]).unwrap_or("")
}
#[must_use]
#[inline]
pub fn check_digits(&self) -> &str {
core::str::from_utf8(&self.bytes[18..20]).unwrap_or("")
}
}
impl core::fmt::Display for Lei {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.as_str())
}
}
impl core::str::FromStr for Lei {
type Err = ValidationError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl AsRef<str> for Lei {
fn as_ref(&self) -> &str {
self.as_str()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::display;
use core::str::FromStr;
const GOLDEN: &[&str] = &[
"5493001KJTIIGC8Y1R12", "549300DTUYXVMJXZNY75", ];
#[test]
fn parses_golden_leis() {
for &s in GOLDEN {
let lei = Lei::parse(s).unwrap_or_else(|e| panic!("{s} should parse: {e}"));
assert_eq!(lei.as_str(), s);
}
}
#[test]
fn segment_accessors() {
let lei = Lei::parse("5493001KJTIIGC8Y1R12").unwrap();
assert_eq!(lei.lou_prefix(), "5493");
assert_eq!(lei.entity_id(), "1KJTIIGC8Y1R");
assert_eq!(lei.check_digits(), "12");
assert_eq!(lei.as_bytes(), b"5493001KJTIIGC8Y1R12");
assert_eq!(Lei::LENGTH, 20);
}
#[test]
fn rejects_bad_check_digit() {
assert_eq!(
Lei::parse("5493001KJTIIGC8Y1R13"),
Err(ValidationError::BadCheckDigit {
expected: '2',
found: '3',
})
);
}
#[test]
fn rejects_bad_first_check_digit() {
assert_eq!(
Lei::parse("5493001KJTIIGC8Y1R99"),
Err(ValidationError::BadCheckDigit {
expected: '1',
found: '9',
})
);
}
#[test]
fn rejects_wrong_length() {
assert_eq!(
Lei::parse("5493001KJTIIGC8Y1R1"),
Err(ValidationError::WrongLength {
expected: 20,
found: 19,
})
);
assert_eq!(
Lei::parse("5493001KJTIIGC8Y1R123"),
Err(ValidationError::WrongLength {
expected: 20,
found: 21,
})
);
assert_eq!(
Lei::parse(""),
Err(ValidationError::WrongLength {
expected: 20,
found: 0,
})
);
}
#[test]
fn rejects_lower_case() {
assert!(matches!(
Lei::parse("5493001kJTIIGC8Y1R12"),
Err(ValidationError::InvalidCharacter { position: 8, .. })
));
}
#[test]
fn rejects_non_digit_check_position() {
assert!(matches!(
Lei::parse("5493001KJTIIGC8Y1RX2"),
Err(ValidationError::InvalidCharacter { position: 19, .. })
));
assert!(matches!(
Lei::parse("5493001KJTIIGC8Y1R1X"),
Err(ValidationError::InvalidCharacter { position: 20, .. })
));
}
#[test]
fn rejects_bad_body_character() {
assert_eq!(
Lei::parse("5493001KJTIIGC8Y1-12"),
Err(ValidationError::InvalidCharacter {
position: 18,
found: '-',
})
);
}
#[test]
fn rejects_reserved_field_not_zero_zero() {
assert_eq!(
Lei::parse("5493011KJTIIGC8Y1R12"),
Err(ValidationError::Structure {
rule: "LEI positions 5-6 must be 00",
})
);
assert_eq!(
Lei::parse("5493A01KJTIIGC8Y1R12"),
Err(ValidationError::Structure {
rule: "LEI positions 5-6 must be 00",
})
);
}
#[test]
fn rejects_non_ascii_without_panic() {
assert!(Lei::parse("5493001KJTIIGC8Y1Ré2").is_err());
assert!(Lei::parse("É493001KJTIIGC8Y1R12").is_err());
}
#[test]
fn round_trips_through_str() {
for &s in GOLDEN {
assert_eq!(Lei::parse(s).unwrap().as_str(), s);
}
}
#[test]
fn from_str_matches_parse() {
assert_eq!(
Lei::from_str("5493001KJTIIGC8Y1R12"),
Lei::parse("5493001KJTIIGC8Y1R12")
);
assert!(Lei::from_str("nonsense").is_err());
}
#[test]
fn display_renders_identifier() {
let lei = Lei::parse("5493001KJTIIGC8Y1R12").unwrap();
assert_eq!(display(lei).as_str(), "5493001KJTIIGC8Y1R12");
}
#[test]
fn as_ref_str() {
let lei = Lei::parse("5493001KJTIIGC8Y1R12").unwrap();
let s: &str = lei.as_ref();
assert_eq!(s, "5493001KJTIIGC8Y1R12");
}
#[test]
fn from_bytes_unchecked_round_trip() {
let lei = Lei::from_bytes_unchecked(*b"5493001KJTIIGC8Y1R12");
assert_eq!(lei, Lei::parse("5493001KJTIIGC8Y1R12").unwrap());
}
#[test]
fn is_copy_and_eq_and_hashable() {
let a = Lei::parse("5493001KJTIIGC8Y1R12").unwrap();
let b = a; assert_eq!(a, b);
assert_ne!(a, Lei::parse("549300DTUYXVMJXZNY75").unwrap());
let keys = [a, b];
assert_eq!(keys[0], keys[1]);
}
}