personnummer/
lib.rs

1#[macro_use]
2extern crate lazy_static;
3
4use chrono::{Datelike, NaiveDate, Utc};
5use regex::{Match, Regex};
6
7use std::{convert::TryFrom, error::Error, fmt};
8
9lazy_static! {
10    static ref PNR_REGEX: Regex = Regex::new(
11        r"(?x)
12        ^                    # Starts with
13        (?P<century>\d{2})?  # Maybe the century
14        (?P<year>\d{2})      # Year with two digits
15        (?P<month>\d{2})     # Month
16        (?P<day>\d{2})       # Day
17        (?P<divider>[-+]?)?  # Divider can be - or +
18        (?P<number>\d{3})    # At least three digits
19        (?P<control>\d?)     # And an optional control digit
20        $"
21    )
22    .unwrap();
23}
24
25/// The extra value added to coordination numbers.
26const COORDINATION_NUMBER: u32 = 60;
27
28#[derive(Debug)]
29pub enum PersonnummerError {
30    InvalidInput,
31    InvalidDate,
32}
33
34impl fmt::Display for PersonnummerError {
35    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
36        match self {
37            PersonnummerError::InvalidInput => write!(f, "Invalid format"),
38            PersonnummerError::InvalidDate => write!(f, "Invalid date"),
39        }
40    }
41}
42
43impl Error for PersonnummerError {}
44
45#[allow(dead_code)]
46/// Personnummer holds relevant data to check for valid personal identity numbers.
47pub struct Personnummer {
48    date: chrono::NaiveDate,
49    serial: u32,
50    control: u8,
51    divider: char,
52    coordination: bool,
53}
54
55/// FormattedPersonnummer holds two formats of a normalized personal identity number, one long and
56/// one short format. The long format displays the full century while the short format only
57/// displays the year.
58pub struct FormattedPersonnummer {
59    long: String,
60    short: String,
61}
62
63impl FormattedPersonnummer {
64    /// Returns the long format of a formatted personal identity number as a String.
65    pub fn long(&self) -> String {
66        self.long.clone()
67    }
68
69    /// Returns the short format of a formatted personal identity number as a String.
70    pub fn short(&self) -> String {
71        self.short.clone()
72    }
73}
74
75impl TryFrom<&str> for Personnummer {
76    type Error = PersonnummerError;
77
78    fn try_from(pnr: &str) -> Result<Self, PersonnummerError> {
79        let caps = PNR_REGEX
80            .captures(pnr)
81            .ok_or(PersonnummerError::InvalidInput)?;
82
83        let century = match caps.name("century") {
84            Some(m) => m.as_str().parse::<u32>().unwrap_or(19) * 100,
85            None => 1900,
86        };
87
88        let match_to_u32 =
89            |m: Option<Match<'_>>| -> u32 { m.unwrap().as_str().parse::<u32>().unwrap_or(0) };
90
91        let year = match_to_u32(caps.name("year"));
92        let month = match_to_u32(caps.name("month"));
93        let day = match_to_u32(caps.name("day"));
94        let serial = match_to_u32(caps.name("number"));
95
96        let control = caps
97            .name("control")
98            .unwrap()
99            .as_str()
100            .parse::<u8>()
101            .unwrap_or(0);
102
103        let divider = caps
104            .name("divider")
105            .unwrap()
106            .as_str()
107            .parse::<char>()
108            .unwrap_or('\0');
109
110        let date = match NaiveDate::from_ymd_opt(
111            (century + year) as i32,
112            month,
113            day % COORDINATION_NUMBER,
114        ) {
115            Some(date) => date,
116            None => return Err(PersonnummerError::InvalidDate),
117        };
118
119        Ok(Personnummer {
120            date,
121            serial,
122            control,
123            divider,
124            coordination: (day > 31),
125        })
126    }
127}
128
129impl Personnummer {
130    /// Returns a new instance of a Personnummer. Panics for invalid dates but not for invalid
131    /// personal identity numbers. Use valid() to check validity.
132    pub fn new(pnr: &str) -> Result<Personnummer, PersonnummerError> {
133        Personnummer::try_from(pnr)
134    }
135
136    /// Same as new() but returns an Option instead of panicing on invalid dates.
137    pub fn parse(pnr: &str) -> Result<Personnummer, PersonnummerError> {
138        Personnummer::try_from(pnr)
139    }
140
141    /// Returns a FormattedPersonnummer from a Personnummer which can be used to display a
142    /// normalized version of the Personnummer.
143    pub fn format(&self) -> FormattedPersonnummer {
144        let day = self.date.day();
145        let day_or_coordination = if self.coordination {
146            day + COORDINATION_NUMBER
147        } else {
148            day
149        };
150
151        let long = format!(
152            "{}{:02}{:02}-{:03}{}",
153            self.date.year(),
154            self.date.month(),
155            day_or_coordination,
156            self.serial,
157            self.control
158        );
159
160        let short = String::from(&long[2..]);
161
162        FormattedPersonnummer { long, short }
163    }
164
165    /// Validate a Personnummer. The validation requires a valid date and that the Luhn checksum
166    /// matches the control digit.
167    pub fn valid(&self) -> bool {
168        let ymd = format!(
169            "{:02}{:02}{:02}",
170            self.date.year() % 100,
171            self.date.month(),
172            self.date.day()
173        );
174
175        let to_control = format!("{:06}{:03}", ymd, self.serial);
176
177        self.serial > 0 && luhn(to_control) == self.control
178    }
179
180    /// Return the age of the person holding the personal identity number. The dates used for the
181    /// person and the current date are naive dates.
182    pub fn get_age(&self) -> i32 {
183        let now = Utc::now();
184
185        if self.date.month() > now.month()
186            || self.date.month() == now.month() && self.date.day() > now.day()
187        {
188            now.year() - self.date.year() - 1
189        } else {
190            now.year() - self.date.year()
191        }
192    }
193
194    /// Check if the person holding the personal identity number is a female.
195    pub fn is_female(&self) -> bool {
196        (self.serial % 10) % 2 == 0
197    }
198
199    /// Check if the person holding the personal identity number is a male.
200    pub fn is_male(&self) -> bool {
201        !self.is_female()
202    }
203
204    /// Check if the personal identity number is a coordination number.
205    pub fn is_coordination_number(&self) -> bool {
206        self.coordination
207    }
208
209    /// Year of date of birth.
210    pub fn year(&self) -> i32 {
211        self.date.year()
212    }
213
214    /// Month of date of birth.
215    pub fn month(&self) -> u32 {
216        self.date.month()
217    }
218
219    /// Day of date of birth.
220    pub fn day(&self) -> u32 {
221        self.date.day()
222    }
223
224    /// Serial part of personal identity number.
225    pub fn serial(&self) -> u32 {
226        self.serial
227    }
228}
229
230/// Calculate the checksum based on luhn algorithm. See more information here:
231/// https://en.wikipedia.org/wiki/Luhn_algorithm.
232fn luhn(value: String) -> u8 {
233    let checksum = value
234        .chars()
235        .map(|c| c.to_digit(10).unwrap_or(0))
236        .enumerate()
237        .fold(0, |acc, (idx, v)| {
238            let value = if idx % 2 == 0 { v * 2 } else { v };
239            acc + if value > 9 { value - 9 } else { value }
240        });
241
242    (10 - (checksum as u8 % 10)) % 10
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248    use chrono::Duration;
249    use std::collections::HashMap;
250
251    #[test]
252    fn test_invalid_date() {
253        let cases = vec!["19901301-1111", "2017-02-29", "", "not-a-date"];
254
255        for tc in cases {
256            assert!(Personnummer::parse(tc).is_err());
257        }
258    }
259
260    #[test]
261    fn test_valid_date_invalid_digits() {
262        let cases = vec![
263            "19900101-1111",
264            "20160229-1111",
265            "6403273814",
266            "20150916-0006",
267        ];
268
269        for tc in cases {
270            assert!(!Personnummer::new(tc).unwrap().valid());
271        }
272    }
273
274    #[test]
275    fn test_valid_personal_identity_number() {
276        let cases = vec![
277            "19900101-0017",
278            "196408233234",
279            "000101-0107",
280            "510818-9167",
281            "19130401+2931",
282        ];
283
284        for tc in cases {
285            assert!(Personnummer::new(tc).unwrap().valid());
286        }
287    }
288
289    #[test]
290    fn test_age() {
291        let now = Utc::now();
292
293        let days_in_a_year = 365;
294        let leap_years_in_20_years = 20 / 4;
295        let twenty_years_ago = (days_in_a_year * 20) + leap_years_in_20_years;
296
297        let leap_years_in_100_years = 100 / 4;
298        let hundred_years_ago = (days_in_a_year * 100) + leap_years_in_100_years;
299
300        let twenty_tomorrow_date = (now - Duration::days(twenty_years_ago - 1)).date();
301        let twenty_tomorrow = format!(
302            "{}{:02}{:02}-1111",
303            twenty_tomorrow_date.year(),
304            twenty_tomorrow_date.month(),
305            twenty_tomorrow_date.day()
306        );
307
308        let twenty_yesterday_date = (now - Duration::days(twenty_years_ago + 1)).date();
309        let twenty_yesterday = format!(
310            "{}{:02}{:02}-1111",
311            twenty_yesterday_date.year(),
312            twenty_yesterday_date.month(),
313            twenty_yesterday_date.day()
314        );
315
316        let hundred_years_ago_date = (now - Duration::days(hundred_years_ago)).date();
317        let hundred_years_age = format!(
318            "{}{:02}{:02}-1111",
319            hundred_years_ago_date.year(),
320            hundred_years_ago_date.month(),
321            hundred_years_ago_date.day()
322        );
323
324        let mut cases: HashMap<&str, i32> = HashMap::new();
325
326        cases.insert(twenty_tomorrow.as_str(), 19);
327        cases.insert(twenty_yesterday.as_str(), 20);
328        cases.insert(hundred_years_age.as_str(), 100);
329
330        for (pnr, age) in cases {
331            assert_eq!(Personnummer::new(pnr).unwrap().get_age(), age);
332        }
333    }
334
335    #[test]
336    fn test_gender() {
337        let mut cases: HashMap<&str, bool> = HashMap::new();
338
339        cases.insert("19090903-6600", true);
340        cases.insert("19900101-0017", false);
341        cases.insert("800101-3294", false);
342        cases.insert("000903-6609", true);
343        cases.insert("800101+3294", false);
344
345        for (pnr, is_female) in cases {
346            let p = Personnummer::new(pnr).unwrap();
347
348            assert!(p.valid());
349            assert_eq!(p.is_female(), is_female);
350            assert_eq!(p.is_male(), !is_female);
351        }
352    }
353
354    #[test]
355    fn test_coordination() {
356        let mut cases: HashMap<&str, bool> = HashMap::new();
357
358        cases.insert("800161-3294", true);
359        cases.insert("800101-3294", false);
360        cases.insert("640327-3813", false);
361
362        for (pnr, is_coordination) in cases {
363            let p = Personnummer::new(pnr).unwrap();
364
365            assert!(p.valid());
366            assert_eq!(p.is_coordination_number(), is_coordination);
367        }
368    }
369}