Info
This post is auto-generated from RSS feed The Rust Programming Language Forum - Latest topics. Source: German ID card number parsing
As a parsing and validation erxercise, I implemented the parsing and validation of German ID card numbers.
Link to German wiki page (may need translation with the tool of your choosing): Ausweisnummer – Wikipedia
main.rs
use std::env::args;
use error::IdCardNumberParseError;
use id_card_number::IdCardNumber;
mod error;
mod id_card_number;
fn main() {
let number = args().nth(1).expect("Please provide a number");
match number.parse::<IdCardNumber>() {
Ok(id_card) => println!("OK: {id_card}"),
Err(err) => eprintln!("ERROR: {err}"),
}
}
error.rs
use std::error::Error;
use std::fmt::{Display, Formatter};
/// Errors that may occur when parsing an ID card number.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum IdCardNumberParseError {
/// The ID card number has an invalid length.
InvalidLength,
/// The ID card number contains an invalid character.
InvalidChar(char),
/// The ID card number's checksum does not match.
ChecksumMismatch { calculated: u32, expected: u32 },
}
impl Display for IdCardNumberParseError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidLength => write!(f, "The length of the string is invalid."),
Self::InvalidChar(chr) => {
write!(f, "Encountered invalid character in number: {chr}")
}
Self::ChecksumMismatch {
calculated,
expected,
} => write!(
f,
"Checksum mismatch. Expected {expected} but calculated {calculated}."
),
}
}
}
impl Error for IdCardNumberParseError {}
id_card_number.rs
use std::array::from_fn;
use std::fmt::Display;
use std::str::FromStr;
use crate::IdCardNumberParseError;
const LENGTH: usize = 9;
const LENGTH_WITH_CHECK_DIGIT: usize = LENGTH + 1;
const RADIX: u32 = 36;
const WEIGHTS: [u32; LENGTH] = [7, 3, 1, 7, 3, 1, 7, 3, 1];
/// Representation of a German ID card number.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct IdCardNumber {
chars: [char; LENGTH],
}
impl IdCardNumber {
/// Calculate the ID card number's checksum.
pub fn checksum(self) -> u32 {
self.chars
.into_iter()
.zip(WEIGHTS)
.map(|(chr, weight)| {
chr.to_digit(RADIX)
.expect("Encountered invalid char in ID card number. This should never happen.")
* weight
})
.sum()
}
/// Return the checksum digit.
pub fn check_digit(&self) -> u32 {
self.checksum() % 10
}
}
impl Display for IdCardNumber {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.chars.iter().collect::<String>().fmt(f)
}
}
impl TryFrom<[char; LENGTH]> for IdCardNumber {
type Error = IdCardNumberParseError;
fn try_from(chars: [char; LENGTH]) -> Result<Self, Self::Error> {
for char in chars {
if char.to_digit(RADIX).is_none() {
return Err(IdCardNumberParseError::InvalidChar(char));
}
}
Ok(IdCardNumber { chars })
}
}
impl TryFrom<[char; LENGTH_WITH_CHECK_DIGIT]> for IdCardNumber {
type Error = IdCardNumberParseError;
fn try_from(chars: [char; LENGTH_WITH_CHECK_DIGIT]) -> Result<Self, Self::Error> {
let [chars @ .., check_char] = chars;
let id_card = Self::try_from(chars)?;
let calculated = id_card.check_digit();
let expected = check_char
.to_digit(10)
.ok_or(IdCardNumberParseError::InvalidChar(check_char))?;
if calculated == expected {
Ok(id_card)
} else {
Err(IdCardNumberParseError::ChecksumMismatch {
calculated,
expected,
})
}
}
}
impl FromStr for IdCardNumber {
type Err = IdCardNumberParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut chars = s.chars();
let array: [char; LENGTH] = [
chars.next().ok_or(IdCardNumberParseError::InvalidLength)?,
chars.next().ok_or(IdCardNumberParseError::InvalidLength)?,
chars.next().ok_or(IdCardNumberParseError::InvalidLength)?,
chars.next().ok_or(IdCardNumberParseError::InvalidLength)?,
chars.next().ok_or(IdCardNumberParseError::InvalidLength)?,
chars.next().ok_or(IdCardNumberParseError::InvalidLength)?,
chars.next().ok_or(IdCardNumberParseError::InvalidLength)?,
chars.next().ok_or(IdCardNumberParseError::InvalidLength)?,
chars.next().ok_or(IdCardNumberParseError::InvalidLength)?,
];
let Some(checksum) = chars.next() else {
return Self::try_from(array);
};
if let Some(_excess_byte) = chars.next() {
return Err(IdCardNumberParseError::InvalidLength);
}
Self::try_from(from_fn::<char, LENGTH_WITH_CHECK_DIGIT, _>(|index| {
// LENGTH_WITH_CHECK_DIGIT = LENGTH + 1
// Hence, the OR case only occurs on the last missing (checksum) digit.
array.get(index).copied().unwrap_or(checksum)
}))
}
}
I'd appreciate any feedback on how the code may be improved.
NB: I intentionally did not use clap
to keep main.rs
simple.
1 post - 1 participant
🏷️ rust_feed