Skip to content

Commit

Permalink
Add mexico validator
Browse files Browse the repository at this point in the history
  • Loading branch information
SLourenco committed Dec 23, 2024
1 parent 46d70f3 commit 4601752
Show file tree
Hide file tree
Showing 13 changed files with 272 additions and 19 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* Add Brazil validator
* Add Germany validator
* Add Mexico validator
* Add Albania validator

### 1.0.0

Expand Down
1 change: 1 addition & 0 deletions src/country.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ List of ISO country codes: https://en.wikipedia.org/wiki/List_of_ISO_3166_countr
**/
#[derive(EnumIter)]
pub enum Code {
AL,
BE,
BR,
CA,
Expand Down
253 changes: 253 additions & 0 deletions src/validator/albania.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
use crate::country::Code;
use crate::{validator, Citizen};
use chrono::NaiveDate;
use regex::Regex;

pub(crate) struct AlbaniaValidator;

/**
Albania National Identifier Number code validation.
TODO: Add official source with checksum calculation logic
Validation logic source: https://lookuptax.com/docs/tax-identification-number/albania-tax-id-guide
**/
impl validator::CountryValidator for AlbaniaValidator {
fn validate_id(&self, id: &str) -> bool {
let standard_id = self.sanitize_id(id);
if !Regex::new(r"^[A-T]\d[0156]\d{6}[A-W]$")
.unwrap()
.is_match(&standard_id)
{
return false;
}

let first_letter = standard_id[0..1].chars().nth(0).unwrap();

let year_or_birth = extract_year_of_birth(standard_id.to_string());
if year_or_birth.is_none() {
return false;
}

let gender = convert_gender(standard_id.to_string()).unwrap();
let month_of_birth = convert_month(&standard_id[2..4], gender);
let day_of_birth = standard_id[4..6].parse::<u32>().unwrap();

let date_of_birth =
NaiveDate::from_ymd_opt(year_or_birth.unwrap(), month_of_birth, day_of_birth);
if date_of_birth.is_none() {
return false;
}

let mut sum = 0;
standard_id[1..9]
.chars()
.enumerate()
.for_each(|(idx, c)| sum += (c.to_digit(10).unwrap() as usize) * (idx + 1));
sum += convert_letter_table(first_letter).unwrap() as usize;
let expected_check_letter = convert_number_table(sum % 23);

expected_check_letter.is_some()
&& expected_check_letter.unwrap() == standard_id[9..].chars().nth(0).unwrap()
}

fn country_code(&self) -> Code {
Code::AL
}

fn extract_citizen(&self, id: &str) -> Option<Citizen> {
if !self.validate_id(id) {
return None;
}

let standard_id = self.sanitize_id(id);
let g = convert_gender(standard_id.to_string()).unwrap();
Some(Citizen {
gender: g,
year_of_birth: extract_year_of_birth(standard_id.to_string()).unwrap(),
month_of_birth: Some(convert_month(&standard_id[2..4], g) as u8),
day_of_birth: Some(standard_id[4..6].parse::<u8>().unwrap()),
place_of_birth: None,
})
}
}

fn extract_year_of_birth(standard_id: String) -> Option<i32> {
let first_letter = standard_id[0..1].chars().nth(0).unwrap();

let birth_decade = match first_letter {
'A' => Some(1900),
'B' => Some(1910),
'C' => Some(1920),
'D' => Some(1930),
'E' => Some(1940),
'F' => Some(1950),
'G' => Some(1960),
'H' => Some(1970),
'I' => Some(1980),
'J' => Some(1990),
'K' => Some(2000),
'L' => Some(2010),
'M' => Some(2020),
'N' => Some(2030),
'O' => Some(2040),
'P' => Some(2050),
'Q' => Some(2060),
'R' => Some(2070),
'S' => Some(2080),
'T' => Some(2090),
_ => None,
};

let year_of_decade = standard_id[1..2].to_string().parse::<i32>();

if birth_decade.is_none() || year_of_decade.is_err() {
return None;
}

Some(birth_decade.unwrap() + year_of_decade.unwrap())
}

fn convert_gender(id: String) -> Option<char> {
let gender = id[2..3].chars().nth(0).unwrap();

if gender == '0' || gender == '1' {
return Some('M');
} else if gender == '5' || gender == '6' {
return Some('F');
}
None
}

fn convert_letter_table(letter: char) -> Option<u32> {
match letter {
'A' => Some(1),
'B' => Some(2),
'C' => Some(3),
'D' => Some(4),
'E' => Some(5),
'F' => Some(6),
'G' => Some(7),
'H' => Some(8),
'I' => Some(9),
'J' => Some(10),
'K' => Some(11),
'L' => Some(12),
'M' => Some(13),
'N' => Some(14),
'O' => Some(15),
'P' => Some(16),
'Q' => Some(17),
'R' => Some(18),
'S' => Some(19),
'T' => Some(20),
'U' => Some(21),
'V' => Some(22),
'W' => Some(0),
_ => None,
}
}

fn convert_number_table(n: usize) -> Option<char> {
match n {
1 => Some('A'),
2 => Some('B'),
3 => Some('C'),
4 => Some('D'),
5 => Some('E'),
6 => Some('F'),
7 => Some('G'),
8 => Some('H'),
9 => Some('I'),
10 => Some('J'),
11 => Some('K'),
12 => Some('L'),
13 => Some('M'),
14 => Some('N'),
15 => Some('O'),
16 => Some('P'),
17 => Some('Q'),
18 => Some('R'),
19 => Some('S'),
20 => Some('T'),
21 => Some('U'),
22 => Some('V'),
0 => Some('W'),
_ => None,
}
}

fn convert_month(month: &str, gender: char) -> u32 {
let m = month.parse::<u32>().unwrap();
if m < 40 && gender == 'F' {
return 0;
}
if gender == 'F' {
return m - 50;
}
m
}

#[cfg(test)]
mod tests {
use crate::validator::albania::AlbaniaValidator;
use crate::validator::CountryValidator;

#[test]
fn al_validation_requires_len_10() {
let validator = AlbaniaValidator;
assert_eq!(false, validator.validate_id("I05199Q"));
}

#[test]
fn al_validation_requires_gender() {
let validator = AlbaniaValidator;
assert_eq!(false, validator.validate_id("H73211672R"));
}

#[test]
fn al_validation_starts_with_valid_letter() {
let validator = AlbaniaValidator;
assert_eq!(false, validator.validate_id("Z71211672R"));
}

#[test]
fn al_validation_requires_valid_date_of_birth() {
let validator = AlbaniaValidator;
assert_eq!(false, validator.validate_id("H71311672R"));
}

#[test]
fn al_validation_invalid_ids() {
let validator = AlbaniaValidator;
assert_eq!(false, validator.validate_id("H71211672A"));
assert_eq!(false, validator.validate_id("I90201535M"));
}

#[test]
fn al_validation_valid_ids() {
let validator = AlbaniaValidator;
assert_eq!(true, validator.validate_id("I05101999I"));
assert_eq!(true, validator.validate_id("I90201535E"));
assert_eq!(true, validator.validate_id("J45423004V"));
assert_eq!(true, validator.validate_id("H71211672R"));
assert_eq!(true, validator.validate_id("I85413200A"));
}

#[test]
fn al_extract_valid_ids() {
let validator = AlbaniaValidator;
let c1 = validator.extract_citizen("I05101999I").unwrap();
assert_eq!('F', c1.gender);
assert_eq!(1980, c1.year_of_birth);
assert_eq!(1, c1.month_of_birth.unwrap());
assert_eq!(1, c1.day_of_birth.unwrap());

let c2 = validator.extract_citizen("H71211672R").unwrap();
assert_eq!('M', c2.gender);
assert_eq!(1977, c2.year_of_birth);
assert_eq!(12, c2.month_of_birth.unwrap());
assert_eq!(11, c2.day_of_birth.unwrap());
}
}
6 changes: 1 addition & 5 deletions src/validator/belgium.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,7 @@ pub(crate) struct BelgiumValidator;
**/
impl validator::CountryValidator for BelgiumValidator {
fn validate_id(&self, id: &str) -> bool {
let standard_id = id
.replace(" ", "")
.replace(".", "")
.replace("-", "")
.to_uppercase();
let standard_id = self.sanitize_id(id);
if standard_id.len() != 11 {
return false;
}
Expand Down
8 changes: 4 additions & 4 deletions src/validator/canada.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,21 @@ pub(crate) struct CanadaValidator;
**/
impl validator::CountryValidator for CanadaValidator {
fn validate_id(&self, id: &str) -> bool {
let standard_id = id.replace(" ", "").replace("-", "");
let standard_id = self.sanitize_id(id);

if standard_id.len() != 9 {
return false;
}

return algorithms::validate_luhn_10(&standard_id);
algorithms::validate_luhn_10(&standard_id)
}

fn country_code(&self) -> Code {
return crate::country::Code::CA;
Code::CA
}

fn extract_citizen(&self, _id: &str) -> Option<Citizen> {
return None;
None
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/validator/france.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub(crate) struct FranceValidator;
**/
impl validator::CountryValidator for FranceValidator {
fn validate_id(&self, id: &str) -> bool {
let standard_id = id.replace(" ", "");
let standard_id = self.sanitize_id(id);
if standard_id.len() != 15 {
return false;
}
Expand Down
2 changes: 1 addition & 1 deletion src/validator/germany.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ title: Steueridentifikationsnummer (IdNr) nach § 139b AO; Informationen zur Ber
**/
impl validator::CountryValidator for GermanyValidator {
fn validate_id(&self, id: &str) -> bool {
let standard_id = id.replace(" ", "").replace("-", "");
let standard_id = self.sanitize_id(id);

if standard_id.len() != 11 || &standard_id[0..1] == "0" {
return false;
Expand Down
4 changes: 2 additions & 2 deletions src/validator/italy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ pub(crate) struct ItalyValidator;
**/
impl validator::CountryValidator for ItalyValidator {
fn validate_id(&self, id: &str) -> bool {
if id.len() != 16 {
let standard_id = self.sanitize_id(id);
if standard_id.len() != 16 {
return false;
}

let standard_id = id.to_uppercase();
let mut is_odd = true;
let mut sum: u32 = 0;
for char in standard_id[0..15].chars() {
Expand Down
2 changes: 1 addition & 1 deletion src/validator/luxembourg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub(crate) struct LuxembourgValidator;
**/
impl validator::CountryValidator for LuxembourgValidator {
fn validate_id(&self, id: &str) -> bool {
let standard_id = id.replace(" ", "").replace("-", "");
let standard_id = self.sanitize_id(id);
if standard_id.len() != 13 {
return false;
}
Expand Down
6 changes: 4 additions & 2 deletions src/validator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub trait CountryValidator {
}
}

mod albania;
mod belgium;
mod brazil;
mod canada;
Expand All @@ -38,7 +39,7 @@ mod usa;
mod words;

pub fn get_validator(country: &country::Code) -> Box<dyn CountryValidator> {
return match country {
match country {
country::Code::BE => Box::new(belgium::BelgiumValidator),
country::Code::BR => Box::new(brazil::BrazilValidator),
country::Code::CA => Box::new(canada::CanadaValidator),
Expand All @@ -51,7 +52,8 @@ pub fn get_validator(country: &country::Code) -> Box<dyn CountryValidator> {
country::Code::US => Box::new(usa::UsaValidator),
country::Code::DE => Box::new(germany::GermanyValidator),
country::Code::MX => Box::new(mexico::MexicoValidator),
};
country::Code::AL => Box::new(albania::AlbaniaValidator),
}
}

#[cfg(test)]
Expand Down
2 changes: 1 addition & 1 deletion src/validator/portugal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub(crate) struct PortugalValidator;
**/
impl validator::CountryValidator for PortugalValidator {
fn validate_id(&self, id: &str) -> bool {
let standard_id = id.replace(" ", "").to_uppercase();
let standard_id = self.sanitize_id(id);
if standard_id.len() != 12 {
return false;
}
Expand Down
2 changes: 1 addition & 1 deletion src/validator/spain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const CONTROL_DIGIT: &str = "TRWAGMYFPDXBNJZSQVHLCKE";
**/
impl validator::CountryValidator for SpainValidator {
fn validate_id(&self, id: &str) -> bool {
let standard_id = id.replace("-", "").to_uppercase();
let standard_id = self.sanitize_id(id);

if standard_id.len() != 9 {
return false;
Expand Down
Loading

0 comments on commit 4601752

Please sign in to comment.