diff --git a/README.md b/README.md index d022e86..652988d 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Brado é um pacote Rust para validação de documentos brasileiros. Este projeto é inspirado na biblioteca Python [validate-docbr](https://github.com/alvarofpp/validate-docbr). -Brado fornece funções para identificação, validação e geração de documentos brasileiros. O nome desta biblioteca (Brado) é um acronimo de BRAzilian DOcs validator (validador de DOcumentos BRAsileiros). +Brado fornece funções para identificação, validação e geração de documentos brasileiros. O nome desta biblioteca (Brado) é um acrônimo de BRAzilian DOcs validator (validador de DOcumentos BRAsileiros). > :warning: A documentação desta biblioteca pode ser acessada [aqui](https://docs.rs/brado/). @@ -29,7 +29,7 @@ cargo add brado Ou adicionar a linha a seguir no arquivo `Cargo.toml`: ```toml -brado = "0.3.1" +brado = "0.4.0" ``` @@ -38,7 +38,7 @@ brado = "0.3.1" - [x] CPF: Cadastro de Pessoa Física; - [x] CNH: Carteira Nacional de Habilitação; - [x] CNPJ: Cadastro Nacional da Pessoa Jurídica; -- [ ] CNS: Cartão Nacional de Saúde; +- [x] CNS: Cartão Nacional de Saúde; - [ ] PIS: PIS/NIS/PASEP/NIT; - [ ] Título eleitoral: Cadastro que permite cidadãos brasileiros votar; - [ ] RENAVAM: Registro Nacional de Veículos Automotores; @@ -65,7 +65,7 @@ cpf::validate("639.292.470-10"); // false ### mask -Mascara o documento passado como parâmetro (`&str`). Retorna uma string (`Result`) correspondente ao documento mascarado ou um erro. A string passada não deve possuir símbolos. +Mascara o documento passado como parâmetro (`&str`), apenas se não possuir símbolos e tiver o número de caracteres do documento sem símbolos. Retorna uma string (`Result`) correspondente ao documento mascarado ou um erro. ```rust use brado::cpf; @@ -83,12 +83,12 @@ Verifica se o documento passado como parâmetro (`&str`) não possui símbolos. ```rust use brado::cpf; -cpf::is_bare("63929247011"); // true -cpf::is_bare("63929247010"); // true +cpf::is_bare("63929247011"); // true (CPF válido sem máscara) +cpf::is_bare("63929247010"); // true (CPF inválido sem máscara) -cpf::is_bare("639.292.470-11"); // false -cpf::is_bare("639.29247011"); // false -cpf::is_bare("639292470110"); // false +cpf::is_bare("639.292.470-11"); // false (CPF válido com máscara) +cpf::is_bare("639.29247011"); // false (CPF válido mascarado errado) +cpf::is_bare("639292470110"); // false (CPF inválido sem máscara) ``` > OBS: se for utilizada a função `cpf::is_bare` para verificar se um CNPJ não possui símbolos, o resultado será `false`! Isso acontece pois esta função considera que a string é um CPF, ou seja, possui 11 dígitos. @@ -100,11 +100,12 @@ Verifica se o documento passado como parâmetro (`&str`) está mascarado de acor ```rust use brado::cpf; -cpf::is_masked("639.292.470-10"); // true +cpf::is_masked("639.292.470-11"); // true (CPF válido com máscara) +cpf::is_masked("639.292.470-10"); // true (CPF inválido com máscara) -cpf::is_masked("63929247011"); // false -cpf::is_masked("6392.92.470-11"); // false -cpf::is_masked("639.292.470-110"); // false +cpf::is_masked("63929247011"); // false (CPF válido sem máscara) +cpf::is_masked("6392.92.470-11"); // false (CPF válido mascarado errado) +cpf::is_masked("639.292.470-110"); // false (CPF inválido com máscara) ``` > OBS: `cpf::is_masked` verifica se a string passada está mascarada como um CPF. `cnpj::is_masked` verifica se a string passada está mascarada como um CNPJ. @@ -116,7 +117,7 @@ Gera um novo documento sem símbolos (`String`). ```rust use brado::cpf; -cpf::generate(); // "639.292.470-11" +cpf::generate(); // "63929247011" ``` ### generate_masked @@ -129,6 +130,17 @@ use brado::cpf; cpf::generate_masked(); // "639.292.470-11" ``` +### docs::is_cpf, docs::is_cnpj, docs::is_cnh, docs::is_cns + +São funções que verificam se o documento passado como parâmetro (`&str`) são, respectivamente, CPF, CNPJ, CNH e CNS válidos. Essas funções são atalhos (apelidos) para as funções de validação de cada documento. São indicadas para o contexto de identificação do tipo do documento. + +```rust +use brado::docs; + +docs::is_cpf("639.292.470-11"); // true +docs::is_cnpj("639.292.470-11"); // false +``` + # Como Contribuir diff --git a/brado/Cargo.toml b/brado/Cargo.toml index 10ce327..dd736ce 100644 --- a/brado/Cargo.toml +++ b/brado/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "brado" -version = "0.3.1" +version = "0.4.0" edition = "2021" license = "MIT" readme = "../README.md" diff --git a/brado/src/cnh.rs b/brado/src/cnh.rs index 1113381..f86ea02 100644 --- a/brado/src/cnh.rs +++ b/brado/src/cnh.rs @@ -5,12 +5,12 @@ use crate::common::{ }; /// Realiza validação de CNH, máscarado ou não. -/// Retorna `true` se o argumento `doc` for uma CNH válido, +/// Retorna `true` se o argumento `doc` for uma CNH válida, /// caso contrário, retorna `false`. /// /// ## Exemplos /// -/// CNHs válidos: +/// CNHs válidas: /// ``` /// use brado::cnh; /// @@ -21,7 +21,7 @@ use crate::common::{ /// assert!(result); /// ``` /// -/// CNHs inválidos: +/// CNHs inválidas: /// ``` /// use brado::cnh; /// @@ -99,12 +99,12 @@ fn generate_second_digit( second as u16 } -/// Verifica se o argumento `doc` pode ser um CNH sem símbolos. +/// Verifica se o argumento `doc` pode ser uma CNH sem símbolos. /// Se for, retorna `true`, caso contrário, retorna `false`. /// /// ## Exemplos /// -/// CNHs válidos: +/// CNHs válidas: /// ``` /// use brado::cnh; /// @@ -115,7 +115,7 @@ fn generate_second_digit( /// assert!(!result); /// ``` /// -/// CNHs inválidos: +/// CNHs inválidas: /// ``` /// use brado::cnh; /// @@ -126,12 +126,12 @@ pub fn is_bare(doc: &str) -> bool { doc.chars().count() == 11 && get_digits(doc).len() == 11 } -/// Verifica se o argumento `doc` pode ser um CNH com símbolos. +/// Verifica se o argumento `doc` pode ser uma CNH com símbolos. /// Se for, retorna `true`, caso contrário, retorna `false`. /// /// ## Exemplos /// -/// CNHs válidos: +/// CNHs válidas: /// ``` /// use brado::cnh; /// @@ -142,7 +142,7 @@ pub fn is_bare(doc: &str) -> bool { /// assert!(!result); /// ``` /// -/// CNHs inválidos: +/// CNHs inválidas: /// ``` /// use brado::cnh; /// @@ -202,7 +202,7 @@ pub fn mask(doc: &str) -> Result { Ok(masked_doc) } -/// Gera e retorna um CNH aleatório sem máscara. +/// Gera e retorna uma CNH aleatório sem máscara. /// /// ## Exemplo /// ``` @@ -224,7 +224,7 @@ pub fn generate() -> String { .join("") } -/// Gera e retorna um CNH aleatório com máscara. +/// Gera e retorna uma CNH aleatório com máscara. /// /// ## Exemplo /// ``` diff --git a/brado/src/cns.rs b/brado/src/cns.rs new file mode 100644 index 0000000..428bda5 --- /dev/null +++ b/brado/src/cns.rs @@ -0,0 +1,302 @@ +//! Utilitários para validação de CNS. + +use crate::common::{ + get_digits, get_symbols, random_digit_from_vector, random_digit_vector, +}; + +/// Realiza validação de CNS, máscarado ou não. +/// Retorna `true` se o argumento `doc` for um CNS válido, +/// caso contrário, retorna `false`. +/// +/// ## Exemplos +/// +/// CNSs válidos: +/// ``` +/// use brado::cns; +/// +/// let result = cns::validate("144082627260004"); // true +/// assert!(result); +/// +/// let result = cns::validate("144 0826 2726 0004"); // true +/// assert!(result); +/// ``` +/// +/// CNSs inválidos: +/// ``` +/// use brado::cns; +/// +/// let result = cns::validate("144082627260005"); // false +/// assert!(!result); +/// +/// let result = cns::validate("144 0826 2726 0005"); // false +/// assert!(!result); +/// ``` +pub fn validate(doc: &str) -> bool { + let size: usize = doc.chars().count(); + + if size != 15 && !is_masked(doc) { + return false; + } + + let digits: Vec = get_digits(doc); + + if digits.len() != 15 || is_first_digit_invalid(&digits[0]) { + return false; + } + + validate_checksum(&digits) +} + +fn valid_first_digits() -> Vec { + vec![1, 2, 7, 8, 9] +} + +fn is_first_digit_valid(digit: &u16) -> bool { + valid_first_digits().contains(digit) +} + +fn is_first_digit_invalid(digit: &u16) -> bool { + !is_first_digit_valid(digit) +} + +fn validate_checksum(doc_slice: &[u16]) -> bool { + if vec![1, 2].contains(&doc_slice[0]) { + let check_digits = generate_last_four_digits(&doc_slice[..11]); + + &doc_slice[11..] == check_digits + } else { + let checksum = cns_sum(&doc_slice, 15); + + checksum % 11 == 0 + } +} + +fn cns_sum( + doc_slice: &[u16], + size: u16, +) -> u16 { + let mut sum: u16 = 0; + + for i in 0..size { + sum += doc_slice[i as usize] * (15 - i); + } + + sum +} + +fn generate_last_four_digits(doc_slice: &[u16]) -> Vec { + let mut checksum = cns_sum(&doc_slice, doc_slice.len() as u16); + + let mut check_digit = 11 - (checksum % 11); + + if check_digit == 11 { + check_digit = 0; + } + + let check_digits: Vec = { + if check_digit == 10 { + checksum += 2; + check_digit = 11 - (checksum % 11); + vec![0, 0, 1, check_digit] + } else { + vec![0, 0, 0, check_digit] + } + }; + + check_digits +} + +/// Verifica se o argumento `doc` pode ser um CNS sem símbolos. +/// Se for, retorna `true`, caso contrário, retorna `false`. +/// +/// ## Exemplos +/// +/// CNSs válidos: +/// ``` +/// use brado::cns; +/// +/// let result = cns::is_bare("144082627260004"); // true +/// assert!(result); +/// +/// let result = cns::is_bare("144 0826 2726 0004"); // false +/// assert!(!result); +/// ``` +/// +/// CNSs inválidos: +/// ``` +/// use brado::cns; +/// +/// let result = cns::is_bare("144082627260005"); // true +/// assert!(result); +/// ``` +pub fn is_bare(doc: &str) -> bool { + doc.chars().count() == 15 && get_digits(doc).len() == 15 +} + +/// Verifica se o argumento `doc` pode ser um CNS com símbolos. +/// Se for, retorna `true`, caso contrário, retorna `false`. +/// +/// ## Exemplos +/// +/// CNSs válidos: +/// ``` +/// use brado::cns; +/// +/// let result = cns::is_masked("144 0826 2726 0004"); // true +/// assert!(result); +/// +/// let result = cns::is_masked("144082627260004"); // false +/// assert!(!result); +/// ``` +/// +/// CNSs inválidos: +/// ``` +/// use brado::cns; +/// +/// let result = cns::is_masked("144 0826 2726 0005"); // true +/// assert!(result); +/// ``` +pub fn is_masked(doc: &str) -> bool { + let symbols: Vec<(usize, char)> = get_symbols(doc); + let digits: Vec = get_digits(doc); + + if symbols.len() != 3 || digits.len() != 15 { + return false; + } + + symbols[0] == (3, ' ') && symbols[1] == (8, ' ') && symbols[2] == (13, ' ') +} + +/// Aplica máscara de CNS no argumento `doc` e retorna resultado. +/// O argumento deve ser uma string sem símbolos, caso contrário, +/// deve lançar erro. +/// +/// ## Exemplos +/// +/// Documento de 11 dígitos sem máscara: +/// ``` +/// use brado::cns; +/// +/// let result = match cns::mask("144082627260004") { // Ok("144 0826 2726 0004") +/// Ok(doc) => doc, +/// Err(e) => panic!("{}", e), +/// }; +/// assert!(cns::is_masked(&result)); // true +/// ``` +/// +/// Documento de 11 dígitos com máscara: +/// ```should_panic +/// use brado::cns; +/// +/// let result = match cns::mask("144 0826 2726 0004") { // It panics! +/// Ok(doc) => doc, +/// Err(e) => panic!("{}", e), +/// }; +/// ``` +pub fn mask(doc: &str) -> Result { + if !is_bare(doc) { + return Err("The given string cannot be masked as CNS!"); + } + + let masked_doc = format!( + "{} {} {} {}", + &doc[0..3], + &doc[3..7], + &doc[7..11], + &doc[11..15], + ); + + Ok(masked_doc) +} + +/// Gera e retorna um CNS aleatório sem máscara. +/// +/// ## Exemplo +/// ``` +/// use brado::cns; +/// +/// let result = cns::generate(); // "144082627260004" +/// assert!(cns::is_bare(&result)); // true +/// ``` +pub fn generate() -> String { + let first_digit = random_digit_from_vector(&valid_first_digits()); + + let cns = { + if vec![1, 2].contains(&first_digit) { + generate_first_case(first_digit) + } else { + generate_second_case(first_digit) + } + }; + + cns.iter() + .map(|d| d.to_string()) + .collect::>() + .join("") +} + +fn generate_first_case(first_digit: u16) -> Vec { + let mut cns: Vec = vec![first_digit]; + cns.extend_from_slice(&random_digit_vector(10)); + cns.extend_from_slice(&generate_last_four_digits(&cns)); + cns +} + +fn generate_second_case(first_digit: u16) -> Vec { + let mut cns: Vec = vec![first_digit]; + cns.extend_from_slice(&random_digit_vector(14)); + + let checksum = cns_sum(&cns, cns.len() as u16); + let rest = checksum % 11; + if rest == 0 { + return cns; + } + + let diff = 11 - rest; + + let mut val: usize = diff as usize; + let mut idx: usize = 15 - val; + + loop { + if val == 0 { + if validate_checksum(&cns) { + return cns; + } else { + let checksum = cns_sum(&cns, cns.len() as u16); + + let diff = 15 - (checksum % 11); + + val = diff as usize; + idx = 15 - diff as usize; + continue; + } + } + + if 15 - idx > val { + idx += 1; + continue; + } + + if cns[idx] != 9 { + cns[idx] += 1; + val -= 15 - idx; + } else { + cns[idx] -= 1; + val += 15 - idx; + idx -= 1; + } + } +} + +/// Gera e retorna um CNS aleatório com máscara. +/// +/// ## Exemplo +/// ``` +/// use brado::cns; +/// +/// let result = cns::generate_masked(); // "144 0826 2726 0004" +/// assert!(cns::is_masked(&result)); // true +/// ``` +pub fn generate_masked() -> String { + mask(&generate()).expect("Valid CNS!") +} diff --git a/brado/src/common.rs b/brado/src/common.rs index 40d7500..8b793bb 100644 --- a/brado/src/common.rs +++ b/brado/src/common.rs @@ -1,4 +1,4 @@ -//! Funções comuns utilizadas na validação de docos. +//! Funções comuns utilizadas na validação de documentos. use rand::Rng; use std::collections::HashSet; @@ -66,7 +66,7 @@ pub fn get_symbols(doc: &str) -> Vec<(usize, char)> { .collect() } -/// Remove os símbolos (desmascara) de uma string (`&str`) +/// Desmascara uma string (`&str`), ou seja, remove os símbolos, /// e retorna a string resultante. /// /// ## Exemplo @@ -102,3 +102,20 @@ pub fn random_digit_vector(size: usize) -> Vec { } digits } + +/// Seleciona aleatoriamente um elemento de um vetor de digítos. +/// +/// ## Exemplo +/// +/// ``` +/// use brado::common::random_digit_from_vector; +/// +/// let options = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; +/// let result = random_digit_from_vector(&options); +/// assert_eq!(options.contains(&result), true); +/// ``` +pub fn random_digit_from_vector(options: &[u16]) -> u16 { + let mut rng = rand::thread_rng(); + let idx = rng.gen_range(0..options.len()); + options[idx] +} diff --git a/brado/src/docs.rs b/brado/src/docs.rs index 75055b7..1c82adb 100644 --- a/brado/src/docs.rs +++ b/brado/src/docs.rs @@ -2,6 +2,7 @@ use crate::cnh; use crate::cnpj; +use crate::cns; use crate::cpf; /// Verifica se um documento `doc` é um CPF, máscarado ou não. @@ -67,12 +68,12 @@ pub fn is_cnpj(doc: &str) -> bool { } /// Verifica se um documento `doc` é uma CNH, máscarado ou não. -/// Retorna `true` se o argumento `doc` for uma CNH válido, +/// Retorna `true` se o argumento `doc` for uma CNH válida, /// caso contrário, retorna `false`. /// /// ## Exemplos /// -/// CNHs válidos: +/// CNHs válidas: /// ``` /// use brado::docs; /// @@ -83,7 +84,7 @@ pub fn is_cnpj(doc: &str) -> bool { /// assert!(result); /// ``` /// -/// CNHs inválidos: +/// CNHs inválidas: /// ``` /// use brado::docs; /// @@ -96,3 +97,34 @@ pub fn is_cnpj(doc: &str) -> bool { pub fn is_cnh(doc: &str) -> bool { cnh::validate(doc) } + +/// Verifica se um documento `doc` é um CNS, máscarado ou não. +/// Retorna `true` se o argumento `doc` for um CNS válido, +/// caso contrário, retorna `false`. +/// +/// ## Exemplos +/// +/// CNSs válidos: +/// ``` +/// use brado::docs; +/// +/// let result = docs::is_cns("144082627260004"); // true +/// assert!(result); +/// +/// let result = docs::is_cns("144 0826 2726 0004"); // true +/// assert!(result); +/// ``` +/// +/// CNSs inválidos: +/// ``` +/// use brado::docs; +/// +/// let result = docs::is_cns("144082627260005"); // false +/// assert!(!result); +/// +/// let result = docs::is_cns("144 0826 2726 0005"); // false +/// assert!(!result); +/// ``` +pub fn is_cns(doc: &str) -> bool { + cns::validate(doc) +} diff --git a/brado/src/lib.rs b/brado/src/lib.rs index c3d62a7..282d435 100644 --- a/brado/src/lib.rs +++ b/brado/src/lib.rs @@ -23,6 +23,7 @@ //! ``` pub mod cnh; pub mod cnpj; +pub mod cns; pub mod common; pub mod cpf; pub mod docs; diff --git a/brado/tests/cns.rs b/brado/tests/cns.rs new file mode 100644 index 0000000..94c15ae --- /dev/null +++ b/brado/tests/cns.rs @@ -0,0 +1,149 @@ +#[cfg(test)] +mod cns_tests { + use brado; + + #[test] + fn cns_validate_1_valid_bare_cns_1() { + let bare_cns: &str = "144082627260004"; + assert_eq!(brado::cns::validate(bare_cns), true); + } + + #[test] + fn cns_validate_2_valid_bare_cns_2() { + let bare_cns: &str = "812297346500000"; + assert_eq!(brado::cns::validate(bare_cns), true); + } + + #[test] + fn cns_validate_3_valid_masked_cns() { + let masked_cns: &str = "144 0826 2726 0004"; + assert_eq!(brado::cns::validate(masked_cns), true); + } + + #[test] + fn cns_validate_4_invalid_bare_cns() { + let bare_cns: &str = "144082627260005"; + assert_eq!(brado::cns::validate(bare_cns), false); + } + + #[test] + fn cns_validate_5_invalid_masked_cns() { + let masked_cns: &str = "144 0826 2726 0005"; + assert_eq!(brado::cns::validate(masked_cns), false); + } + + #[test] + fn cns_validate_6_invalid_mask() { + let document: &str = "14 4082 6272 60004"; + assert_eq!(brado::cns::validate(document), false); + } + + #[test] + fn cns_validate_7_invalid_other_document_1() { + let document: &str = "1440826272600"; + assert_eq!(brado::cns::validate(document), false); + } + + #[test] + fn cns_validate_8_invalid_other_document_2() { + let document: &str = "0144 0826 2726 0004"; + assert_eq!(brado::cns::validate(document), false); + } + + #[test] + fn cns_validate_9_invalid_other_document_3() { + let document: &str = "144 0826 2726 00:04"; + assert_eq!(brado::cns::validate(document), false); + } + + #[test] + fn cns_is_bare_1_bare_cns() { + let bare_cns: &str = "144082627260004"; + assert_eq!(brado::cns::is_bare(bare_cns), true); + } + + #[test] + fn cns_is_bare_2_masked_cns() { + let masked_cns: &str = "144 0826 2726 0004"; + assert_eq!(brado::cns::is_bare(masked_cns), false); + } + + #[test] + fn cns_is_bare_3_other_document() { + let bare_document: &str = "144082627260005"; + assert_eq!(brado::cns::is_bare(bare_document), true); + } + + #[test] + fn cns_is_bare_4_other_document() { + let bare_document: &str = "144 082627260004"; + assert_eq!(brado::cns::is_bare(bare_document), false); + } + + #[test] + fn cns_is_bare_5_other_document() { + let bare_document: &str = "1440826272600045"; + assert_eq!(brado::cns::is_bare(bare_document), false); + } + + #[test] + fn cns_is_masked_1_masked_cns() { + let masked_cns: &str = "144 0826 2726 0004"; + assert_eq!(brado::cns::is_masked(masked_cns), true); + } + + #[test] + fn cns_is_masked_2_bare_cns() { + let bare_cns: &str = "144082627260004"; + assert_eq!(brado::cns::is_masked(bare_cns), false); + } + + #[test] + fn cns_is_masked_3_other_document() { + let masked_document: &str = "1440 0826 2726 0004"; + assert_eq!(brado::cns::is_masked(masked_document), false); + } + + #[test] + fn cns_is_masked_4_other_document() { + let masked_document: &str = "144 0826 2726 00045"; + assert_eq!(brado::cns::is_masked(masked_document), false); + } + + #[test] + fn cns_mask_1_bare_cns() { + let bare_cns: &str = "144082627260004"; + assert_eq!( + brado::cns::mask(bare_cns), + Ok(String::from("144 0826 2726 0004")) + ); + } + + #[test] + fn cns_mask_2_masked_cns() { + let masked_cns: &str = "144 0826 2726 0004"; + let result = brado::cns::mask(masked_cns); + assert_eq!(result, Err("The given string cannot be masked as CNS!"),); + } + + #[test] + fn cns_mask_3_invalid_cns() { + let document: &str = "1440826272600"; + let result = brado::cns::mask(document); + assert_eq!(result, Err("The given string cannot be masked as CNS!"),); + } + + #[test] + fn cns_generate_1() { + let cns = brado::cns::generate(); + assert_eq!(brado::cns::validate(&cns), true); + assert_eq!(brado::cns::is_bare(&cns), true); + } + + #[test] + fn cns_generate_masked_1() { + let cns = brado::cns::generate_masked(); + assert_eq!(brado::cns::validate(&cns), true); + assert_eq!(brado::cns::is_masked(&cns), true); + } +}