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;

use super::checksum;
use super::date::Gender;
use super::IdResult;

// Real 6-digit administrative division codes (valid from 1985 or earlier)
static REGIONS: &[u32] = &[
    // Beijing (11)
    110101, 110102, 110105, 110106, 110107, 110108, 110109, // Tianjin (12)
    120101, 120102, 120103, 120104, 120105, 120106, // Hebei (13)
    130102, 130104, 130105, 130107, 130203, 130204, 130205, 130302, 130303, 130304, 130402, 130403,
    130502, 130503, 130602, 130604, 130702, // Shanxi (14)
    140202, 140203, 140212, 140302, // Inner Mongolia (15)
    150102, 150103, 150104, 150105, 150202, 150203, 150204, 150302, 150402,
    // Liaoning (21)
    210102, 210103, 210104, 210105, 210106, 210111, 210112, 210113, 210114, 210202, 210203, 210204,
    210211, 210302, 210303, 210304, 210402, 210502, 210602, 210702, // Jilin (22)
    220102, 220103, 220104, 220105, 220202, 220203, 220204, 220302,
    // Heilongjiang (23)
    230102, 230103, 230104, 230108, 230202, 230203, 230204, 230302, 230303, 230402, 230502, 230602,
    230603, 230702, 230802, 230803, 230902, 231002, // Shanghai (31)
    310101, 310104, 310105, 310106, 310107, 310109, 310110, 310112, // Jiangsu (32)
    320102, 320104, 320105, 320106, 320111, 320113, 320114, 320211, 320302, 320311, 320402, 320404,
    320502, 320602, // Zhejiang (33)
    330102, 330103, 330104, 330105, 330106, 330202, 330203, 330205, 330206, 330211, 330302, 330303,
    330402, 330502, // Anhui (34)
    340102, 340103, 340104, 340111, 340202, 340203, 340302, 340304, 340402, 340403, 340405, 340406,
    340502, 340503, 340602, 340604, 340802, // Fujian (35)
    350102, 350103, 350104, 350105, 350111, 350121, 350128, 350202, 350203, 350205, 350211, 350302,
    350303, 350402, 350403, 350502, // Jiangxi (36)
    360102, 360103, 360104, 360111, 360202, 360203, 360302, 360402, 360502,
    // Shandong (37)
    370102, 370103, 370104, 370105, 370202, 370203, 370211, 370302, 370303, 370402,
    // Henan (41)
    410102, 410103, 410104, 410105, 410106, 410202, 410203, 410204, 410205, 410302, 410303, 410304,
    410305, 410402, 410411, 410502, 410503, 410602, 410702, // Hubei (42)
    420102, 420103, 420104, 420105, 420106, 420107, 420111, 420112, 420113, 420202, 420203, 420205,
    420302, 420303, 420602, 420802, // Hunan (43)
    430102, 430103, 430104, 430105, 430111, 430202, 430203, 430204, 430211, 430302, 430304, 430402,
    430502, 430503, 430602, 430603, // Guangdong (44)
    440103, 440104, 440105, 440106, 440111, 440112, 440203, 440204, 440222, 440224, 440229, 440232,
    440302, 440303, 440304, 440305, 440402, 440511, // Guangxi (45)
    450102, 450103, 450105, 450202, 450203, 450204, 450205, 450302, 450303, 450304, 450305, 450402,
    450403, 450502, // Sichuan (51)
    510112, 510113, 510121, 510129, 510131, 510302, 510303, 510304, 510402, 510403, 510411,
    // Guizhou (52)
    520102, 520103, 520111, 520112, 520113, 520203, // Yunnan (53)
    530102, 530103, 530111, 530112, // Tibet (54)
    540102, // Shaanxi (61)
    610102, 610103, 610104, 610111, 610112, 610113, 610114, 610202, 610203, 610302, 610303, 610402,
    610403, // Gansu (62)
    620102, 620103, 620104, 620105, 620111, 620201, 620302, 620402, 620502,
    // Qinghai (63)
    630102, 630103, 630104, 632221, 632222, 632223, 632224, // Ningxia (64)
    640121, 640122, 640202, // Xinjiang (65)
    650102, 650103, 650104, 650105, 650106, 650107, 650121, 650202, 650203,
];

pub fn generate(opts: &super::GenOptions, rng: &mut impl Rng) -> String {
    let gender = Gender::resolve_or_random(opts.gender, rng);
    // Use 1985+ to match region code validity dates
    let (year, month, day) = match opts.year {
        Some(y) => super::date::rand_date_with_year(rng, y),
        None => super::date::rand_date(rng, 1985, 2005),
    };

    let full_region = REGIONS[rng.gen_range(0..REGIONS.len())];

    // Sequence: odd = male, even = female
    let seq = match gender {
        Gender::Male => rng.gen_range(0..=498u16) * 2 + 1, // odd
        Gender::Female => rng.gen_range(0..=499u16) * 2,   // even
    };

    let base = format!(
        "{:06}{:04}{:02}{:02}{:03}",
        full_region, year, month, day, seq
    );
    let digits: Vec<u8> = base.bytes().map(|b| b - b'0').collect();
    let check = checksum::iso7064_mod11_2(&digits);
    format!("{}{}", base, check)
}

pub fn validate(code: &str) -> bool {
    let upper = code.to_uppercase();
    if upper.len() != 18 {
        return false;
    }
    if !upper[..17].chars().all(|c| c.is_ascii_digit()) {
        return false;
    }
    let last = upper.chars().last().unwrap();
    if !last.is_ascii_digit() && last != 'X' {
        return false;
    }
    let digits: Vec<u8> = upper[..17].bytes().map(|b| b - b'0').collect();
    checksum::iso7064_mod11_2(&digits) == last
}

pub fn parse(code: &str) -> IdResult {
    let upper = code.to_uppercase();
    let (gender, dob) = if upper.len() == 18 {
        let seq_digit = upper.as_bytes()[16] - b'0';
        let g = if seq_digit % 2 == 1 {
            Some("male".to_string())
        } else {
            Some("female".to_string())
        };
        let d = format!("{}-{}-{}", &upper[6..10], &upper[10..12], &upper[12..14]);
        (g, Some(d))
    } else {
        (None, None)
    };

    IdResult {
        country_code: "".to_string(),
        code: upper,
        gender,
        dob,
        valid: validate(code),
    }
}