cinv 0.1.0

GB 11643-1999 Chinese citizen identification number validator
Documentation
mod address_codes;

use std::collections::HashMap;
use address_codes::DEFAULT_ADDRESS_CODES;

/// ---------- 校验模式 ----------
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AddressMatchMode {
    /// 仅精确匹配 6 位地址码(最严格)
    Exact6,
    /// 6 位不匹配时,回溯到 4 位地级码,再回溯到 2 位省级码(兼容历史变动)
    Fallback64,
}

impl Default for AddressMatchMode {
    fn default() -> Self {
        AddressMatchMode::Fallback64
    }
}

/// ---------- 公开接口1:用内置地址码 + 默认模式(Fallback64)----------
pub fn is_valid(id: &str) -> bool {
    is_valid_with_mode(id, AddressMatchMode::default())
}

/// ---------- 公开接口2:用内置地址码 + 指定模式 ----------
pub fn is_valid_with_mode(
    id: &str,
    mode: AddressMatchMode,
) -> bool {
    is_valid_with_map_and_mode(id, &DEFAULT_ADDRESS_CODES, mode)
}

/// ---------- 公开接口3:用外部地址码 + 默认模式 ----------
pub fn is_valid_with_map(id: &str, codes: &HashMap<&str, &str>) -> bool {
    is_valid_with_map_and_mode(id, codes, AddressMatchMode::default())
}

/// ---------- 公开接口4:用外部地址码 + 指定模式(最灵活) ----------
pub fn is_valid_with_map_and_mode(
    id: &str,
    codes: &HashMap<&str, &str>,
    mode: AddressMatchMode,
) -> bool {
    let id = id.trim();

    // 1. 长度
    if id.len() != 18 {
        return false;
    }

    // 2. 前17位必须是数字
    let body = &id[..17];
    if !body.chars().all(|c| c.is_ascii_digit()) {
        return false;
    }

    // 3. 第18位
    let last = match id.chars().nth(17) {
        Some(c) => c.to_ascii_uppercase(),
        None => return false,
    };
    if !(last.is_ascii_digit() || last == 'X') {
        return false;
    }

    // 4. 地址码首位不能为0
    match id.chars().next() {
        Some('0') => return false,
        None => return false,
        _ => {}
    }

    // 5. 出生日期
    if !is_valid_date(&id[6..14]) {
        return false;
    }

    // 6. 校验码 MOD 11-2
    if !verify_checksum(body, last) {
        return false;
    }

    // 7. 地址码存在性(按模式选择策略)
    match mode {
        AddressMatchMode::Exact6 => is_address_exact6(id, codes),
        AddressMatchMode::Fallback64 => is_address_fallback64(id, codes),
    }
}

/// ---------- 日期校验(纯标准库) ----------
fn is_valid_date(s: &str) -> bool {
    if s.len() != 8 {
        return false;
    }

    let year = match s[0..4].parse::<u32>() {
        Ok(v) => v,
        Err(_) => return false,
    };
    let month = match s[4..6].parse::<u32>() {
        Ok(v) => v,
        Err(_) => return false,
    };
    let day = match s[6..8].parse::<u32>() {
        Ok(v) => v,
        Err(_) => return false,
    };

    if month < 1 || month > 12 {
        return false;
    }

    let days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
    let max_day = if month == 2 && is_leap_year(year) {
        29
    } else {
        days_in_month[(month - 1) as usize]
    };

    day >= 1 && day <= max_day
}

/// ---------- 闰年判断 ----------
#[inline]
fn is_leap_year(y: u32) -> bool {
    (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0)
}

/// ---------- MOD 11-2 校验码 ----------
const WEIGHTS: [u32; 17] =
    [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];

const CHECK_MAP: &[u8; 11] = b"10X98765432";

/// ---------- 校验码验证 ----------
fn verify_checksum(body: &str, last: char) -> bool {
    let mut sum = 0u32;

    for (i, c) in body.chars().enumerate() {
        let d = match c.to_digit(10) {
            Some(v) => v,
            None => return false,
        };
        sum += d * WEIGHTS[i];
    }

    let expected = CHECK_MAP[(sum % 11) as usize] as char;
    expected == last
}

