1#![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, )]
10#![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
64pub 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 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
131pub 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 = ®ion_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(®ion_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}