use crate::error::{IdentifierError, LengthExpectation};
#[inline]
fn char_value(b: u8) -> u32 {
if b.is_ascii_digit() {
u32::from(b - b'0')
} else {
u32::from(b)
}
}
pub(super) fn bdew_check_digit(base: &[u8]) -> u8 {
let odd: u32 = base.iter().step_by(2).map(|&b| char_value(b)).sum();
let even: u32 = base.iter().skip(1).step_by(2).map(|&b| char_value(b)).sum();
((10 - ((odd + even * 2) % 10)) % 10) as u8
}
pub(super) fn validate_numeric_id(
s: &str,
len: usize,
min_first: u8,
) -> Result<(), IdentifierError> {
if s.len() != len {
return Err(IdentifierError::InvalidLength {
expected: LengthExpectation::Exact(len),
actual: s.len(),
});
}
let bytes = s.as_bytes();
for (i, &b) in bytes.iter().enumerate() {
if !b.is_ascii_digit() {
return Err(IdentifierError::InvalidCharacter {
position: i,
character: char_at(s, i),
});
}
}
if bytes[0] - b'0' < min_first {
return Err(IdentifierError::InvalidFormat {
description: format!(
"first digit (Vergabestelle) must be {}-9, got '{}'",
min_first, bytes[0] as char,
)
.into(),
});
}
let (base, check) = bytes.split_at(len - 1);
if check[0] - b'0' != bdew_check_digit(base) {
return Err(IdentifierError::InvalidChecksum);
}
Ok(())
}
pub(super) fn compute_numeric_id_from_base(
base: &str,
len: usize,
min_first: u8,
) -> Result<String, IdentifierError> {
let base_len = len - 1;
if base.len() != base_len {
return Err(IdentifierError::InvalidLength {
expected: LengthExpectation::Exact(base_len),
actual: base.len(),
});
}
let bytes = base.as_bytes();
for (i, &b) in bytes.iter().enumerate() {
if !b.is_ascii_digit() {
return Err(IdentifierError::InvalidCharacter {
position: i,
character: char_at(base, i),
});
}
}
if bytes[0] - b'0' < min_first {
return Err(IdentifierError::InvalidFormat {
description: format!(
"first digit (Vergabestelle) must be {}-9, got '{}'",
min_first, bytes[0] as char,
)
.into(),
});
}
let mut out = String::with_capacity(len);
out.push_str(base);
out.push(char::from(b'0' + bdew_check_digit(bytes)));
Ok(out)
}
fn check_ascii_base(base: &str, prefix: &[u8]) -> Result<(), IdentifierError> {
let bytes = base.as_bytes();
if !bytes.starts_with(prefix) {
return Err(IdentifierError::InvalidFormat {
description: format!(
"identifier must start with Codetyp \"{}\", got \"{}\"",
std::str::from_utf8(prefix).unwrap_or("?"),
base.chars().take(prefix.len()).collect::<String>(),
)
.into(),
});
}
for (i, &b) in bytes.iter().enumerate().skip(prefix.len()) {
if !b.is_ascii_uppercase() && !b.is_ascii_digit() {
return Err(IdentifierError::InvalidCharacter {
position: i,
character: char_at(base, i),
});
}
}
Ok(())
}
pub(super) fn validate_ascii_id(s: &str, prefix: &[u8]) -> Result<(), IdentifierError> {
if s.len() != 11 {
return Err(IdentifierError::InvalidLength {
expected: LengthExpectation::Exact(11),
actual: s.len(),
});
}
if let Some((i, c)) = s.char_indices().find(|(_, c)| !c.is_ascii()) {
return Err(IdentifierError::InvalidCharacter {
position: i,
character: c,
});
}
let bytes = s.as_bytes();
check_ascii_base(&s[..10], prefix)?;
if !bytes[10].is_ascii_digit() {
return Err(IdentifierError::InvalidCharacter {
position: 10,
character: char_at(s, 10),
});
}
if bytes[10] - b'0' != bdew_check_digit(&bytes[..10]) {
return Err(IdentifierError::InvalidChecksum);
}
Ok(())
}
pub(super) fn compute_ascii_id_from_base(
base: &str,
prefix: &[u8],
) -> Result<String, IdentifierError> {
if base.len() != 10 {
return Err(IdentifierError::InvalidLength {
expected: LengthExpectation::Exact(10),
actual: base.len(),
});
}
check_ascii_base(base, prefix)?;
let mut out = String::with_capacity(11);
out.push_str(base);
out.push(char::from(b'0' + bdew_check_digit(base.as_bytes())));
Ok(out)
}
#[inline]
fn char_at(s: &str, i: usize) -> char {
if s.is_char_boundary(i) {
s[i..].chars().next().unwrap_or('\u{FFFD}')
} else {
'\u{FFFD}'
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bdew_reference_vector_lok_waggon() {
assert_eq!(bdew_check_digit(b"4137355924"), 1);
}
#[test]
fn bdew_reference_vector_ascii() {
assert_eq!(bdew_check_digit(b"A113735592"), 5);
}
#[test]
fn multiple_of_ten_yields_zero() {
assert_eq!(bdew_check_digit(b"0000000000"), 0);
}
#[test]
fn numeric_and_ascii_paths_agree() {
let full = compute_numeric_id_from_base("4137355924", 11, 1).unwrap();
assert_eq!(full, "41373559241");
assert_eq!(bdew_check_digit(b"4137355924"), 1);
}
#[test]
fn numeric_id_rejects_leading_zero_when_required() {
let err = validate_numeric_id("01234567890", 11, 1).unwrap_err();
assert!(matches!(err, IdentifierError::InvalidFormat { .. }));
assert!(matches!(
validate_numeric_id("0123456789012", 13, 0),
Err(IdentifierError::InvalidChecksum) | Ok(())
));
}
#[test]
fn non_ascii_error_reports_full_char() {
let err = validate_numeric_id("4137355924ä", 11, 1);
assert!(err.is_err());
}
}