china_citizen_id/
lib.rs

1//! 中华人民共和国公民身份号码 (GB 11643-1999)
2
3#![deny(unsafe_code)]
4#![deny(clippy::all, clippy::pedantic, clippy::cargo)]
5#![allow(
6    clippy::inline_always,
7    clippy::needless_range_loop,
8    clippy::missing_errors_doc, // TODO
9)]
10// ---
11#![cfg_attr(docsrs, feature(doc_cfg))]
12
13use core::fmt;
14use core::str;
15use std::collections::HashMap;
16use std::sync::LazyLock;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19#[non_exhaustive]
20pub enum Error {
21    InvalidLength,
22    InvalidCharacter,
23    WrongCheckNumber,
24    InvalidBirthday,
25}
26
27impl fmt::Display for Error {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        <Self as fmt::Debug>::fmt(self, f)
30    }
31}
32
33impl core::error::Error for Error {}
34
35pub struct ParsedIdNumber {
36    sex: Sex,
37    birthday: (u16, u8, u8),
38    region: Region,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum Sex {
43    Male,
44    Female,
45}
46
47impl ParsedIdNumber {
48    #[must_use]
49    pub fn sex(&self) -> Sex {
50        self.sex
51    }
52
53    #[must_use]
54    pub fn birthday_ymd(&self) -> (u16, u8, u8) {
55        self.birthday
56    }
57
58    #[must_use]
59    pub fn region(&self) -> &Region {
60        &self.region
61    }
62}
63
64/// 二代身份证号 (18位)
65pub fn parse_v2(id_str: &str) -> Result<ParsedIdNumber, Error> {
66    let id: [u8; 18] = id_str
67        .as_bytes()
68        .try_into()
69        .map_err(|_| Error::InvalidLength)?;
70
71    for i in 0..17 {
72        if !id[i].is_ascii_digit() {
73            return Err(Error::InvalidCharacter);
74        }
75    }
76    if !id[17].is_ascii_digit() && id[17] != b'X' {
77        return Err(Error::InvalidCharacter);
78    }
79
80    {
81        const W: [u8; 17] = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
82        let mut sum: u32 = if id[17] == b'X' {
83            10
84        } else {
85            u32::from(id[17] - b'0')
86        };
87        for i in 0..17 {
88            sum += u32::from(id[i] - b'0') * u32::from(W[i]);
89            sum %= 11;
90        }
91        if sum != 1 {
92            return Err(Error::WrongCheckNumber);
93        }
94    }
95
96    let birthday = {
97        let year = u16_from_char4([id[6], id[7], id[8], id[9]]);
98        let month = u8_from_char2([id[10], id[11]]);
99        let day = u8_from_char2([id[12], id[13]]);
100
101        // Please change it after I'm 200 years old :)
102        if year <= 1800 || year >= 2200 {
103            return Err(Error::InvalidBirthday);
104        }
105
106        if !validate_ymd(year, month, day) {
107            return Err(Error::InvalidBirthday);
108        }
109
110        (year, month, day)
111    };
112
113    let region = {
114        let region_code = [id[0], id[1], id[2], id[3], id[4], id[5]];
115        get_region(region_code, birthday.0)
116    };
117
118    let sex = if id[16] & 1 == 1 {
119        Sex::Male
120    } else {
121        Sex::Female
122    };
123
124    Ok(ParsedIdNumber {
125        sex,
126        birthday,
127        region,
128    })
129}
130
131/// 一代身份证号 (15位)
132pub fn parse_v1(id_str: &str) -> Result<ParsedIdNumber, Error> {
133    let id: [u8; 15] = id_str
134        .as_bytes()
135        .try_into()
136        .map_err(|_| Error::InvalidLength)?;
137
138    for i in 0..15 {
139        if !id[i].is_ascii_digit() {
140            return Err(Error::InvalidCharacter);
141        }
142    }
143
144    let birthday = {
145        let year = u16_from_char4([b'1', b'9', id[6], id[7]]);
146        let month = u8_from_char2([id[8], id[9]]);
147        let day = u8_from_char2([id[10], id[11]]);
148
149        if !validate_ymd(year, month, day) {
150            return Err(Error::InvalidBirthday);
151        }
152
153        (year, month, day)
154    };
155
156    let region = {
157        let region_code = [id[0], id[1], id[2], id[3], id[4], id[5]];
158        get_region(region_code, birthday.0)
159    };
160
161    let sex = if id[14] & 1 == 1 {
162        Sex::Male
163    } else {
164        Sex::Female
165    };
166
167    Ok(ParsedIdNumber {
168        sex,
169        birthday,
170        region,
171    })
172}
173
174#[derive(Debug, Clone, PartialEq, Eq)]
175pub struct Region {
176    pub province: Option<&'static str>,
177    pub city: Option<&'static str>,
178    pub district: Option<&'static str>,
179}
180
181fn get_region(region_code: [u8; 6], year: u16) -> Region {
182    static DATASET: LazyLock<HashMap<u16, HashMap<&'static str, &'static str>>> =
183        LazyLock::new(|| serde_json::from_str(include_str!("region.json")).unwrap());
184
185    let c = &region_code;
186    let t1 = [c[0], c[1], b'0', b'0', b'0', b'0'];
187    let t1 = str::from_utf8(&t1).unwrap();
188    let t2 = [c[0], c[1], c[2], c[3], b'0', b'0'];
189    let t2 = str::from_utf8(&t2).unwrap();
190    let t3 = str::from_utf8(&region_code).unwrap();
191
192    let dataset = &*DATASET;
193
194    if t1 == t3 {
195        if let Some(data) = dataset.get(&year) {
196            let province = data.get(t1).copied();
197            return Region {
198                province,
199                city: None,
200                district: None,
201            };
202        }
203    } else if let Some(data) = dataset.get(&year) {
204        let province = data.get(t1).copied();
205        let city = data.get(t2).copied();
206        let district = data.get(t3).copied();
207        return Region {
208            province,
209            city,
210            district,
211        };
212    }
213
214    Region {
215        province: None,
216        city: None,
217        district: None,
218    }
219}
220
221fn validate_ymd(year: u16, month: u8, day: u8) -> bool {
222    let month: time::Month = match month.try_into() {
223        Ok(m) => m,
224        Err(_) => return false,
225    };
226
227    time::Date::from_calendar_date(i32::from(year), month, day).is_ok()
228}
229
230#[inline(always)]
231fn u16_from_char4(c: [u8; 4]) -> u16 {
232    u16::from(c[0] - b'0') * 1000
233        + u16::from(c[1] - b'0') * 100
234        + u16::from(c[2] - b'0') * 10
235        + u16::from(c[3] - b'0')
236}
237
238#[inline(always)]
239fn u8_from_char2(c: [u8; 2]) -> u8 {
240    (c[0] - b'0') * 10 + (c[1] - b'0')
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn test_parse() {
249        {
250            let id = "11010519491231002X";
251            let parsed = parse_v2(id).unwrap();
252            assert_eq!(parsed.birthday, (1949, 12, 31));
253            assert_eq!(parsed.sex(), Sex::Female);
254        }
255
256        {
257            let id = "440524188001010014";
258            let parsed = parse_v2(id).unwrap();
259            assert_eq!(parsed.birthday, (1880, 1, 1));
260            assert_eq!(parsed.sex(), Sex::Male);
261        }
262
263        {
264            let id = "420111198203251029";
265            let parsed = parse_v2(id).unwrap();
266            assert_eq!(parsed.birthday, (1982, 3, 25));
267            assert_eq!(parsed.sex(), Sex::Female);
268            assert_eq!(parsed.region().province, Some("湖北省"));
269            assert_eq!(parsed.region().city, Some("武汉市"));
270            assert_eq!(parsed.region().district, Some("洪山区"));
271        }
272    }
273}