idsmith 0.5.4

Validate and generate checksum-correct IBANs, personal IDs, bank accounts, credit cards, SWIFT/BIC, company IDs, driver's licenses, tax IDs, and passports.
Documentation
use rand::Rng;
#[cfg(feature = "json")]
use serde::Serialize;

#[derive(Debug, Clone)]
#[cfg_attr(feature = "json", derive(Serialize))]
pub struct LeiResult {
    pub code: String,
    pub lou: String,
    pub country_code: String,
    pub valid: bool,
}

#[derive(Debug, Clone, Default)]
pub struct GenOptions {
    pub country: Option<String>,
}

pub struct Registry;

impl Default for Registry {
    fn default() -> Self {
        Self::new()
    }
}

impl Registry {
    pub fn new() -> Self {
        Self
    }

    pub fn generate(&self, opts: &GenOptions, rng: &mut impl Rng) -> LeiResult {
        let lou: String = (0..4)
            .map(|_| {
                let idx = rng.gen_range(0..36);
                if idx < 10 {
                    (b'0' + idx) as char
                } else {
                    (b'A' + idx - 10) as char
                }
            })
            .collect();

        let country = opts
            .country
            .as_deref()
            .unwrap_or_else(|| {
                let countries = [
                    "US", "GB", "DE", "FR", "IT", "ES", "NL", "CH", "JP", "AU", "CA", "SG", "HK",
                    "IN", "CN",
                ];
                countries[rng.gen_range(0..countries.len())]
            })
            .to_uppercase();

        let entity_part: String = (0..12)
            .map(|_| {
                let idx = rng.gen_range(0..36);
                if idx < 10 {
                    (b'0' + idx) as char
                } else {
                    (b'A' + idx - 10) as char
                }
            })
            .collect();

        let base = format!("{}{}{}", lou, country, entity_part);

        let numeric = alpha_to_digits(&base);
        let remainder = mod97(&format!("{}00", numeric));
        let check = 98 - remainder;
        let code = format!("{}{:02}", base, check);

        LeiResult {
            lou: code[..4].to_string(),
            country_code: code[4..6].to_string(),
            code,
            valid: true,
        }
    }

    pub fn validate(&self, code: &str) -> bool {
        if code.len() != 20 {
            return false;
        }
        if !code.chars().all(|c| c.is_ascii_alphanumeric()) {
            return false;
        }
        let numeric = alpha_to_digits(code);
        mod97(&numeric) == 1
    }
}

fn alpha_to_digits(s: &str) -> String {
    let mut out = String::with_capacity(s.len() * 2);
    for c in s.chars() {
        if c.is_ascii_digit() {
            out.push(c);
        } else {
            let val = c.to_ascii_uppercase() as u32 - b'A' as u32 + 10;
            out.push_str(&val.to_string());
        }
    }
    out
}

fn mod97(s: &str) -> u64 {
    let mut remainder: u64 = 0;
    for ch in s.chars() {
        remainder = remainder * 10 + ch.to_digit(10).unwrap_or(0) as u64;
        remainder %= 97;
    }
    remainder
}