/// ---------- 地址码校验:仅精确 6 位 ----------
/// 地址码进行 6→4→2 回溯(即精确 6 位不匹配时,回溯到 4 位地级/直辖市汇总码、再回溯到 2 位省级码),
/// 根本原因是你的配置文件(HashMap)的数据完整度与国标 GB 11643‑1999 中地址码的实际层级之间存在现实差距,主要出于以下三点考量:

/// 配置文件的裁剪与层级缺失
/// GB/T 2260的行政区划代码是六级嵌套的(省 2 位、地市 2 位、县区 2 位,拼成 6 位)。但在实际工程中:
/// 很多内置或自定义 JSON 只配置了常用的县级 6 位码,或者只配了省级/地级汇总码(如 110000北京市,330100杭州市)。
/// 如果一个身份证是合法的 330106(西湖区),但你的配置里只存了 330000(浙江省),不做回溯就会直接判为非法,导致大量真实老号码被拦截。

/// 历史行政区划变动的兼容
/// 身份证号一旦分配终身不变,但行政区划会合并、撤县设区、改名甚至撤销:
/// 某人早年发证用的是旧县码(如已撤销的 XXXXXX),该码在最新的民政部表里可能被移除。
/// 但该号码在法律上依然有效。回溯到上级 4 位或 2 位码(上级码相对稳定),可以在不丢失严格性的前提下,
/// 承认其所属的上一级行政区是合法的,避免误杀历史存量数据。

/// 国标语义上的合理性
/// GB 11643‑1999 规定前 6 位为县(市、旗、区)级代码,其结构是严格递进的:
/// 1‑2 位:省级(省、自治区、直辖市)
/// 3‑4 位:地级(市、州、盟)
/// 5‑6 位:县级(区、县、旗)
/// 当精确的 6 位码因配置不全查不到时,只要其上级行政隶属关系在配置中存在(如 33浙江、3301杭州),就可以认为地址码“结构合法”。
/// 这是一种降级兼容校验,常用于配置数据非全量加载的场景。
fn is_address_exact6(id: &str, codes: &HashMap<&str, &str>) -> bool {
    let code6 = match id.get(0..6) {
        Some(c) => c,
        None => return false,
    };
    codes.contains_key(code6)
}

/// ---------- 地址码校验:6 → 4 → 2 回溯 ----------
fn is_address_fallback64(id: &str, codes: &HashMap<&str, &str>) -> bool {
    let code6 = match id.get(0..6) {
        Some(c) => c,
        None => return false,
    };
    if codes.contains_key(code6) {
        return true;
    }

    let code4 = match id.get(0..4) {
        Some(c) => format!("{}00", c),
        None => return false,
    };
    if codes.contains_key(code4.as_str()) {
        return true;
    }

    let code2 = match id.get(0..2) {
        Some(c) => format!("{}0000", c),
        None => return false,
    };
    if codes.contains_key(code2.as_str()) {
        return true;
    }

    false
}

