diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 93c2820c..e3bae4e5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -102,7 +102,7 @@ emulate bash -c '. .../bin/activate' Para testar se o ambiente virtual está ativo corretamente, execute o comando e verifique se a resposta é algo parecido com a seguinte: ```sh -$ poetry env inf +$ poetry env info Virtualenv Python: 3.x.y Implementation: CPython diff --git a/brutils/__init__.py b/brutils/__init__.py index cbfa194c..11b59eb1 100644 --- a/brutils/__init__.py +++ b/brutils/__init__.py @@ -42,6 +42,9 @@ remove_symbols as remove_symbols_cpf, ) +# Date imports +from brutils.date import convert_date_to_text + # Email Import from brutils.email import is_valid as is_valid_email @@ -136,6 +139,8 @@ "generate_cpf", "is_valid_cpf", "remove_symbols_cpf", + # Date + "convert_date_to_text", # Email "is_valid_email", # Legal Process diff --git a/brutils/data/enums/months.py b/brutils/data/enums/months.py new file mode 100644 index 00000000..93560864 --- /dev/null +++ b/brutils/data/enums/months.py @@ -0,0 +1,57 @@ +from brutils.data.enums.better_enum import BetterEnum + + +class MonthsEnum(BetterEnum): + JANEIRO = 1 + FEVEREIRO = 2 + MARCO = 3 + ABRIL = 4 + MAIO = 5 + JUNHO = 6 + JULHO = 7 + AGOSTO = 8 + SETEMBRO = 9 + OUTUBRO = 10 + NOVEMBRO = 11 + DEZEMBRO = 12 + + @property + def mont_name(self) -> str: + if self == MonthsEnum.JANEIRO: + return "janeiro" + elif self == MonthsEnum.FEVEREIRO: + return "fevereiro" + elif self == MonthsEnum.MARCO: + return "marco" + elif self == MonthsEnum.ABRIL: + return "abril" + elif self == MonthsEnum.MAIO: + return "maio" + elif self == MonthsEnum.JUNHO: + return "junho" + elif self == MonthsEnum.JULHO: + return "julho" + elif self == MonthsEnum.AGOSTO: + return "agosto" + elif self == MonthsEnum.SETEMBRO: + return "setembro" + elif self == MonthsEnum.OUTUBRO: + return "outubro" + elif self == MonthsEnum.NOVEMBRO: + return "novembro" + else: + return "dezembro" + + @classmethod + def is_valid_month(cls, month: int) -> bool: + """ + Checks if the given month value is valid. + Args: + month (int): The month to check. + + Returns: + True if the month is valid, False otherwise. + """ + return ( + True if month in set(month.value for month in MonthsEnum) else False + ) diff --git a/brutils/date.py b/brutils/date.py new file mode 100644 index 00000000..6a8e39a5 --- /dev/null +++ b/brutils/date.py @@ -0,0 +1,64 @@ +import re +from typing import Union + +from num2words import num2words + +from brutils.data.enums.months import MonthsEnum + + +def convert_date_to_text(date: str) -> Union[str, None]: + """ + Converts a given date in Brazilian format (dd/mm/yyyy) to its textual representation. + + This function takes a date as a string in the format dd/mm/yyyy and converts it + to a string with the date written out in Brazilian Portuguese, including the full + month name and the year. + + Args: + date (str): The date to be converted into text. Expected format: dd/mm/yyyy. + + Returns: + str or None: A string with the date written out in Brazilian Portuguese, + or None if the date is invalid. + + """ + pattern = re.compile(r"\d{2}/\d{2}/\d{4}") + if not re.match(pattern, date): + raise ValueError( + "Date is not a valid date. Please pass a date in the format dd/mm/yyyy." + ) + + day_str, month_str, year_str = date.split("/") + day = int(day_str) + month = int(month_str) + year = int(year_str) + + if 0 <= day > 31: + return None + + if not MonthsEnum.is_valid_month(month): + return None + + # Leap year. + if MonthsEnum(int(month)) is MonthsEnum.FEVEREIRO: + if (int(year) % 4 == 0 and int(year) % 100 != 0) or ( + int(year) % 400 == 0 + ): + if day > 29: + return None + else: + if day > 28: + return None + + day_string = "Primeiro" if day == 1 else num2words(day, lang="pt") + month = MonthsEnum(month) + year_string = num2words(year, lang="pt") + + date_string = ( + day_string.capitalize() + + " de " + + month.mont_name + + " de " + + year_string + ) + return date_string diff --git a/poetry.lock b/poetry.lock index 87d565f0..20b9ce37 100644 --- a/poetry.lock +++ b/poetry.lock @@ -84,34 +84,58 @@ files = [ [package.extras] toml = ["tomli"] +[[package]] +name = "docopt" +version = "0.6.2" +description = "Pythonic argument parser, that will make you smile" +optional = false +python-versions = "*" +files = [ + {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, +] + +[[package]] +name = "num2words" +version = "0.5.13" +description = "Modules to convert numbers to words. Easily extensible." +optional = false +python-versions = "*" +files = [ + {file = "num2words-0.5.13-py3-none-any.whl", hash = "sha256:39e662c663f0a7e15415431ea68eb3dc711b49e3b776d93403e1da0a219ca4ee"}, + {file = "num2words-0.5.13.tar.gz", hash = "sha256:a3064716fbbf90d75c449450cebfbc73a6a13e63b2531d09bdecc3ab1a2209cf"}, +] + +[package.dependencies] +docopt = ">=0.6.2" + [[package]] name = "ruff" -version = "0.6.4" +version = "0.6.5" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.6.4-py3-none-linux_armv6l.whl", hash = "sha256:c4b153fc152af51855458e79e835fb6b933032921756cec9af7d0ba2aa01a258"}, - {file = "ruff-0.6.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:bedff9e4f004dad5f7f76a9d39c4ca98af526c9b1695068198b3bda8c085ef60"}, - {file = "ruff-0.6.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d02a4127a86de23002e694d7ff19f905c51e338c72d8e09b56bfb60e1681724f"}, - {file = "ruff-0.6.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7862f42fc1a4aca1ea3ffe8a11f67819d183a5693b228f0bb3a531f5e40336fc"}, - {file = "ruff-0.6.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eebe4ff1967c838a1a9618a5a59a3b0a00406f8d7eefee97c70411fefc353617"}, - {file = "ruff-0.6.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:932063a03bac394866683e15710c25b8690ccdca1cf192b9a98260332ca93408"}, - {file = "ruff-0.6.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:50e30b437cebef547bd5c3edf9ce81343e5dd7c737cb36ccb4fe83573f3d392e"}, - {file = "ruff-0.6.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c44536df7b93a587de690e124b89bd47306fddd59398a0fb12afd6133c7b3818"}, - {file = "ruff-0.6.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ea086601b22dc5e7693a78f3fcfc460cceabfdf3bdc36dc898792aba48fbad6"}, - {file = "ruff-0.6.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b52387d3289ccd227b62102c24714ed75fbba0b16ecc69a923a37e3b5e0aaaa"}, - {file = "ruff-0.6.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0308610470fcc82969082fc83c76c0d362f562e2f0cdab0586516f03a4e06ec6"}, - {file = "ruff-0.6.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:803b96dea21795a6c9d5bfa9e96127cc9c31a1987802ca68f35e5c95aed3fc0d"}, - {file = "ruff-0.6.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:66dbfea86b663baab8fcae56c59f190caba9398df1488164e2df53e216248baa"}, - {file = "ruff-0.6.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:34d5efad480193c046c86608dbba2bccdc1c5fd11950fb271f8086e0c763a5d1"}, - {file = "ruff-0.6.4-py3-none-win32.whl", hash = "sha256:f0f8968feea5ce3777c0d8365653d5e91c40c31a81d95824ba61d871a11b8523"}, - {file = "ruff-0.6.4-py3-none-win_amd64.whl", hash = "sha256:549daccee5227282289390b0222d0fbee0275d1db6d514550d65420053021a58"}, - {file = "ruff-0.6.4-py3-none-win_arm64.whl", hash = "sha256:ac4b75e898ed189b3708c9ab3fc70b79a433219e1e87193b4f2b77251d058d14"}, - {file = "ruff-0.6.4.tar.gz", hash = "sha256:ac3b5bfbee99973f80aa1b7cbd1c9cbce200883bdd067300c22a6cc1c7fba212"}, + {file = "ruff-0.6.5-py3-none-linux_armv6l.whl", hash = "sha256:7e4e308f16e07c95fc7753fc1aaac690a323b2bb9f4ec5e844a97bb7fbebd748"}, + {file = "ruff-0.6.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:932cd69eefe4daf8c7d92bd6689f7e8182571cb934ea720af218929da7bd7d69"}, + {file = "ruff-0.6.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3a8d42d11fff8d3143ff4da41742a98f8f233bf8890e9fe23077826818f8d680"}, + {file = "ruff-0.6.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a50af6e828ee692fb10ff2dfe53f05caecf077f4210fae9677e06a808275754f"}, + {file = "ruff-0.6.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:794ada3400a0d0b89e3015f1a7e01f4c97320ac665b7bc3ade24b50b54cb2972"}, + {file = "ruff-0.6.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:381413ec47f71ce1d1c614f7779d88886f406f1fd53d289c77e4e533dc6ea200"}, + {file = "ruff-0.6.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:52e75a82bbc9b42e63c08d22ad0ac525117e72aee9729a069d7c4f235fc4d276"}, + {file = "ruff-0.6.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09c72a833fd3551135ceddcba5ebdb68ff89225d30758027280968c9acdc7810"}, + {file = "ruff-0.6.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:800c50371bdcb99b3c1551d5691e14d16d6f07063a518770254227f7f6e8c178"}, + {file = "ruff-0.6.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e25ddd9cd63ba1f3bd51c1f09903904a6adf8429df34f17d728a8fa11174253"}, + {file = "ruff-0.6.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7291e64d7129f24d1b0c947ec3ec4c0076e958d1475c61202497c6aced35dd19"}, + {file = "ruff-0.6.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9ad7dfbd138d09d9a7e6931e6a7e797651ce29becd688be8a0d4d5f8177b4b0c"}, + {file = "ruff-0.6.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:005256d977021790cc52aa23d78f06bb5090dc0bfbd42de46d49c201533982ae"}, + {file = "ruff-0.6.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:482c1e6bfeb615eafc5899127b805d28e387bd87db38b2c0c41d271f5e58d8cc"}, + {file = "ruff-0.6.5-py3-none-win32.whl", hash = "sha256:cf4d3fa53644137f6a4a27a2b397381d16454a1566ae5335855c187fbf67e4f5"}, + {file = "ruff-0.6.5-py3-none-win_amd64.whl", hash = "sha256:3e42a57b58e3612051a636bc1ac4e6b838679530235520e8f095f7c44f706ff9"}, + {file = "ruff-0.6.5-py3-none-win_arm64.whl", hash = "sha256:51935067740773afdf97493ba9b8231279e9beef0f2a8079188c4776c25688e0"}, + {file = "ruff-0.6.5.tar.gz", hash = "sha256:4d32d87fab433c0cf285c3683dd4dae63be05fd7a1d65b3f5bf7cdd05a6b96fb"}, ] [metadata] lock-version = "2.0" python-versions = "^3.8.1" -content-hash = "989d447f5ac999811a4e25ed6b1fc0749aa127fb45290a675b17e652d85b4123" +content-hash = "957675c81621c16701bae337f25dc9571f3c2a787002725627de366f1b01d16a" diff --git a/pyproject.toml b/pyproject.toml index e70382e8..7d967168 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.8.1" +num2words = "0.5.13" [tool.poetry.group.test.dependencies] coverage = "^7.2.7" diff --git a/tests/test_date.py b/tests/test_date.py new file mode 100644 index 00000000..54d4f9d5 --- /dev/null +++ b/tests/test_date.py @@ -0,0 +1,58 @@ +from unittest import TestCase + +from num2words import num2words + +from brutils.date import convert_date_to_text + + +class TestNum2Words(TestCase): + def test_num_conversion(self) -> None: + """ + Smoke test of the num2words library. + This test is used to guarantee that our dependency still works. + """ + self.assertEqual(num2words(30, lang="pt-br"), "trinta") + self.assertEqual(num2words(42, lang="pt-br"), "quarenta e dois") + self.assertEqual( + num2words(2024, lang="pt-br"), "dois mil e vinte e quatro" + ) + self.assertEqual(num2words(0, lang="pt-br"), "zero") + self.assertEqual(num2words(-1, lang="pt-br"), "menos um") + + +class TestDate(TestCase): + def test_convert_date_to_text(self): + self.assertEqual( + convert_date_to_text("15/08/2024"), + "Quinze de agosto de dois mil e vinte e quatro", + ) + self.assertEqual( + convert_date_to_text("01/01/2000"), + "Primeiro de janeiro de dois mil", + ) + self.assertEqual( + convert_date_to_text("31/12/1999"), + "Trinta e um de dezembro de mil novecentos e noventa e nove", + ) + + # + self.assertIsNone(convert_date_to_text("30/02/2020"), None) + self.assertIsNone(convert_date_to_text("30/00/2020"), None) + self.assertIsNone(convert_date_to_text("30/02/2000"), None) + self.assertIsNone(convert_date_to_text("50/09/2000"), None) + self.assertIsNone(convert_date_to_text("25/15/2000"), None) + + # Invalid date pattern. + self.assertRaises(ValueError, convert_date_to_text, "Invalid") + self.assertRaises(ValueError, convert_date_to_text, "25/1/2020") + self.assertRaises(ValueError, convert_date_to_text, "1924/08/20") + self.assertRaises(ValueError, convert_date_to_text, "5/09/2020") + + self.assertEqual( + convert_date_to_text("29/02/2020"), + "Vinte e nove de fevereiro de dois mil e vinte", + ) + self.assertEqual( + convert_date_to_text("01/01/1900"), + "Primeiro de janeiro de mil e novecentos", + )