German ID card number parsing

⚓ rust    📅 2025-06-04    👤 surdeus    👁️ 1      

surdeus

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

Read full topic

🏷️ rust_feed