diff --git a/README.md b/README.md index 1276964..82b6d2e 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,8 @@ False - [format_processo_juridico](#format_processo_juridico) - [remove\_symbols\_processo\_juridico](#remove_symbols_processo_juridico) - [generate_processo_juridico](#generate_processo_juridico) +- [Título Eleitoral](#título-eleitoral) + - [format_titulo_eleitoral](#format_titulo_eleitoral) - [is_valid_processo_juridico](#is_valid_processo_juridico) ## CPF @@ -557,10 +559,22 @@ Gera um número de processo válido de acordo com o ano informado e o órgão. P >>> ``` +## Título Eleitoral + +### format_titulo_eleitoral + +Formata título eleitoral (se válido) +```python +>>> from brutils import format_titulo_eleitoral +>>> format_titulo_eleitoral('370834200183') +'3708 3420 01 83' +>>> format_titulo_eleitoral('107381280116') +'1073 8128 01 16' +``` + ## is_valid_processo_juridico Verifica se o número de um processo informado por string é válido ou não. - ```python >>> from brutils import is_valid_processo_juridico >>> is_valid_processo_juridico('10188748220234018200') @@ -571,7 +585,6 @@ True False >>> is_valid_processo_juridico('455323423QQWEQWSsasd&*(()') False ->>> ``` # Novos Utilitários e Reportar Bugs diff --git a/README_EN.md b/README_EN.md index a2450a0..a4f3c01 100644 --- a/README_EN.md +++ b/README_EN.md @@ -85,6 +85,8 @@ False - [format_processo_juridico](#format_processo_juridico) - [remove\_symbols\_processo\_juridico](#remove_symbols_processo_juridico) - [generate_processo_juridico](#generate_processo_juridico) +- [Voter Registration Number](#voter-registration-number) + - [format_titulo_eleitoral](#format_titulo_eleitoral) - [is_valid_processo_juridico](#is_valid_processo_juridico) ## CPF @@ -565,10 +567,22 @@ Generates a valid legal process number according to the arguments of _ano_ which >>> ``` +## Voter Registration Number + +### format_titulo_eleitoral + +Formats voter registration number (if valid) +```python +>>> from brutils import format_titulo_eleitoral +>>> format_titulo_eleitoral('370834200183') +'3708 3420 01 83' +>>> format_titulo_eleitoral('107381280116') +'1073 8128 01 16' +``` + ## is_valid_processo_juridico Checks if a string containing a legal process number is valid or not. - ```python >>> from brutils import is_valid_processo_juridico >>> is_valid_processo_juridico('10188748220234018200') @@ -579,7 +593,6 @@ True False >>> is_valid_processo_juridico('455323423QQWEQWSsasd&*(()') False ->>> ``` # Feature Request and Bug Report diff --git a/brutils/__init__.py b/brutils/__init__.py index 9efb679..ac8ded6 100644 --- a/brutils/__init__.py +++ b/brutils/__init__.py @@ -55,3 +55,5 @@ is_valid_processo_juridico, remove_symbols as remove_symbols_processo_juridico, ) + +from brutils.voter_registration import format as format_titulo_eleitoral diff --git a/brutils/voter_registration.py b/brutils/voter_registration.py new file mode 100644 index 0000000..ba0b271 --- /dev/null +++ b/brutils/voter_registration.py @@ -0,0 +1,77 @@ +import re +from typing import Optional + +# FORMATTING +############ + + +def format(voter_registration_number: str) -> Optional[str]: + """ + Formats a voter registration number with spaces + Returns None with invalid input + Ex: 017746811074 - > 0177 4681 10 74 + 117351120817 - > 1173 5112 08 17 + 6455327 - > 'None' + """ + + if not isinstance(voter_registration_number, str) or not _is_valid( + voter_registration_number + ): + return None + + first_four = voter_registration_number[0:4] + second_four = voter_registration_number[4:8] + federal_unit_numbers = voter_registration_number[8:10] + check_digits = voter_registration_number[10:12] + + return f"{first_four} {second_four} {federal_unit_numbers} {check_digits}" + + +# OPERATIONS +############ + + +def _is_valid(voter_registration_number: str) -> bool: + """ + Checks if the number check digit calculation is correct + Also checks if the Federal Unit identifier is valid (1 - 28) + """ + if not re.match(r"^\d{12}$", voter_registration_number): + return False + + fu_identifier = int(voter_registration_number[8:10]) + + if not (1 <= fu_identifier <= 28): + return False + + check_digits = voter_registration_number[10:12] + calculated_check_digits = _calculate_check_digits(voter_registration_number) + + return check_digits == calculated_check_digits + + +def _calculate_check_digits( + voter_registration_number: str, +): # type: (str) -> str + """ + Calculates voter registration number check digits + """ + first_digit_sum = sum( + int(digit) * (index + 2) + for index, digit in enumerate(voter_registration_number[:8]) + ) + calculated_first_digit = first_digit_sum % 11 + + if calculated_first_digit == 10: + calculated_first_digit = 0 + + second_digit_sum = sum( + int(digit) * (index + 7) + for index, digit in enumerate(voter_registration_number[8:11]) + ) + calculated_second_digit = second_digit_sum % 11 + + if calculated_second_digit == 10: + calculated_second_digit = 0 + + return f"{calculated_first_digit}{calculated_second_digit}" diff --git a/tests/test_voter_registration.py b/tests/test_voter_registration.py new file mode 100644 index 0000000..8321c49 --- /dev/null +++ b/tests/test_voter_registration.py @@ -0,0 +1,71 @@ +import unittest +from brutils import format_titulo_eleitoral + + +class TestVoterRegistrationFormat(unittest.TestCase): + def test_valid_number(self): + # Specific number + response = format_titulo_eleitoral("370834200183") + self.assertEqual( + response, "3708 3420 01 83", "AssertionError for 370834200183" + ) + + def test_valid_numbers_type_and_regexp(self): + # Valid voter registration numbers + valid_numbers = [ + "370834200183", + "107381280116", + "587816450175", + "260577100132", + "248146750108", + ] + for n in valid_numbers: + try: + formatted = format_titulo_eleitoral(n) + self.assertIsInstance(formatted, str) + self.assertRegex(formatted, r"^(\d{4} \d{4} \d{2} \d{2})$") + except: # noqa: E722 + print(f"AssertionError for number: {n}") + raise AssertionError + + def test_invalid_registration_numbers(self): + # Invalid voter registration numbers should return None + invalid_numbers = [ + "123", + "370834200181", + "107381280111", + "587816450179", + "goiaba", + ] + for n in invalid_numbers: + try: + self.assertEqual(format_titulo_eleitoral(n), None) + except: # noqa: E722 + print(f"AssertionError for number: {n}") + raise AssertionError + + def test_non_string_input(self): + # Non-string input should also return None + non_strings = [None, 123, True, ["test@example.com"]] + for value in non_strings: + self.assertEqual(format_titulo_eleitoral(value), None) + + def test_empty_string(self): + # Empty string should return None too + self.assertEqual(format_titulo_eleitoral(""), None) + + def test_invalid_fu(self): + # Document with invalid UF should fail + self.assertEqual(format_titulo_eleitoral("370834209983"), None) + + def test_mods_equal_zero(self): + # Ensure that digit calculation works when sum equals 10 + valid_numbers = ["175526800205", "417667660230"] + for n in valid_numbers: + try: + formatted = format_titulo_eleitoral(n) + self.assertIsInstance(formatted, str) + self.assertRegex(formatted, r"^(\d{4} \d{4} \d{2} \d{2})$") + except: # noqa: E722 + print(f"AssertionError for number: {n}") + raise AssertionError