pesel_rs/
lib.rs

1//! [PESEL](https://en.wikipedia.org/wiki/PESEL) validation and detail extraction with multiple data layout implementations.
2//!
3//! [![Crates.io Version](https://img.shields.io/crates/v/pesel-rs?color=green)](https://crates.io/crates/pesel-rs)
4//! [![Static Badge](https://img.shields.io/badge/docs-orange)](https://docs.rs/pesel-rs/latest/pesel_rs/)
5//! [![Crates.io License](https://img.shields.io/crates/l/pesel-rs)](https://crates.io/crates/pesel-rs)
6//!
7//! # Definitions
8//!
9//! PESEL: `YYMMDDOOOOC`
10//!
11//! - `YY` - Last two digits of year of birth
12//! - `MM` - Month of birth (shifted depending on year of birth as shown by the table below)
13//!
14//! | Year        | 1800 - 1899 | 1900 - 1999 | 2000 - 2099 | 2100 - 2199 | 2200 - 2299 |
15//! |-------------|-------------|-------------|-------------|-------------|-------------|
16//! | Month shift | +80         | 0           | +20         | +40         | +60         |
17//!
18//! - `DD` - Day of birth
19//! - `OOOO` - Ordinal number, where the last digit denotes the gender ([0, 2, 4, 6, 8] = female, [1,
20//!   3, 5, 7, 9] = male)
21//! - `C` - Control number
22//!
23//! # Usage
24//!
25//! There are two PESEL structs provided by the crate, both implementing the [`PeselTrait`].
26//!
27//! - [`crate::bit_fields::Pesel`] - Stores each section of the PESEL in the following layout:
28//!   `7 bits | YY | 5 bits | MM | 5 bits | DD | 5 bits | OOOO | 5 bits | C`, where in between bits
29//!   are unused. Extracting each field is done using bitwise operations. You can get the human
30//!   readable number using `u64::from`.
31//!
32//! - [`crate::human_redable::Pesel`] - Stores the PESEL as a plain number, extracting each field
33//!   requires modulo and division operations, if often accessing individual fields is important to
34//!   you, you should probably use [`crate::bit_fields::Pesel`].
35//!
36//! If you just need to validate a number or extract a specific section without using the structs,
37//! you could use functions in the lib root. Most of these functions won't check if the value
38//! they're returning is valid, unlike the structs who are guaranteed to always return a valid
39//! value.
40//!
41//! # Examples
42//!
43//! Function that takes a name and welcomes the person based on date of birth and gender from the
44//! PESEL. Implemented using [`crate::bit_fields::Pesel`] because we're mostly reading the fields.
45//!
46//! ```rust
47//! use pesel_rs::{prelude::*, bit_fields::Pesel};
48//!
49//! fn welcome(first_name: &str, pesel: u64) {
50//!     match Pesel::try_from(pesel) {
51//!         Ok(pesel) => {
52//!             if pesel.date_of_birth() > NaiveDate::from_ymd_opt(2015, 1, 1).unwrap() {
53//!                 let gender = if pesel.gender() == Gender::Male { "boy" } else { "girl" };
54//!                 println!("Wow {first_name}! You're such a young {gender}!");
55//!             } else {
56//!                 println!("{first_name}, you're very old, I'm sorry 😞");
57//!             }
58//!         }
59//!         Err(_) => println!("Huh, what you gave me doesn't seem to be a valid pesel {first_name}..."),
60//!     }
61//! }
62//! ```
63//!
64//! Function finding a pesel with the oldest date of birth. Working with a generic PESEL, we
65//! introduce additional bounds (required by [`PeselTrait`]).
66//! ```rust
67//! use pesel_rs::prelude::*;
68//!
69//! fn oldest<T: PeselTrait>(pesels: &[T])
70//! where
71//!     u64: From<T>,
72//!     for<'a> u64: From<&'a T>
73//! {
74//!     assert!(pesels.len() > 0);
75//!
76//!     let mut oldest_index = 0;
77//!     pesels.iter().skip(1).enumerate().for_each(|(i, pesel)| {
78//!         if pesels[oldest_index].date_of_birth() < pesel.date_of_birth() {
79//!             oldest_index = i;
80//!         }
81//!     });
82//!
83//!     let date_of_birth = pesels[oldest_index].date_of_birth();
84//!     println!("PESEL nr. {oldest_index} is the oldest! Born at {date_of_birth}")
85//! }
86//! ```
87
88pub mod bit_fields;
89pub mod human_redable;
90
91pub use chrono;
92#[cfg(feature = "serde")]
93pub use serde;
94pub use thiserror;
95
96pub mod prelude {
97    pub use crate::{validate, Gender, PeselTrait};
98    pub use chrono::NaiveDate;
99}
100
101use chrono::NaiveDate;
102use thiserror::Error;
103
104#[derive(Debug, Clone, PartialEq, Eq)]
105#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
106pub enum Gender {
107    Male,
108    Female,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Error)]
112pub enum ValidationError {
113    #[error("Pesel is too short.")]
114    TooShort(usize),
115    #[error("Pesel is too long.")]
116    TooLong(usize),
117    #[error("Pesel has an invalid date of birth.")]
118    BirthDate,
119    #[error("Pesel has an invalid control digit.")]
120    ControlDigit,
121}
122
123const PESEL_WEIGHTS: [u8; 11] = [1, 3, 7, 9, 1, 3, 7, 9, 1, 3, 1];
124
125#[cfg(feature = "serde")]
126#[cfg_attr(feature = "serde", macro_export)]
127macro_rules! impl_pesel_visitor {
128    ($name:ident) => {
129        pub struct PeselVisitor;
130
131        impl<'de> serde::de::Visitor<'de> for PeselVisitor {
132            type Value = $name;
133
134            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
135                write!(formatter, "a valid PESEL as u64, &str, &'de str, or String")
136            }
137
138            fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
139            where
140                E: serde::de::Error,
141            {
142                $name::try_from(v).map_err(|err| serde::de::Error::custom(err.to_string()))
143            }
144
145            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
146            where
147                E: serde::de::Error,
148            {
149                $name::try_from(v).map_err(|err| serde::de::Error::custom(err.to_string()))
150            }
151
152            fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
153            where
154                E: serde::de::Error,
155            {
156                $name::try_from(v).map_err(|err| serde::de::Error::custom(err.to_string()))
157            }
158
159            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
160            where
161                E: serde::de::Error,
162            {
163                $name::try_from(v).map_err(|err| serde::de::Error::custom(err.to_string()))
164            }
165        }
166    };
167}
168#[cfg(feature = "serde")]
169#[cfg_attr(feature = "serde", macro_export)]
170macro_rules! impl_pesel_deserializer {
171    ($name:ident) => {
172        impl_pesel_visitor!($name);
173
174        impl<'de> serde::Deserialize<'de> for $name {
175            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
176            where
177                D: serde::Deserializer<'de>,
178            {
179                deserializer.deserialize_any(PeselVisitor)
180            }
181        }
182    };
183}
184
185/// # Errors
186/// Returns `None` if:
187/// - `month_section` is not in range of `<1,92>`
188pub const fn month_from_section(month_section: u8) -> Option<u8> {
189    if !(1 <= month_section && month_section <= 92) {
190        return None;
191    }
192
193    Some(month_section - (((month_section / 10) / 2) * 20))
194}
195
196/// # Errors
197/// Returns `None` if:
198/// - `month` is not in range of `<1,12>`
199/// - `year` is not in range of `<1800,2299>`
200pub const fn month_to_section(month: u8, year: u16) -> Option<u8> {
201    if !(1 <= month && month <= 12) {
202        return None;
203    }
204    if !(1800 <= year && year <= 2299) {
205        return None;
206    }
207
208    // TODO: Find a better conversion method
209    let base = ((year / 100) - 10) as u8;
210    let shift = match base {
211        8 => 80,
212        9 => 0,
213        base => (base + 1) * 20,
214    };
215
216    Some(month + shift)
217}
218
219pub const fn year_from_sections(month_section: u8, year_section: u8) -> u16 {
220    let shift = ((month_section / 10) / 2) * 2;
221
222    (match shift {
223        8 => 1800,
224        shift => 1900 + (shift as u16) * 50,
225    } + year_section as u16)
226}
227
228/// Trait for implementing a [PESEL](https://en.wikipedia.org/wiki/PESEL).
229///
230/// It's required for a PESEL to implement [`TryFrom<u64>`] and [`Into<u64>`] (for `Self` and `&Self`)
231/// where the [`u64`] PESEL must be represented as a human readable number.
232///
233/// The only required methods are for extracting each section. The rest is computed based on that.
234pub trait PeselTrait: TryFrom<u64> + Into<u64>
235where
236    u64: From<Self>,
237    for<'a> u64: From<&'a Self>,
238{
239    /// Day of birth section.
240    fn day_section(&self) -> u8;
241
242    /// Month of birth section.
243    fn month_section(&self) -> u8;
244
245    /// Year of birth section.
246    fn year_section(&self) -> u8;
247
248    /// Ordinal section.
249    fn ordinal_section(&self) -> u16;
250
251    /// Control section.
252    fn control_section(&self) -> u8;
253
254    /// Day of birth.
255    fn day(&self) -> u8 {
256        self.day_section()
257    }
258
259    /// Month of birth.
260    fn month(&self) -> u8 {
261        match month(self) {
262            Some(month) => month,
263            None => unreachable!(),
264        }
265    }
266
267    /// Year of birth.
268    fn year(&self) -> u16 {
269        year(self)
270    }
271
272    /// Date of birth.
273    fn date_of_birth(&self) -> NaiveDate {
274        match date_of_birth(self) {
275            Some(date_of_birth) => date_of_birth,
276            None => unreachable!(),
277        }
278    }
279
280    /// Gender.
281    fn gender(&self) -> Gender {
282        gender(self)
283    }
284}
285
286/// Extract the day of birth section.
287pub fn day_section(pesel: impl Into<u64>) -> u8 {
288    ((pesel.into() % 10_000_000) / 100_000) as u8
289}
290
291/// Extract the month of birth section.
292pub fn month_section(pesel: impl Into<u64>) -> u8 {
293    ((pesel.into() % 1_000_000_000) / 10_000_000) as u8
294}
295
296/// Extract the year of birth section.
297pub fn year_section(pesel: impl Into<u64>) -> u8 {
298    ((pesel.into() % 100_000_000_000) / 1_000_000_000) as u8
299}
300
301/// Extract the ordinal section.
302pub fn ordinal_section(pesel: impl Into<u64>) -> u16 {
303    ((pesel.into() % 100_000) / 10) as u16
304}
305
306/// Extract the control section.
307pub fn control_section(pesel: impl Into<u64>) -> u8 {
308    (pesel.into() % 10) as u8
309}
310
311/// Extract day of birth.
312pub fn day(pesel: impl Into<u64>) -> u8 {
313    day_section(pesel)
314}
315
316/// Extract month of birth.
317///
318/// # Errors
319/// Returns `None` if:
320/// - `month_section` is not in range of `<1,92>`
321pub fn month(pesel: impl Into<u64>) -> Option<u8> {
322    month_from_section(month_section(pesel))
323}
324
325/// Extract year of birth.
326pub fn year(pesel: impl Into<u64>) -> u16 {
327    let pesel = pesel.into();
328    year_from_sections(month_section(pesel), year_section(pesel))
329}
330
331/// Extract date of birth.
332pub fn date_of_birth(pesel: impl Into<u64>) -> Option<NaiveDate> {
333    let pesel = pesel.into();
334    NaiveDate::from_ymd_opt(
335        year(pesel) as i32,
336        match month(pesel) {
337            Some(month) => month as u32,
338            None => return None,
339        },
340        day(pesel) as u32,
341    )
342}
343
344/// Extract gender.
345pub fn gender(pesel: impl Into<u64>) -> Gender {
346    if ordinal_section(pesel) % 2 == 0 {
347        Gender::Female
348    } else {
349        Gender::Male
350    }
351}
352
353/// Check if the PESEL is valid.
354pub fn validate(pesel: impl Into<u64>) -> Result<(), ValidationError> {
355    let pesel = pesel.into();
356    let mut pesel_str = pesel.to_string();
357
358    if pesel_str.len() < 8 {
359        return Err(ValidationError::TooShort(pesel_str.len()));
360    }
361
362    if pesel_str.len() > 11 {
363        return Err(ValidationError::TooLong(pesel_str.len()));
364    }
365
366    if pesel_str.len() < 11 {
367        let mut new_value_str = "0".to_string();
368
369        for _ in (pesel_str.len() + 1)..11 {
370            new_value_str.push('0');
371        }
372
373        pesel_str = new_value_str + &pesel_str;
374    }
375
376    if date_of_birth(pesel).is_none() {
377        return Err(ValidationError::BirthDate);
378    }
379
380    let mut sum = 0;
381    for (i, digit) in pesel_str
382        .chars()
383        .take(11)
384        .map(|char| char.to_digit(10).unwrap())
385        .enumerate()
386    {
387        sum += (digit as u8) * PESEL_WEIGHTS[i];
388    }
389
390    if let Some(Some(last_digit)) = sum.to_string().chars().last().map(|char| char.to_digit(10)) {
391        if last_digit != 0 {
392            return Err(ValidationError::ControlDigit);
393        }
394        Ok(())
395    } else {
396        Err(ValidationError::ControlDigit)
397    }
398}
399
400#[derive(Debug, Clone, PartialEq, Eq, Error)]
401#[error("{0}")]
402pub enum PeselTryFromError<T> {
403    ValidationError(#[from] ValidationError),
404    Other(T),
405}
406
407#[macro_export]
408macro_rules! impl_try_from_str_for_pesel {
409    ($name:ident) => {
410        impl TryFrom<&str> for $name {
411            type Error = PeselTryFromError<std::num::ParseIntError>;
412
413            fn try_from(value: &str) -> Result<Self, Self::Error> {
414                let value = u64::from_str_radix(value, 10).map_err(PeselTryFromError::Other)?;
415                validate(value)?;
416                Self::try_from(value).map_err(PeselTryFromError::ValidationError)
417            }
418        }
419
420        impl TryFrom<&String> for $name {
421            type Error = PeselTryFromError<std::num::ParseIntError>;
422
423            fn try_from(value: &String) -> Result<Self, Self::Error> {
424                Self::try_from(value.as_str())
425            }
426        }
427
428        impl TryFrom<String> for $name {
429            type Error = PeselTryFromError<std::num::ParseIntError>;
430
431            fn try_from(value: String) -> Result<Self, Self::Error> {
432                Self::try_from(&value)
433            }
434        }
435    };
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441
442    static PESEL1: u64 = 02290486168;
443    static PESEL2: u64 = 01302534699;
444    static PESEL3: u64 = 00010128545;
445    static PESEL4: u64 = 98250993285;
446    static PESEL5: u64 = 60032417874;
447
448    #[test]
449    fn day_section() {
450        assert_eq!(super::day_section(PESEL1), 04);
451        assert_eq!(super::day_section(PESEL2), 25);
452        assert_eq!(super::day_section(PESEL3), 01);
453        assert_eq!(super::day_section(PESEL4), 09);
454        assert_eq!(super::day_section(PESEL5), 24);
455    }
456
457    #[test]
458    fn month_section() {
459        assert_eq!(super::month_section(PESEL1), 29);
460        assert_eq!(super::month_section(PESEL2), 30);
461        assert_eq!(super::month_section(PESEL3), 01);
462        assert_eq!(super::month_section(PESEL4), 25);
463        assert_eq!(super::month_section(PESEL5), 03);
464    }
465
466    #[test]
467    fn year_section() {
468        assert_eq!(super::year_section(PESEL1), 02);
469        assert_eq!(super::year_section(PESEL2), 01);
470        assert_eq!(super::year_section(PESEL3), 00);
471        assert_eq!(super::year_section(PESEL4), 98);
472        assert_eq!(super::year_section(PESEL5), 60);
473    }
474
475    #[test]
476    fn ordinal_section() {
477        assert_eq!(super::ordinal_section(PESEL1), 8616);
478        assert_eq!(super::ordinal_section(PESEL2), 3469);
479        assert_eq!(super::ordinal_section(PESEL3), 2854);
480        assert_eq!(super::ordinal_section(PESEL4), 9328);
481        assert_eq!(super::ordinal_section(PESEL5), 1787);
482    }
483
484    #[test]
485    fn control_section() {
486        assert_eq!(super::control_section(PESEL1), 8);
487        assert_eq!(super::control_section(PESEL2), 9);
488        assert_eq!(super::control_section(PESEL3), 5);
489        assert_eq!(super::control_section(PESEL4), 5);
490        assert_eq!(super::control_section(PESEL5), 4);
491    }
492
493    #[test]
494    fn day() {
495        assert_eq!(super::day(PESEL1), 04);
496        assert_eq!(super::day(PESEL2), 25);
497        assert_eq!(super::day(PESEL3), 01);
498        assert_eq!(super::day(PESEL4), 09);
499        assert_eq!(super::day(PESEL5), 24);
500    }
501
502    #[test]
503    fn month() {
504        assert_eq!(super::month(PESEL1), Some(09));
505        assert_eq!(super::month(PESEL2), Some(10));
506        assert_eq!(super::month(PESEL3), Some(01));
507        assert_eq!(super::month(PESEL4), Some(05));
508        assert_eq!(super::month(PESEL5), Some(03));
509    }
510
511    #[test]
512    fn invalid_month() {
513        assert_eq!(super::month(02990486168u64), None);
514        assert_eq!(super::month(02970486168u64), None);
515        assert_eq!(super::month(02930486168u64), None);
516    }
517
518    #[test]
519    fn year() {
520        assert_eq!(super::year(PESEL1), 2002);
521        assert_eq!(super::year(PESEL2), 2001);
522        assert_eq!(super::year(PESEL3), 1900);
523        assert_eq!(super::year(PESEL4), 2098);
524        assert_eq!(super::year(PESEL5), 1960);
525    }
526
527    #[test]
528    fn date_of_birth() {
529        assert_eq!(
530            super::date_of_birth(PESEL1),
531            NaiveDate::from_ymd_opt(2002, 09, 04)
532        );
533        assert_eq!(
534            super::date_of_birth(PESEL2),
535            NaiveDate::from_ymd_opt(2001, 10, 25)
536        );
537        assert_eq!(
538            super::date_of_birth(PESEL3),
539            NaiveDate::from_ymd_opt(1900, 01, 01)
540        );
541        assert_eq!(
542            super::date_of_birth(PESEL4),
543            NaiveDate::from_ymd_opt(2098, 05, 09)
544        );
545        assert_eq!(
546            super::date_of_birth(PESEL5),
547            NaiveDate::from_ymd_opt(1960, 03, 24)
548        );
549    }
550
551    #[test]
552    fn gender() {
553        assert_eq!(super::gender(PESEL1), Gender::Female);
554        assert_eq!(super::gender(PESEL2), Gender::Male);
555        assert_eq!(super::gender(PESEL3), Gender::Female);
556        assert_eq!(super::gender(PESEL4), Gender::Female);
557        assert_eq!(super::gender(PESEL5), Gender::Male);
558    }
559
560    #[test]
561    fn validate() {
562        assert_eq!(super::validate(PESEL1), Ok(()));
563        assert_eq!(super::validate(PESEL2), Ok(()));
564        assert_eq!(super::validate(PESEL3), Ok(()));
565        assert_eq!(super::validate(PESEL4), Ok(()));
566        assert_eq!(super::validate(PESEL5), Ok(()));
567    }
568
569    #[test]
570    fn invalid_pesels() {
571        assert_eq!(super::validate(4355u64), Err(ValidationError::TooShort(4)));
572        assert_eq!(
573            super::validate(435585930294485u64),
574            Err(ValidationError::TooLong(15))
575        );
576        assert_eq!(
577            super::validate(99990486167u64),
578            Err(ValidationError::BirthDate)
579        );
580        assert_eq!(
581            super::validate(02290486167u64),
582            Err(ValidationError::ControlDigit)
583        );
584    }
585}