Skip to content

428 #488

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open

428 #488

Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ 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_rg` [#428] (https://github.com/brazilian-utils/brutils-python/issues/428)

## [2.2.0] - 2024-09-12

### Added
Expand Down
6 changes: 6 additions & 0 deletions brutils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# RG Imports
# CEP Imports
from brutils.cep import (
format_cep,
Expand Down Expand Up @@ -70,6 +71,9 @@
from brutils.pis import is_valid as is_valid_pis
from brutils.pis import remove_symbols as remove_symbols_pis

# RG Imports
from brutils.rg import is_valid as is_valid_rg

# Voter ID Imports
from brutils.voter_id import format_voter_id
from brutils.voter_id import generate as generate_voter_id
Expand Down Expand Up @@ -121,6 +125,8 @@
"generate_pis",
"is_valid_pis",
"remove_symbols_pis",
# RG
"is_valid_rg",
# Voter ID
"format_voter_id",
"generate_voter_id",
Expand Down
116 changes: 116 additions & 0 deletions brutils/rg.py
Original file line number Diff line number Diff line change
@@ -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
111 changes: 111 additions & 0 deletions tests/test_rg.py
Original file line number Diff line number Diff line change
@@ -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))
Loading