From 542d6043fcdf2dd71fc9e49d3368b31191d341cd Mon Sep 17 00:00:00 2001 From: Oleari19 Date: Sat, 1 Feb 2025 07:38:32 -0300 Subject: [PATCH 1/3] Implementation of issue #430, which requests the validation of RENAVAM and its respective tests. Additionally, the appropriate documentation as required by the contribution file. --- README.md | 34 ++++++++++++++++++++++++++++++ README_EN.md | 34 ++++++++++++++++++++++++++++++ brutils/__init__.py | 5 +++++ brutils/renavam.py | 48 +++++++++++++++++++++++++++++++++++++++++++ tests/test_renavam.py | 42 +++++++++++++++++++++++++++++++++++++ 5 files changed, 163 insertions(+) create mode 100644 brutils/renavam.py create mode 100644 tests/test_renavam.py diff --git a/README.md b/README.md index 42a41b0..c553b59 100644 --- a/README.md +++ b/README.md @@ -1254,6 +1254,40 @@ Exemplo: None ``` +## RENAVAM + +### is_valid_renavam + +Valida o número de registro de veículo brasileiro (RENAVAM). + +Esta função recebe uma string RENAVAM e verifica se é válida. Um RENAVAM +válido consiste exatamente em 11 dígitos. O último dígito é um dígito verificador +calculado a partir dos primeiros 10 dígitos usando um sistema de pesos específicos. + +Parâmetros: + renavam (str): A string do RENAVAM a ser validada. + +Retorna: + bool: True se o RENAVAM for válido, False caso contrário. + +Exemplo: + +```python +>>> from brutils import is_valid_renavam +>>> is_valid_renavam('35298206229') +True +>>> is_valid_renavam('12345678900') +False +>>> is_valid_renavam('1234567890a') +False +>>> is_valid_renavam('12345678 901') +False +>>> is_valid_renavam('12345678') # Less than 11 digits +False +>>> is_valid_renavam('') # Empty string +False +``` + # Novos Utilitários e Reportar Bugs Caso queira sugerir novas funcionalidades ou reportar bugs, basta criar diff --git a/README_EN.md b/README_EN.md index 4f97c00..229b732 100644 --- a/README_EN.md +++ b/README_EN.md @@ -1257,6 +1257,40 @@ Example: None ``` +## RENAVAM + +### is_valid_renavam + +Validates the Brazilian vehicle registration number (RENAVAM). + +This function takes a RENAVAM string and checks if it is valid. +A valid RENAVAM consists of exactly 11 digits. Theast digit a check digit +calculated from the the first 10 digits using a specific weighting system. + +Args: + renavam (str): The RENAVAM string to be validated. + +Returns: + bool: True if the RENAVAM is valid, False otherwise. + +Example: + +```python +>>> from brutils import is_valid_renavam +>>> is_valid_renavam('35298206229') +True +>>> is_valid_renavam('12345678900') +False +>>> is_valid_renavam('1234567890a') +False +>>> is_valid_renavam('12345678 901') +False +>>> is_valid_renavam('12345678') # Less than 11 digits +False +>>> is_valid_renavam('') # Empty string +False +``` + # Feature Request and Bug Report If you want to suggest new features or report bugs, simply create diff --git a/brutils/__init__.py b/brutils/__init__.py index 66b1d2d..e444601 100644 --- a/brutils/__init__.py +++ b/brutils/__init__.py @@ -70,6 +70,9 @@ from brutils.pis import is_valid as is_valid_pis from brutils.pis import remove_symbols as remove_symbols_pis +# RENAVAM Imports +from brutils.renavam import is_valid_renavam + # Voter ID Imports from brutils.voter_id import format_voter_id from brutils.voter_id import generate as generate_voter_id @@ -133,4 +136,6 @@ "is_holiday", # Currency "format_currency", + # RENAVAM + "is_valid_renavam", ] diff --git a/brutils/renavam.py b/brutils/renavam.py new file mode 100644 index 0000000..0a99b81 --- /dev/null +++ b/brutils/renavam.py @@ -0,0 +1,48 @@ +def is_valid_renavam(renavam): # type: (str) -> bool + """ + Validates the Brazilian vehicle registration number (RENAVAM). + + This function takes a RENAVAM string and checks if it is valid. + A valid RENAVAM consists of exactly 11 digits. Theast digit a check digit + calculated from the the first 10 digits using a specific weighting system. + + Args: + renavam (str): The RENAVAM string to be validated. + + Returns: + bool: True if the RENAVAM is valid, False otherwise. + + Example: + >>> is_valid_renavam('35298206229') + True + >>> is_valid_renavam('12345678900') + False + >>> is_valid_renavam('1234567890a') + False + >>> is_valid_renavam('12345678 901') + False + >>> is_valid_renavam('12345678') # Less than 11 digits + False + >>> is_valid_renavam('') # Empty string + False + """ + + if len(renavam) != 11 or not renavam.isdigit(): + return False + + ## Calculating the check digit + digits = [int(digit) for digit in renavam[:10]] # 10 digits + weights = [3, 2, 9, 8, 7, 6, 5, 4, 3, 2] + checksum = sum( + digit * weight for digit, weight in zip(digits, weights) + ) # Sum of the products of the digits and weights + + remainder = checksum % 11 + check_digit = 0 if remainder == 0 else 11 - remainder + + # If the calculated check digit is 0, return False + if check_digit == 0: + return False + + # Checking if the calculated check digit is equal to the last digit of the RENAVAM + return int(renavam[-1]) == check_digit diff --git a/tests/test_renavam.py b/tests/test_renavam.py new file mode 100644 index 0000000..f08e5ea --- /dev/null +++ b/tests/test_renavam.py @@ -0,0 +1,42 @@ +from unittest import TestCase + +from brutils.renavam import is_valid_renavam + + +class TestRENAVAM(TestCase): + def test_valid_renavam(self): + # First + self.assertTrue(is_valid_renavam("79831854647")) + self.assertFalse(is_valid_renavam("1234567890")) # Less than 11 digits + self.assertFalse( + is_valid_renavam("123456789012") + ) # More than 11 digits + # Second + self.assertFalse(is_valid_renavam("1234567890a")) # With letter + self.assertFalse(is_valid_renavam("12345678 901")) # With space + self.assertFalse( + is_valid_renavam("12345678901!") + ) # With special character + self.assertFalse(is_valid_renavam("")) # Empty string + + # Third + def test_renavam_check_digit(self): + self.assertTrue(is_valid_renavam("01044683357")) + self.assertFalse(is_valid_renavam("12345678901")) # Invalid check digit + + # Fourth - Issue + def test_is_valid_renavam(self): + # Tests for valid RENAVAM + self.assertTrue( + is_valid_renavam("35298206229") + ) # Change this one because in the issue example it is invalid + self.assertFalse(is_valid_renavam("12345678900")) + + # Tests for invalid RENAVAM + self.assertFalse(is_valid_renavam("1234567890a")) + self.assertFalse(is_valid_renavam("12345678 901")) + self.assertFalse(is_valid_renavam("12345678")) + self.assertFalse(is_valid_renavam("")) + self.assertFalse(is_valid_renavam("123456789012")) + self.assertFalse(is_valid_renavam("abcdefghijk")) + self.assertFalse(is_valid_renavam("12345678901!")) From e7e9fd03ed5e847c409fb19961bb4e7c5bb5865c Mon Sep 17 00:00:00 2001 From: Maria Clara Oleari de Araujo <110275583+Oleari19@users.noreply.github.com> Date: Sat, 1 Feb 2025 10:51:29 -0300 Subject: [PATCH 2/3] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index da7eb34..3fcb723 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Utilitário `get_municipality_by_code` [412](https://github.com/brazilian-utils/brutils-python/pull/412) - Utilitário `get_code_by_municipality_name` [#399](https://github.com/brazilian-utils/brutils-python/issues/399) - Utilitário `format_currency` [#426](https://github.com/brazilian-utils/brutils-python/issues/426) +- Utilitário `is_valid_renavam` [#430](https://github.com/brazilian-utils/brutils-python/issues/430) ## [2.2.0] - 2024-09-12 From 09777e715e62d6181aa32b2a341fb71a3760c906 Mon Sep 17 00:00:00 2001 From: Oleari19 Date: Wed, 5 Feb 2025 13:27:36 -0300 Subject: [PATCH 3/3] fix: resolvendo conflitos entre o PR 487 e 488 --- CHANGELOG.md | 3 +- brutils/__init__.py | 9 +++- brutils/rg.py | 116 ++++++++++++++++++++++++++++++++++++++++++++ tests/test_rg.py | 111 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 236 insertions(+), 3 deletions(-) create mode 100644 brutils/rg.py create mode 100644 tests/test_rg.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fcb723..56a3a4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [2.3.0] ### Added - Utilitário `convert_code_to_uf` [#397](https://github.com/brazilian-utils/brutils-python/pull/410) @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Utilitário `get_code_by_municipality_name` [#399](https://github.com/brazilian-utils/brutils-python/issues/399) - Utilitário `format_currency` [#426](https://github.com/brazilian-utils/brutils-python/issues/426) - Utilitário `is_valid_renavam` [#430](https://github.com/brazilian-utils/brutils-python/issues/430) +- Utilitário `is_valid_rg` [#428] (https://github.com/brazilian-utils/brutils-python/issues/428) ## [2.2.0] - 2024-09-12 diff --git a/brutils/__init__.py b/brutils/__init__.py index e444601..6a715ee 100644 --- a/brutils/__init__.py +++ b/brutils/__init__.py @@ -73,6 +73,9 @@ # RENAVAM Imports from brutils.renavam import is_valid_renavam +# RG Imports +from brutils.rg import is_valid_rg + # Voter ID Imports from brutils.voter_id import format_voter_id from brutils.voter_id import generate as generate_voter_id @@ -124,6 +127,10 @@ "generate_pis", "is_valid_pis", "remove_symbols_pis", + # RENAVAM + "is_valid_renavam", + # RG + "is_valid_rg", # Voter ID "format_voter_id", "generate_voter_id", @@ -136,6 +143,4 @@ "is_holiday", # Currency "format_currency", - # RENAVAM - "is_valid_renavam", ] diff --git a/brutils/rg.py b/brutils/rg.py new file mode 100644 index 0000000..13458a4 --- /dev/null +++ b/brutils/rg.py @@ -0,0 +1,116 @@ +import re + + +def is_valid_rg(rg: str, uf: str) -> bool: + """ + Validates a Brazilian RG (Registro Geral) based on the state (UF). + + This function checks whether a given RG is valid for a specific state in Brazil. + Each state may have its own RG format, and the function should handle these differences. + + Additionally, for RGs issued by various states, the function verifies the mathematical rule + for the check digit where applicable. + + Args: + rg (str): The RG to be validated. + uf (str): The state (UF) for which the RG should be validated. + + Returns: + bool: Returns True if the RG is valid, or False if it is invalid. + + Example: + >>> is_valid_rg('12.345.678-9', 'SP') + True + >>> is_valid_rg('MG-12.345.678', 'MG') + True + >>> is_valid_rg('123456789', 'RJ') + False + >>> is_valid_rg('A12345678', 'SP') + False + >>> is_valid_rg('12.345.678', 'SP') + False + """ + # Se rg ou uf forem None, retorna False + if rg is None or uf is None: + return False + + # Remove caracteres indesejados (mantém dígitos, letras, pontos e hífens) + rg_cleaned = re.sub(r"[^0-9A-Za-z.-]", "", rg) + + # Definição de padrões comuns para RG + common_patterns = { + "9_digits": r"^\d{9}$", # Ex.: '123456789' + "8_digits_dash": r"^\d{8}-\d$", # Ex.: '12345678-9' + "7_10_digits": r"^\d{1,2}\.\d{3}\.\d{3}$", # Ex.: '1.234.567' ou '12.345.678' + "3_groups": r"^\d{3}\.\d{3}\.\d{3}$", # Ex.: '123.456.789' + "3_groups_dash": r"^\d{2}\.\d{3}\.\d{3}-[0-9X]$", # Ex.: '12.345.678-9' ou com 'X' no dígito verificador + "2_groups_dash": r"^\d{2}\.\d{3}\.\d{3}$", # Ex.: '12.345.678' + "2_3_groups_dash": r"^\d{2}\.\d{3}\.\d{3}-\d$", # Ex.: '12.345.678-9' + "mg_format": r"^MG-\d{2}\.\d{3}\.\d{3}$", # Ex.: 'MG-12.345.678' + } + + # Mapeamento dos padrões de RG para cada UF + rg_patterns = { + # Norte + "AC": common_patterns["2_3_groups_dash"], + "AP": common_patterns["2_3_groups_dash"], + "AM": common_patterns["2_3_groups_dash"], + "PA": common_patterns["2_3_groups_dash"], + "RO": common_patterns["2_3_groups_dash"], + "RR": common_patterns["2_3_groups_dash"], + "TO": common_patterns["2_3_groups_dash"], + # Nordeste + "AL": common_patterns["2_3_groups_dash"], + "BA": common_patterns["8_digits_dash"], + "CE": common_patterns["2_3_groups_dash"], + "MA": common_patterns["2_3_groups_dash"], + "PB": common_patterns["9_digits"], + "PE": common_patterns["2_3_groups_dash"], + "PI": common_patterns["2_3_groups_dash"], + "RN": common_patterns["2_3_groups_dash"], + "SE": common_patterns["9_digits"], + # Centro-Oeste + "DF": common_patterns["2_3_groups_dash"], + "GO": common_patterns["2_3_groups_dash"], + "MT": common_patterns["2_3_groups_dash"], + "MS": common_patterns["2_3_groups_dash"], + # Sudeste + "ES": common_patterns["2_3_groups_dash"], + "MG": common_patterns["mg_format"], + # Para RJ, aceita RG composto por 9 dígitos sem formatação + "RJ": common_patterns["9_digits"], + "SP": common_patterns["3_groups_dash"], + # Sul + "PR": common_patterns["3_groups_dash"], + "RS": common_patterns["7_10_digits"], + "SC": common_patterns["3_groups"], + } + + # Se a UF não estiver mapeada, retorna False + if uf not in rg_patterns: + return False + + # Verifica se o RG corresponde ao padrão esperado para a UF + if not re.match(rg_patterns[uf], rg_cleaned): + return False + + # Validação do dígito verificador apenas para São Paulo (SP) + if uf == "SP": + # Formato esperado: "12.345.678-9" + parts = rg_cleaned.split("-") + if len(parts) != 2: + return False + # Remove quaisquer caracteres não numéricos da parte principal + main = re.sub(r"\D", "", parts[0]) + check_digit = parts[1] + # O número principal deve ter exatamente 8 dígitos + if len(main) != 8: + return False + # Cálculo: multiplica cada dígito pelos pesos [2, 3, 4, 5, 6, 7, 8, 9] + total = sum(int(d) * w for d, w in zip(main, [2, 3, 4, 5, 6, 7, 8, 9])) + remainder = total % 11 + expected = "X" if remainder == 10 else str(remainder) + if check_digit != expected: + return False + + return True diff --git a/tests/test_rg.py b/tests/test_rg.py new file mode 100644 index 0000000..30b1000 --- /dev/null +++ b/tests/test_rg.py @@ -0,0 +1,111 @@ +from unittest import TestCase + +from brutils.rg import is_valid_rg + + +class TestRG(TestCase): + # Base do Github + # def test_is_valid_rg(self): + # # Testes para RGs válidos + # self.assertTrue(is_valid_rg('12.345.678-9', 'SP')) + # self.assertTrue(is_valid_rg('MG-12.345.678', 'MG')) + # self.assertTrue(is_valid_rg('123456789', 'RJ')) + # + # # Testes para RGs inválidos + # self.assertFalse(is_valid_rg('A12345678', 'SP')) # Letras não permitidas + # self.assertFalse(is_valid_rg('1234567890', 'SP')) # RG longo demais + # self.assertFalse(is_valid_rg('12.345.678-10', 'SP')) # Dígito verificador incorreto + # + # # Testes para entradas malformadas + # self.assertFalse(is_valid_rg('', 'SP')) # Entrada vazia + # self.assertFalse(is_valid_rg('12.345.678', 'SP')) # Formato incorreto sem dígito verificador + # self.assertFalse(is_valid_rg('12.345.678-9', 'XX')) # UF inválida + + def test_valid_rg_sp(self): + # Testes para SP (São Paulo) – validação com dígito verificador + self.assertTrue(is_valid_rg("12.345.678-9", "SP")) + # Exemplo com dígito calculado: "11.111.111-0" (1*2+1*3+...+1*9 = 44; 44 % 11 = 0) + self.assertTrue(is_valid_rg("11.111.111-0", "SP")) + # Dígito verificador incorreto + self.assertFalse(is_valid_rg("11.111.111-1", "SP")) + # Formato incorreto: falta o hífen + self.assertFalse(is_valid_rg("12.345.6789", "SP")) + + def test_valid_rg_mg(self): + # Testes para MG (Minas Gerais) – somente verificação de formato + self.assertTrue(is_valid_rg("MG-12.345.678", "MG")) + # Ausência do prefixo "MG-" + self.assertFalse(is_valid_rg("12.345.678", "MG")) + # Formato sem hífen após o prefixo + self.assertFalse(is_valid_rg("MG12.345.678", "MG")) + + def test_valid_rg_rj(self): + # Testes para RJ (Rio de Janeiro) – somente RG composto por 9 dígitos + self.assertTrue(is_valid_rg("123456789", "RJ")) + # Formatação não permitida para RJ + self.assertFalse(is_valid_rg("12.345.678-9", "RJ")) + # Contém caractere não numérico + self.assertFalse(is_valid_rg("12345678A", "RJ")) + + def test_valid_rg_norte(self): + # Estados da Região Norte: AC, AP, AM, PA, RO, RR, TO (padrão "2_3_groups_dash") + for uf in ["AC", "AP", "AM", "PA", "RO", "RR", "TO"]: + with self.subTest(uf=uf): + self.assertTrue(is_valid_rg("12.345.678-9", uf)) + # Formato incorreto: ausência do hífen + self.assertFalse(is_valid_rg("12.345.6789", uf)) + + def test_valid_rg_nordeste(self): + # Estados com padrão "2_3_groups_dash": AL, CE, MA, PE, PI, RN + for uf in ["AL", "CE", "MA", "PE", "PI", "RN"]: + with self.subTest(uf=uf): + self.assertTrue(is_valid_rg("12.345.678-9", uf)) + # Formato sem formatação adequada (apenas dígitos) + self.assertFalse(is_valid_rg("123456789", uf)) + # BA utiliza o padrão "8_digits_dash": exemplo "12345678-9" + self.assertTrue(is_valid_rg("12345678-9", "BA")) + self.assertFalse(is_valid_rg("12.345.678-9", "BA")) + # PB e SE usam o padrão "9_digits" + for uf in ["PB", "SE"]: + with self.subTest(uf=uf): + self.assertTrue(is_valid_rg("123456789", uf)) + self.assertFalse(is_valid_rg("12.345.678-9", uf)) + + def test_valid_rg_centro_oeste(self): + # Estados: DF, GO, MT, MS (padrão "2_3_groups_dash") + for uf in ["DF", "GO", "MT", "MS"]: + with self.subTest(uf=uf): + self.assertTrue(is_valid_rg("12.345.678-9", uf)) + # Formato sem formatação esperada + self.assertFalse(is_valid_rg("123456789", uf)) + + def test_valid_rg_sudeste(self): + # ES utiliza o padrão "2_3_groups_dash" + self.assertTrue(is_valid_rg("12.345.678-9", "ES")) + self.assertFalse(is_valid_rg("123456789", "ES")) + # RJ e SP já foram testados separadamente e possuem regras próprias + + def test_valid_rg_sul(self): + # PR utiliza o padrão "3_groups_dash" (igual a SP, mas sem a validação do dígito verificador) + self.assertTrue(is_valid_rg("12.345.678-9", "PR")) + self.assertFalse(is_valid_rg("123456789", "PR")) + # RS utiliza o padrão "7_10_digits" – aceita RG com 7 ou 8 dígitos formatados com pontos + self.assertTrue(is_valid_rg("1.234.567", "RS")) + self.assertTrue(is_valid_rg("12.345.678", "RS")) + self.assertFalse(is_valid_rg("12.345.678-9", "RS")) + # SC utiliza o padrão "3_groups": formato exato de três grupos de três dígitos + self.assertTrue(is_valid_rg("123.456.789", "SC")) + self.assertFalse(is_valid_rg("12.345.678-9", "SC")) + + def test_invalid_inputs(self): + # Teste para entradas malformadas e UFs inválidas + self.assertFalse(is_valid_rg("", "SP")) # Entrada vazia + self.assertFalse(is_valid_rg(" ", "RJ")) # Somente espaços + self.assertFalse( + is_valid_rg("12.345.678", "SP") + ) # Falta dígito verificador + self.assertFalse(is_valid_rg("12.345.678-9", "XX")) # UF não mapeada + + # Testando parâmetros None: esperamos que ocorra um erro de tipo + self.assertFalse(is_valid_rg(None, "SP")) + self.assertFalse(is_valid_rg("12.345.678-9", None))