/// 生成合法的测试身份证号
/// 
/// # 参数
/// - `area_code`: 6位地址码
/// - `birth_date`: 8位出生日期(YYYYMMDD)
/// - `seq`: 3位顺序码
/// 
/// # 返回值
/// - `Some(String)`: 生成的18位合法身份证号
/// - `None`: 输入参数不合法(包含非数字字符等)
pub fn generate_test_id_with_area_code(area_code: &str, birth_date: &str, seq: &str) -> Option<String> {
    let base = format!("{}{}{}", area_code, birth_date, seq);
    
    // 检查长度
    if base.len() != 17 {
        return None;
    }

    let mut sum: u32 = 0;
    for (i, c) in base.chars().enumerate() {
        let digit = match c.to_digit(10) {
            Some(d) => d,
            None => return None,  // 包含非数字字符
        };
        sum += digit * WEIGHTS[i];
    }
    let check = CHECK_MAP[(sum % 11) as usize] as char;
    Some(format!("{}{}", base, check))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_valid_default_mode() {
        assert!(is_valid("11010519491231002X"));
    }

    #[test]
    fn test_exact6_mode() {
        // 330106 西湖区 在内置表中
        assert!(is_valid_with_mode("330106199003071236", AddressMatchMode::Exact6));

        // 330109 萧山区 不在内置表中(示例表没配),Exact6 模式下应失败
        assert!(!is_valid_with_mode("330109199003071239", AddressMatchMode::Exact6));
    }

    #[test]
    fn test_fallback64_mode() {
        // 330109 萧山区 不在内置表中,但 330100 杭州市在表中,Fallback64 下应成功
        assert!(is_valid_with_mode("330109199003071235", AddressMatchMode::Fallback64));
    }

    #[test]
    fn test_with_external_map() {
        let mut custom = HashMap::new();
        custom.insert("330106", "浙江省杭州市西湖区");

        // 默认模式(Fallback64)
        assert!(is_valid_with_map("330106199003071236", &custom));

        // 精确模式
        assert!(is_valid_with_map_and_mode(
            "330106199003071236",
            &custom,
            AddressMatchMode::Exact6,
        ));

        // 330109 不在自定义表中,精确模式应失败
        assert!(!is_valid_with_map_and_mode(
            "330109199003071239",
            &custom,
            AddressMatchMode::Exact6,
        ));

        // 330109 不在自定义表中,但 330100 也不在,回溯也会失败
        assert!(!is_valid_with_map_and_mode(
            "330109199003071239",
            &custom,
            AddressMatchMode::Fallback64,
        ));
    }

    #[test]
    fn test_invalid_examples() {
        assert!(!is_valid("110105194912310021")); // 校验码错误
        assert!(!is_valid("11010519491331002X")); // 月份13
        assert!(!is_valid("11010519490229002X")); // 1949年2月29
        assert!(!is_valid("01010519491231002X")); // 地址码首位0
        assert!(!is_valid(""));                    // 空字符串
    }

    #[test]
    fn test_generate_valid_id() {
        let id = generate_test_id_with_area_code("330109", "19900307", "123");
        assert_eq!(id, Some("330109199003071235".into()));
        assert!(is_valid(id.as_ref().unwrap()));

        let id2 = generate_test_id_with_area_code("330109", "19900307", "001");
        assert_eq!(id2, Some("330109199003070013".into()));
        assert!(is_valid(id2.as_ref().unwrap()));

        // 生成正确的测试身份证号
        let id = generate_test_id_with_area_code("330109", "19900307", "123");
        assert!(id.is_some());
        
        let id_str = id.unwrap();  // 这里 unwrap 是安全的,因为上面已经检查过了
        dbg!(&id_str);
        
        // 验证生成的号码可以通过校验
        assert!(is_valid(&id_str));
    }

    #[test]
    fn test_generate_id_with_invalid_input() {
        // 包含非数字字符
        let id = generate_test_id_with_area_code("33010A", "19900307", "123");
        assert!(id.is_none());
        
        // 长度不足
        let id = generate_test_id_with_area_code("3301", "19900307", "123");
        assert!(id.is_none());
    }

    #[test]
    fn test_fallback64_mode_with_generated_id() {
        // 使用生成的正确身份证号
        let id = generate_test_id_with_area_code("330109", "19900307", "123").unwrap();
        assert!(is_valid_with_mode(&id, AddressMatchMode::Fallback64));
    }

    #[test]
    fn test_exact6_mode_with_generated_id() {
        // 330106 西湖区 在内置表中
        let id = generate_test_id_with_area_code("330106", "19900307", "123").unwrap();
        assert!(is_valid_with_mode(&id, AddressMatchMode::Exact6));

        // 330115 不在内置表中,Exact6 模式下应失败
        let id2 = generate_test_id_with_area_code("330115", "19900307", "123").unwrap();
        assert!(!is_valid_with_mode(&id2, AddressMatchMode::Exact6));
    }
}