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
25const 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)]
46pub struct Personnummer {
48 date: chrono::NaiveDate,
49 serial: u32,
50 control: u8,
51 divider: char,
52 coordination: bool,
53}
54
55pub struct FormattedPersonnummer {
59 long: String,
60 short: String,
61}
62
63impl FormattedPersonnummer {
64 pub fn long(&self) -> String {
66 self.long.clone()
67 }
68
69 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 pub fn new(pnr: &str) -> Result<Personnummer, PersonnummerError> {
133 Personnummer::try_from(pnr)
134 }
135
136 pub fn parse(pnr: &str) -> Result<Personnummer, PersonnummerError> {
138 Personnummer::try_from(pnr)
139 }
140
141 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 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 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 pub fn is_female(&self) -> bool {
196 (self.serial % 10) % 2 == 0
197 }
198
199 pub fn is_male(&self) -> bool {
201 !self.is_female()
202 }
203
204 pub fn is_coordination_number(&self) -> bool {
206 self.coordination
207 }
208
209 pub fn year(&self) -> i32 {
211 self.date.year()
212 }
213
214 pub fn month(&self) -> u32 {
216 self.date.month()
217 }
218
219 pub fn day(&self) -> u32 {
221 self.date.day()
222 }
223
224 pub fn serial(&self) -> u32 {
226 self.serial
227 }
228}
229
230fn 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}