idcard/
lib.rs

1//!  Chinese Indentity Card Utilities
2//!
3//! This package provides utilities to validate ID number, to extract detailed
4//! ID information, to upgrade ID number from 15-digit to 18-digit, to generate
5//! fake ID number, and some related functions for HongKong/Macau/Taiwan ID.
6//!
7//! # Examples
8//!
9//! ```
10//! use idcard::{Identity, fake, Gender};
11//!
12//! let id = Identity::new("632123820927051");
13//!
14//! // Determines whether the ID number is valid.
15//! assert_eq!(id.is_valid(), true);
16//!
17//! // Gets properties
18//! let number = id.number();
19//! let gender = id.gender();
20//! let age = id.age();
21//! let birth_date = id.birth_date();
22//! let region = id.region();
23//! // and so on...
24//!
25//! // Upgrades an ID number from 15-digit to 18-digit.
26//! let id18 = idcard::upgrade("310112850409522").unwrap();
27//! assert_eq!(&id18, "310112198504095227");
28//!
29//! // Validates an ID number.
30//! assert_eq!(idcard::validate("230127197908177456"), true);
31//!
32//! // Generates a random fake ID number using the given options.
33//! let opts = fake::FakeOptions::new()
34//!     .region("3301")
35//!     .min_year(1990)
36//!     .max_year(2000)
37//!     .female();
38//! match fake::rand_with(&opts) {
39//!     Ok(num) => println!("{}", num),
40//!     Err(e) => println!("{}", e),
41//! }
42//! ```
43//! For more information ,please read the API documentation.
44//!
45
46#[macro_use]
47extern crate lazy_static;
48
49use chrono::{Datelike, Local, NaiveDate};
50use std::collections::HashMap;
51use std::fmt;
52
53pub mod fake;
54pub mod hk;
55pub mod mo;
56pub mod region;
57pub mod tw;
58
59const ID_V1_LEN: usize = 15;
60const ID_V2_LEN: usize = 18;
61
62static CHINESE_ZODIAC: [&'static str; 12] = [
63    "猪", "鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊", "猴", "鸡", "狗",
64];
65
66static CELESTIAL_STEM: [&'static str; 10] =
67    ["癸", "甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "任"];
68
69static TERRESTRIAL_BRANCH: [&'static str; 12] = [
70    "亥", "子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌",
71];
72
73lazy_static! {
74    static ref PROVINCE_CODE_NAME: HashMap<&'static str, &'static str> = {
75        let mut map = HashMap::new();
76        map.insert("11", "北京");
77        map.insert("12", "天津");
78        map.insert("13", "河北");
79        map.insert("14", "山西");
80        map.insert("15", "内蒙古");
81        map.insert("21", "辽宁");
82        map.insert("22", "吉林");
83        map.insert("23", "黑龙江");
84        map.insert("31", "上海");
85        map.insert("32", "江苏");
86        map.insert("33", "浙江");
87        map.insert("34", "安徽");
88        map.insert("35", "福建");
89        map.insert("36", "江西");
90        map.insert("37", "山东");
91        map.insert("41", "河南");
92        map.insert("42", "湖北");
93        map.insert("43", "湖南");
94        map.insert("44", "广东");
95        map.insert("45", "广西");
96        map.insert("46", "海南");
97        map.insert("50", "重庆");
98        map.insert("51", "四川");
99        map.insert("52", "贵州");
100        map.insert("53", "云南");
101        map.insert("54", "西藏");
102        map.insert("61", "陕西");
103        map.insert("62", "甘肃");
104        map.insert("63", "青海");
105        map.insert("64", "宁夏");
106        map.insert("65", "新疆");
107        map.insert("71", "台湾");
108        map.insert("81", "香港");
109        map.insert("82", "澳门");
110        map.insert("83", "台湾");
111        map.insert("91", "国外");
112        map
113    };
114}
115
116/// Custom error type.
117#[derive(Debug)]
118pub enum Error {
119    InvalidNumber,
120    UpgradeError,
121    GenerateFakeIDError(String),
122}
123
124impl std::error::Error for Error {}
125
126impl fmt::Display for Error {
127    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
128        match self {
129            Error::InvalidNumber => write!(f, "Invalid Number"),
130            Error::UpgradeError => write!(f, "Upgrade Failed"),
131            Error::GenerateFakeIDError(msg) => write!(f, "Generate Fake ID Error: {}", msg),
132        }
133    }
134}
135
136/// The type of demographic genders
137#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
138pub enum Gender {
139    Male,
140    Female,
141}
142
143/// An object representation of the Chinese ID.
144#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
145pub struct Identity {
146    number: String,
147    valid: bool,
148}
149
150impl Identity {
151    /// Creates an identity object from given number.
152    pub fn new(number: &str) -> Self {
153        let mut id = Identity {
154            number: number.trim().to_ascii_uppercase(),
155            valid: false,
156        };
157        if id.number.len() == ID_V1_LEN {
158            match upgrade(&id.number) {
159                Ok(value) => {
160                    id.number = value;
161                    id.valid = true;
162                }
163                _ => id.valid = false,
164            }
165        } else if id.number.len() == ID_V2_LEN {
166            id.valid = if validate_v2(&id.number) { true } else { false }
167        } else {
168            id.valid = false;
169        }
170        id
171    }
172
173    /// Returns the ID number.
174    pub fn number(&self) -> &str {
175        &self.number
176    }
177
178    /// Returns the formatted date of birth(yyyy-mm-dd).
179    pub fn birth_date(&self) -> Option<String> {
180        if !self.is_valid() {
181            return None;
182        }
183        let birth = &self.number[6..14];
184        let year = &birth[0..4];
185        let month = &birth[4..6];
186        let date = &birth[6..8];
187        Some(format!("{}-{}-{}", year, month, date))
188    }
189
190    /// Returns the year of birth.
191    pub fn year(&self) -> Option<u32> {
192        if !self.is_valid() {
193            return None;
194        }
195        if let Ok(year) = self.number[6..10].parse::<u32>() {
196            Some(year)
197        } else {
198            None
199        }
200    }
201
202    /// Returns the month of birth.
203    pub fn month(&self) -> Option<u32> {
204        if !self.is_valid() {
205            return None;
206        }
207        if let Ok(month) = self.number[10..12].parse::<u32>() {
208            Some(month)
209        } else {
210            None
211        }
212    }
213
214    /// Returns the day in the month of the birth.
215    pub fn day(&self) -> Option<u32> {
216        if !self.is_valid() {
217            return None;
218        }
219        if let Ok(day) = self.number[12..14].parse::<u32>() {
220            Some(day)
221        } else {
222            None
223        }
224    }
225
226    /// Calculates the current age based on the computer's local date,
227    /// if the birth year is less than the local's, it returns `None`.
228    pub fn age(&self) -> Option<u32> {
229        if !self.is_valid() {
230            return None;
231        }
232        if let Ok(year) = self.number[6..10].parse::<u32>() {
233            let current = Local::now().year() as u32;
234            if current < year {
235                return None;
236            }
237            Some(current - year)
238        } else {
239            None
240        }
241    }
242
243    /// Calculates the age based on the given year, if the given year is less
244    /// than the birth year, it returns `None`.
245    pub fn age_in_year(&self, year: u32) -> Option<u32> {
246        if !self.is_valid() {
247            return None;
248        }
249        if let Ok(value) = self.number[6..10].parse::<u32>() {
250            if year < value {
251                return None;
252            }
253            Some(year - value)
254        } else {
255            None
256        }
257    }
258
259    /// Returns the gender.
260    pub fn gender(&self) -> Option<Gender> {
261        if !self.is_valid() {
262            return None;
263        }
264        if let Ok(code) = self.number[16..17].parse::<u32>() {
265            if code % 2 != 0 {
266                Some(Gender::Male)
267            } else {
268                Some(Gender::Female)
269            }
270        } else {
271            None
272        }
273    }
274
275    /// Returns the province name based on the first 2 digits of the number
276    pub fn province(&self) -> Option<&str> {
277        if !self.is_valid() {
278            return None;
279        }
280        let code = &self.number[0..2];
281        match PROVINCE_CODE_NAME.get(code) {
282            Some(name) => Some(*name),
283            None => None,
284        }
285    }
286
287    /// Returns the region name based on the first 6 digits of the number
288    pub fn region(&self) -> Option<&str> {
289        if !self.is_valid() {
290            return None;
291        }
292        region::query(&self.number[0..6])
293    }
294
295    /// Returns the region code(the first 6 digits)
296    pub fn region_code(&self) -> Option<&str> {
297        if !self.is_valid() {
298            return None;
299        }
300        Some(&self.number[0..6])
301    }
302
303    /// Returns the constellation by the date of birth.
304    pub fn constellation(&self) -> Option<&str> {
305        if !self.is_valid() {
306            return None;
307        }
308        let month = match self.month() {
309            Some(value) => value,
310            None => return None,
311        };
312        let day = match self.day() {
313            Some(value) => value,
314            None => return None,
315        };
316        constellation(month, day)
317    }
318
319    /// Returns the Chinese Era by the year of birth.
320    pub fn chinese_era(&self) -> Option<String> {
321        if !self.is_valid() {
322            return None;
323        }
324        let year = match self.year() {
325            Some(value) => value,
326            None => return None,
327        };
328        chinese_era(year)
329    }
330
331    /// Returns the Chinese Zodiac animal by the year of birth.
332    pub fn chinese_zodiac(&self) -> Option<&str> {
333        if !self.is_valid() {
334            return None;
335        }
336        let year = match self.year() {
337            Some(value) => value,
338            None => return None,
339        };
340        chinese_zodiac(year)
341    }
342
343    /// Checks if the number is valid.
344    pub fn is_valid(&self) -> bool {
345        self.valid
346    }
347
348    /// Checks if the number is empty.
349    pub fn is_empty(&self) -> bool {
350        self.number.is_empty()
351    }
352
353    /// Returns the length of the number.
354    pub fn len(&self) -> usize {
355        self.number.len()
356    }
357}
358
359/// Returns the Chinese Zodiac animal by the given year, the given year
360/// should not be less than 1000.
361pub fn chinese_zodiac(year: u32) -> Option<&'static str> {
362    if year < 1000 {
363        return None;
364    }
365    let end = 3;
366    let idx = (year - end) % 12;
367    let zod = CHINESE_ZODIAC[idx as usize];
368    Some(zod)
369}
370
371/// Returns the Chinese Era by the given year, the given year
372/// should not be less than 1000.
373pub fn chinese_era(year: u32) -> Option<String> {
374    if year < 1000 {
375        return None;
376    }
377    let i = (year - 3) % 10;
378    let j = (year - 3) % 12;
379    let era = format!(
380        "{}{}",
381        CELESTIAL_STEM[i as usize], TERRESTRIAL_BRANCH[j as usize]
382    );
383    Some(era)
384}
385
386/// Returns the constellation by the given month and day.
387pub fn constellation(month: u32, day: u32) -> Option<&'static str> {
388    let result = if (month == 1 && day >= 20) || (month == 2 && day <= 18) {
389        "水瓶座"
390    } else if (month == 2 && day >= 19) || (month == 3 && day <= 20) {
391        "双鱼座"
392    } else if (month == 3 && day > 20) || (month == 4 && day <= 19) {
393        "白羊座"
394    } else if (month == 4 && day >= 20) || (month == 5 && day <= 20) {
395        "金牛座"
396    } else if (month == 5 && day >= 21) || (month == 6 && day <= 21) {
397        "双子座"
398    } else if (month == 6 && day > 21) || (month == 7 && day <= 22) {
399        "巨蟹座"
400    } else if (month == 7 && day > 22) || (month == 8 && day <= 22) {
401        "狮子座"
402    } else if (month == 8 && day >= 23) || (month == 9 && day <= 22) {
403        "处女座"
404    } else if (month == 9 && day >= 23) || (month == 10 && day <= 23) {
405        "天秤座"
406    } else if (month == 10 && day > 23) || (month == 11 && day <= 22) {
407        "天蝎座"
408    } else if (month == 11 && day > 22) || (month == 12 && day <= 21) {
409        "射手座"
410    } else if (month == 12 && day > 21) || (month == 1 && day <= 19) {
411        "魔羯座"
412    } else {
413        return None;
414    };
415    Some(result)
416}
417
418/// Upgrades a Chinese ID number from 15-digit to 18-digit.
419pub fn upgrade(number: &str) -> Result<String, Error> {
420    let number = number.trim().to_ascii_uppercase();
421    if number.len() == ID_V1_LEN && is_digital(&number) {
422        let mut idv2 = String::new();
423        let birthday = "19".to_owned() + &number[6..12];
424        let birth_date = NaiveDate::parse_from_str(&birthday, "%Y%m%d");
425
426        let cal = match birth_date {
427            Ok(value) => value,
428            _ => return Err(Error::UpgradeError),
429        };
430
431        idv2.push_str(&number[0..6]);
432        idv2.push_str(&cal.year().to_string());
433        idv2.push_str(&number[8..]);
434
435        let iarr = match string_to_integer_array(&idv2) {
436            Ok(value) => value,
437            _ => return Err(Error::UpgradeError),
438        };
439
440        let weight = get_weights_sum(&iarr);
441        if let Some(code) = get_check_code(weight) {
442            idv2.push_str(code);
443            Ok(idv2)
444        } else {
445            return Err(Error::UpgradeError);
446        }
447    } else {
448        Err(Error::InvalidNumber)
449    }
450}
451
452/// Validates a Chinese ID number(only supports 15/18-digit).
453pub fn validate(number: &str) -> bool {
454    let number = number.trim().to_ascii_uppercase();
455    if number.len() == ID_V1_LEN {
456        validate_v1(&number)
457    } else if number.len() == ID_V2_LEN {
458        validate_v2(&number)
459    } else {
460        false
461    }
462}
463
464fn validate_v1(number: &str) -> bool {
465    if number.len() == ID_V1_LEN && is_digital(number) {
466        let code = &number[0..2];
467        if !PROVINCE_CODE_NAME.contains_key(code) {
468            return false;
469        }
470
471        let birthday = "19".to_owned() + &number[6..12];
472        let birth_date = NaiveDate::parse_from_str(&birthday, "%Y%m%d");
473        birth_date.is_ok()
474    } else {
475        false
476    }
477}
478
479fn validate_v2(number: &str) -> bool {
480    if number.len() != ID_V2_LEN {
481        return false;
482    }
483
484    let birth_date = NaiveDate::parse_from_str(&number[6..14], "%Y%m%d");
485    if !birth_date.is_ok() {
486        return false;
487    }
488
489    let code17 = &number[0..17];
490    let code18 = &number[17..18];
491    if is_digital(code17) {
492        let iarr = match string_to_integer_array(code17) {
493            Ok(value) => value,
494            _ => return false,
495        };
496
497        let sum17 = get_weights_sum(&iarr);
498        if let Some(code) = get_check_code(sum17) {
499            if code == code18.to_uppercase() {
500                return true;
501            }
502        }
503    }
504    false
505}
506
507fn is_digital(s: &str) -> bool {
508    if s.is_empty() {
509        false
510    } else {
511        s.chars().all(char::is_numeric)
512    }
513}
514
515fn get_check_code(sum: u32) -> Option<&'static str> {
516    let code = match sum % 11 {
517        10 => "2",
518        9 => "3",
519        8 => "4",
520        7 => "5",
521        6 => "6",
522        5 => "7",
523        4 => "8",
524        3 => "9",
525        2 => "X",
526        1 => "0",
527        0 => "1",
528        _ => return None,
529    };
530    Some(code)
531}
532
533fn string_to_integer_array(s: &str) -> Result<Vec<u32>, Error> {
534    let mut v: Vec<u32> = Vec::new();
535    for ch in s.chars() {
536        match ch.to_digit(10) {
537            Some(i) => v.push(i),
538            None => return Err(Error::InvalidNumber),
539        }
540    }
541    Ok(v)
542}
543
544fn get_weights_sum(arr: &[u32]) -> u32 {
545    let weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
546    let mut sum = 0;
547    if weights.len() == arr.len() {
548        for i in 0..arr.len() {
549            for j in 0..weights.len() {
550                if i == j {
551                    sum = sum + arr[i] * weights[j];
552                }
553            }
554        }
555    }
556    sum
557}
558
559#[cfg(test)]
560mod tests {
561    use super::*;
562
563    #[test]
564    fn test_upgrade() {
565        let id = Identity::new("632123820927051");
566        assert_eq!(id.is_valid(), true);
567        assert_eq!(id.number(), "632123198209270518");
568        let id = upgrade("310112850409522").unwrap();
569        assert_eq!(&id, "310112198504095227");
570    }
571
572    #[test]
573    fn test_validate() {
574        assert_eq!(validate("511702800222130"), true);
575        assert_eq!(validate("230127197908177456"), true);
576    }
577
578    #[test]
579    fn test_compute_age() {
580        let id = Identity::new("511702800222130");
581        assert_eq!(id.age_in_year(2020), Some(40));
582        assert_eq!(id.age_in_year(1980), Some(0));
583        assert_eq!(id.age_in_year(1900), None);
584    }
585
586    #[test]
587    fn test_utilities() {
588        assert_eq!(chinese_zodiac(1000), Some("鼠"));
589        assert_eq!(chinese_zodiac(1900), Some("鼠"));
590        assert_eq!(chinese_zodiac(2021), Some("牛"));
591        assert_eq!(chinese_era(1000), Some("庚子".to_string()));
592        assert_eq!(chinese_era(1900), Some("庚子".to_string()));
593        assert_eq!(chinese_era(2021), Some("辛丑".to_string()));
594        assert_eq!(constellation(10, 25), Some("天蝎座"));
595        assert_eq!(constellation(2, 29), Some("双鱼座"));
596        assert_eq!(constellation(0, 32), None);
597    }
598
599    #[test]
600    fn test_identity() {
601        let a = Identity::new("632123820927051");
602        let b = Identity::new("632123198209270518");
603        assert_eq!(a == b, true);
604        let a = Identity::new("21021119810503545X");
605        let b = Identity::new("21021119810503545x");
606        assert_eq!(a == b, true);
607        let a = Identity::new("330421197402080974");
608        let b = Identity::new("130133197909136078");
609        assert_eq!(a != b, true);
610    }
611}