Skip to content

Commit 736fd76

Browse files
feat(387): Conversão de Moeda Real para Texto (#525)
* feat(387): Adiciona método monetário 'convert_real_to_text' * Atualiza o arquivo CHANGELOG.md * Adiciona nota sobre o uso da lib num2words * usa Union[str,None] para compatibilidade de versões anteriores do python * fix readme arguments description * Apply suggestions from code review Co-authored-by: Camila Maia <cmaiacd@gmail.com> * conserta pequenos typos, add mais testes e atualiza docs sobre limitacao de precisao de centavos para grandes valores --------- Co-authored-by: Camila Maia <cmaiacd@gmail.com>
1 parent cec57bc commit 736fd76

6 files changed

Lines changed: 281 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- Utilitário `get_municipality_by_code` [412](https://github.com/brazilian-utils/brutils-python/pull/412)
1515
- Utilitário `get_code_by_municipality_name` [#399](https://github.com/brazilian-utils/brutils-python/issues/399)
1616
- Utilitário `format_currency` [#426](https://github.com/brazilian-utils/brutils-python/issues/426)
17+
- Utilitário `convert_real_to_text` [#387](https://github.com/brazilian-utils/brutils-python/pull/525)
1718

1819
## [2.2.0] - 2024-09-12
1920

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ False
9797
- [is_holiday](#is_holiday)
9898
- [Monetário](#monetário)
9999
- [format\_currency](#format_currency)
100+
- [convert\_real\_to\_text](#convert_real_to_text)
100101

101102
## CPF
102103

@@ -1254,6 +1255,42 @@ Exemplo:
12541255
None
12551256
```
12561257

1258+
### convert_real_to_text
1259+
1260+
Converte um valor monetário em reais para sua representação por extenso. Esta função recebe um número decimal representando um valor monetário em reais e o converte para uma string com o valor escrito por extenso em português do Brasil. Ela trata tanto a parte inteira (reais) quanto a parte fracionária (centavos), respeitando a gramática correta para os casos de singular e plural, bem como casos especiais como zero e valores negativos.
1261+
1262+
Argumentos:
1263+
- amount (decimal): O valor monetário a ser convertido por extenso.
1264+
- A parte inteira representa os reais.
1265+
- A parte decimal representa os centavos.
1266+
- 2 casas decimais.
1267+
1268+
Retorna:
1269+
- str: Uma string com o valor monetário escrito por extenso em português do Brasil.
1270+
- Retorna "Zero reais" para o valor 0,00.
1271+
- Retorna None se o valor for inválido ou absolutamente maior que 1 quatrilhão.
1272+
- Trata valores negativos, adicionando "Menos" no início da string.
1273+
1274+
Limitações:
1275+
- Esta função pode perder precisão em ±1 centavo para casos em que o valor absoluto
1276+
ultrapasse trilhões devido a erros de arredondamento de ponto flutuante.
1277+
1278+
Exemplo:
1279+
1280+
```python
1281+
>>> from brutils.currency import convert_real_to_text
1282+
>>> convert_real_to_text(1523.45)
1283+
'Mil, quinhentos e vinte e três reais e quarenta e cinco centavos'
1284+
>>> convert_real_to_text(0.01)
1285+
'Um centavo'
1286+
>>> convert_real_to_text(0.00)
1287+
'Zero reais'
1288+
>>> convert_real_to_text(-50.25)
1289+
'Menos cinquenta reais e vinte e cinco centavos'
1290+
>>> convert_real_to_text("invalid")
1291+
None
1292+
```
1293+
12571294
# Novos Utilitários e Reportar Bugs
12581295

12591296
Caso queira sugerir novas funcionalidades ou reportar bugs, basta criar

README_EN.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ False
9797
- [is_holiday](#is_holiday)
9898
- [Monetary](#monetary)
9999
- [format_currency](#format_currency)
100+
- [convert\_real\_to\_text](#convert_real_to_text)
100101

101102
## CPF
102103

@@ -1257,6 +1258,44 @@ Example:
12571258
None
12581259
```
12591260

1261+
### convert_real_to_text
1262+
1263+
Converts a given monetary value in Brazilian Reais to its textual representation. It takes a decimal number representing a monetary value in Reais and converts it to a string with the amount written out in Brazilian Portuguese. It handles both the integer part (Reais) and the fractional part (centavos), respecting the correct grammar for singular and plural cases, as well as special cases like zero and negative values.
1264+
1265+
Args:
1266+
1267+
- amount (decimal): The monetary value to be converted into text.
1268+
- The integer part represents Reais.
1269+
- The decimal part represents centavos.
1270+
- 2 decimal places
1271+
1272+
Returns:
1273+
1274+
- str: A string with the monetary value written out in Brazilian Portuguese.
1275+
- Returns "Zero reais" for a value of 0.00.
1276+
- Returns None if the amount is invalid or absolutely greater than 1 quadrillion.
1277+
- Handles negative values, adding "Menos" at the beginning of the string.
1278+
1279+
Limitations:
1280+
- This function may lose precision by ±1 cent for cases where the absolute value
1281+
is beyond trillions due to floating-point rounding errors.
1282+
1283+
Example:
1284+
1285+
```python
1286+
>>> from brutils.currency import convert_real_to_text
1287+
>>> convert_real_to_text(1523.45)
1288+
'Mil, quinhentos e vinte e três reais e quarenta e cinco centavos'
1289+
>>> convert_real_to_text(0.01)
1290+
'Um centavo'
1291+
>>> convert_real_to_text(0.00)
1292+
'Zero reais'
1293+
>>> convert_real_to_text(-50.25)
1294+
'Menos cinquenta reais e vinte e cinco centavos'
1295+
>>> convert_real_to_text("invalid")
1296+
None
1297+
```
1298+
12601299
# Feature Request and Bug Report
12611300

12621301
If you want to suggest new features or report bugs, simply create

brutils/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from brutils.cpf import remove_symbols as remove_symbols_cpf
2222

2323
# Currency
24-
from brutils.currency import format_currency
24+
from brutils.currency import convert_real_to_text, format_currency
2525

2626
# Date imports
2727
from brutils.date import convert_date_to_text
@@ -133,4 +133,5 @@
133133
"is_holiday",
134134
# Currency
135135
"format_currency",
136+
"convert_real_to_text",
136137
]

brutils/currency.py

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
from decimal import Decimal, InvalidOperation
1+
from decimal import ROUND_DOWN, Decimal, InvalidOperation
2+
from typing import Union
3+
4+
from num2words import num2words
25

36

47
def format_currency(value): # type: (float) -> str | None
@@ -38,3 +41,92 @@ def format_currency(value): # type: (float) -> str | None
3841
return formatted_value
3942
except InvalidOperation:
4043
return None
44+
45+
46+
def convert_real_to_text(amount: Decimal) -> Union[str, None]:
47+
"""
48+
Converts a given monetary value in Brazilian Reais to its textual representation.
49+
50+
This function takes a decimal number representing a monetary value in Reais
51+
and converts it to a string with the amount written out in Brazilian Portuguese. It
52+
handles both the integer part (Reais) and the fractional part (centavos), respecting
53+
the correct grammar for singular and plural cases, as well as special cases like zero
54+
and negative values.
55+
56+
Args:
57+
amount (decimal): The monetary value to be converted into text.
58+
- The integer part represents Reais.
59+
- The decimal part represents centavos.
60+
- 2 decimal places
61+
62+
Returns:
63+
str: A string with the monetary value written out in Brazilian Portuguese.
64+
- Returns "Zero reais" for a value of 0.00.
65+
- Returns None if the amount is invalid or absolutely greater than 1 quadrillion.
66+
- Handles negative values, adding "Menos" at the beginning of the string.
67+
68+
Limitations:
69+
- This function may lose precision by ±1 cent for cases where the absolute value
70+
is beyond trillions due to floating-point rounding errors.
71+
72+
Example:
73+
>>> convert_real_to_text(1523.45)
74+
"Mil, quinhentos e vinte e três reais e quarenta e cinco centavos"
75+
>>> convert_real_to_text(1.00)
76+
"Um real"
77+
>>> convert_real_to_text(0.50)
78+
"Cinquenta centavos"
79+
>>> convert_real_to_text(0.00)
80+
"Zero reais"
81+
>>> convert_real_to_text(-50.25)
82+
"Menos cinquenta reais e vinte e cinco centavos"
83+
"""
84+
85+
try:
86+
amount = Decimal(str(amount)).quantize(
87+
Decimal("0.01"), rounding=ROUND_DOWN
88+
)
89+
except InvalidOperation:
90+
return None
91+
92+
if amount.is_nan() or amount.is_infinite():
93+
return None
94+
95+
if abs(amount) > Decimal("1000000000000000.00"): # 1 quadrillion
96+
return None
97+
98+
negative = amount < 0
99+
amount = abs(amount)
100+
101+
reais = int(amount)
102+
centavos = int((amount - reais) * 100)
103+
104+
parts = []
105+
106+
if reais > 0:
107+
"""
108+
Note:
109+
Although the `num2words` library provides a "to='currency'" feature, it has known
110+
issues with the representation of "zero reais" and "zero centavos". Therefore, this
111+
implementation uses only the traditional number-to-text conversion for better accuracy.
112+
"""
113+
reais_text = num2words(reais, lang="pt_BR")
114+
currency_text = "real" if reais == 1 else "reais"
115+
conector = "de " if reais_text.endswith(("lhão", "lhões")) else ""
116+
parts.append(f"{reais_text} {conector}{currency_text}")
117+
118+
if centavos > 0:
119+
centavos_text = f"{num2words(centavos, lang='pt_BR')} {'centavo' if centavos == 1 else 'centavos'}"
120+
if reais > 0:
121+
parts.append(f"e {centavos_text}")
122+
else:
123+
parts.append(centavos_text)
124+
125+
if reais == 0 and centavos == 0:
126+
parts.append("Zero reais")
127+
128+
result = " ".join(parts)
129+
if negative:
130+
result = f"Menos {result}"
131+
132+
return result.capitalize()

tests/test_currency.py

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from decimal import Decimal
22
from unittest import TestCase
33

4-
from brutils.currency import format_currency
4+
from brutils.currency import convert_real_to_text, format_currency
55

66

77
class TestFormatCurrency(TestCase):
@@ -25,3 +25,111 @@ def test_when_value_is_not_a_valid_currency(self):
2525
assert format_currency("not a number") is None
2626
assert format_currency("09809,87") is None
2727
assert format_currency("897L") is None
28+
29+
30+
class TestConvertRealToText(TestCase):
31+
def test_convert_real_to_text(self):
32+
self.assertEqual(convert_real_to_text(0.00), "Zero reais")
33+
self.assertEqual(convert_real_to_text(0.01), "Um centavo")
34+
self.assertEqual(convert_real_to_text(0.50), "Cinquenta centavos")
35+
self.assertEqual(convert_real_to_text(1.00), "Um real")
36+
self.assertEqual(
37+
convert_real_to_text(-50.25),
38+
"Menos cinquenta reais e vinte e cinco centavos",
39+
)
40+
self.assertEqual(
41+
convert_real_to_text(1523.45),
42+
"Mil, quinhentos e vinte e três reais e quarenta e cinco centavos",
43+
)
44+
self.assertEqual(convert_real_to_text(1000000.00), "Um milhão de reais")
45+
self.assertEqual(
46+
convert_real_to_text(2000000.00), "Dois milhões de reais"
47+
)
48+
self.assertEqual(
49+
convert_real_to_text(1000000000.00), "Um bilhão de reais"
50+
)
51+
self.assertEqual(
52+
convert_real_to_text(2000000000.00), "Dois bilhões de reais"
53+
)
54+
self.assertEqual(
55+
convert_real_to_text(1000000000000.00), "Um trilhão de reais"
56+
)
57+
self.assertEqual(
58+
convert_real_to_text(2000000000000.00), "Dois trilhões de reais"
59+
)
60+
self.assertEqual(
61+
convert_real_to_text(1000000.45),
62+
"Um milhão de reais e quarenta e cinco centavos",
63+
)
64+
self.assertEqual(
65+
convert_real_to_text(2000000000.99),
66+
"Dois bilhões de reais e noventa e nove centavos",
67+
)
68+
self.assertEqual(
69+
convert_real_to_text(1234567890.50),
70+
"Um bilhão, duzentos e trinta e quatro milhões, quinhentos e sessenta e sete mil, oitocentos e noventa reais e cinquenta centavos",
71+
)
72+
73+
# Almost zero values
74+
self.assertEqual(convert_real_to_text(0.001), "Zero reais")
75+
self.assertEqual(convert_real_to_text(0.009), "Zero reais")
76+
77+
# Negative milions
78+
self.assertEqual(
79+
convert_real_to_text(-1000000.00), "Menos um milhão de reais"
80+
)
81+
self.assertEqual(
82+
convert_real_to_text(-2000000.50),
83+
"Menos dois milhões de reais e cinquenta centavos",
84+
)
85+
86+
# billions with cents
87+
self.assertEqual(
88+
convert_real_to_text(1000000000.01),
89+
"Um bilhão de reais e um centavo",
90+
)
91+
self.assertEqual(
92+
convert_real_to_text(1000000000.99),
93+
"Um bilhão de reais e noventa e nove centavos",
94+
)
95+
96+
self.assertEqual(
97+
convert_real_to_text(999999999999.99),
98+
"Novecentos e noventa e nove bilhões, novecentos e noventa e nove milhões, novecentos e noventa e nove mil, novecentos e noventa e nove reais e noventa e nove centavos",
99+
)
100+
101+
# trillions with cents
102+
self.assertEqual(
103+
convert_real_to_text(1000000000000.01),
104+
"Um trilhão de reais e um centavo",
105+
)
106+
self.assertEqual(
107+
convert_real_to_text(1000000000000.99),
108+
"Um trilhão de reais e noventa e nove centavos",
109+
)
110+
self.assertEqual(
111+
convert_real_to_text(9999999999999.99),
112+
"Nove trilhões, novecentos e noventa e nove bilhões, novecentos e noventa e nove milhões, novecentos e noventa e nove mil, novecentos e noventa e nove reais e noventa e nove centavos",
113+
)
114+
115+
# 1 quadrillion
116+
self.assertEqual(
117+
convert_real_to_text(1000000000000000.00),
118+
"Um quatrilhão de reais",
119+
)
120+
121+
# Edge cases should return None
122+
self.assertIsNone(
123+
convert_real_to_text("invalid_value")
124+
) # invalid value
125+
self.assertIsNone(convert_real_to_text(None)) # None value
126+
self.assertIsNone(
127+
convert_real_to_text(-1000000000000001.00)
128+
) # less than -1 quadrillion
129+
self.assertIsNone(
130+
convert_real_to_text(-1000000000000001.00)
131+
) # more than 1 quadrillion
132+
self.assertIsNone(convert_real_to_text(float("inf"))) # Infinity
133+
self.assertIsNone(
134+
convert_real_to_text(float("nan"))
135+
) # Not a number (NaN)

0 commit comments

Comments
 (0)