mod address_codes;
use std::collections::HashMap;
use address_codes::DEFAULT_ADDRESS_CODES;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AddressMatchMode {
Exact6,
Fallback64,
}
impl Default for AddressMatchMode {
fn default() -> Self {
AddressMatchMode::Fallback64
}
}
pub fn is_valid(id: &str) -> bool {
is_valid_with_mode(id, AddressMatchMode::default())
}
pub fn is_valid_with_mode(
id: &str,
mode: AddressMatchMode,
) -> bool {
is_valid_with_map_and_mode(id, &DEFAULT_ADDRESS_CODES, mode)
}
pub fn is_valid_with_map(id: &str, codes: &HashMap<&str, &str>) -> bool {
is_valid_with_map_and_mode(id, codes, AddressMatchMode::default())
}
pub fn is_valid_with_map_and_mode(
id: &str,
codes: &HashMap<&str, &str>,
mode: AddressMatchMode,
) -> bool {
let id = id.trim();
if id.len() != 18 {
return false;
}
let body = &id[..17];
if !body.chars().all(|c| c.is_ascii_digit()) {
return false;
}
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;
}
match id.chars().next() {
Some('0') => return false,
None => return false,
_ => {}
}
if !is_valid_date(&id[6..14]) {
return false;
}
if !verify_checksum(body, last) {
return false;
}
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)
}
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
}
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)
}
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
}
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() {
assert!(is_valid_with_mode("330106199003071236", AddressMatchMode::Exact6));
assert!(!is_valid_with_mode("330109199003071239", AddressMatchMode::Exact6));
}
#[test]
fn test_fallback64_mode() {
assert!(is_valid_with_mode("330109199003071235", AddressMatchMode::Fallback64));
}
#[test]
fn test_with_external_map() {
let mut custom = HashMap::new();
custom.insert("330106", "浙江省杭州市西湖区");
assert!(is_valid_with_map("330106199003071236", &custom));
assert!(is_valid_with_map_and_mode(
"330106199003071236",
&custom,
AddressMatchMode::Exact6,
));
assert!(!is_valid_with_map_and_mode(
"330109199003071239",
&custom,
AddressMatchMode::Exact6,
));
assert!(!is_valid_with_map_and_mode(
"330109199003071239",
&custom,
AddressMatchMode::Fallback64,
));
}
#[test]
fn test_invalid_examples() {
assert!(!is_valid("110105194912310021")); assert!(!is_valid("11010519491331002X")); assert!(!is_valid("11010519490229002X")); assert!(!is_valid("01010519491231002X")); 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(); 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() {
let id = generate_test_id_with_area_code("330106", "19900307", "123").unwrap();
assert!(is_valid_with_mode(&id, AddressMatchMode::Exact6));
let id2 = generate_test_id_with_area_code("330115", "19900307", "123").unwrap();
assert!(!is_valid_with_mode(&id2, AddressMatchMode::Exact6));
}
}