Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `convert_real_to_text` [#387](https://github.com/brazilian-utils/brutils-python/pull/525)

## [2.2.0] - 2024-09-12

Expand Down
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ False
- [is_holiday](#is_holiday)
- [Monetário](#monetário)
- [format\_currency](#format_currency)
- [convert\_real\_to\_text](#convert_real_to_text)

## CPF

Expand Down Expand Up @@ -1254,6 +1255,42 @@ Exemplo:
None
```

### convert_real_to_text

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.

Argumentos:
- amount (decimal): O valor monetário a ser convertido por extenso.
- A parte inteira representa os reais.
- A parte decimal representa os centavos.
- 2 casas decimais.

Retorna:
- str: Uma string com o valor monetário escrito por extenso em português do Brasil.
- Retorna "Zero reais" para o valor 0,00.
- Retorna None se o valor for inválido ou absolutamente maior que 1 quatrilhão.
- Trata valores negativos, adicionando "Menos" no início da string.

Limitações:
- Esta função pode perder precisão em ±1 centavo para casos em que o valor absoluto
ultrapasse trilhões devido a erros de arredondamento de ponto flutuante.

Exemplo:

```python
>>> from brutils.currency import convert_real_to_text
>>> convert_real_to_text(1523.45)
'Mil, quinhentos e vinte e três reais e quarenta e cinco centavos'
>>> convert_real_to_text(0.01)
'Um centavo'
>>> convert_real_to_text(0.00)
'Zero reais'
>>> convert_real_to_text(-50.25)
'Menos cinquenta reais e vinte e cinco centavos'
>>> convert_real_to_text("invalid")
None
```

# Novos Utilitários e Reportar Bugs

Caso queira sugerir novas funcionalidades ou reportar bugs, basta criar
Expand Down
39 changes: 39 additions & 0 deletions README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ False
- [is_holiday](#is_holiday)
- [Monetary](#monetary)
- [format_currency](#format_currency)
- [convert\_real\_to\_text](#convert_real_to_text)

## CPF

Expand Down Expand Up @@ -1257,6 +1258,44 @@ Example:
None
```

### convert_real_to_text

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.

Args:

- amount (decimal): The monetary value to be converted into text.
- The integer part represents Reais.
- The decimal part represents centavos.
- 2 decimal places

Returns:

- str: A string with the monetary value written out in Brazilian Portuguese.
- Returns "Zero reais" for a value of 0.00.
- Returns None if the amount is invalid or absolutely greater than 1 quadrillion.
- Handles negative values, adding "Menos" at the beginning of the string.

Limitations:
- This function may lose precision by ±1 cent for cases where the absolute value
is beyond trillions due to floating-point rounding errors.

Example:

```python
>>> from brutils.currency import convert_real_to_text
>>> convert_real_to_text(1523.45)
'Mil, quinhentos e vinte e três reais e quarenta e cinco centavos'
>>> convert_real_to_text(0.01)
'Um centavo'
>>> convert_real_to_text(0.00)
'Zero reais'
>>> convert_real_to_text(-50.25)
'Menos cinquenta reais e vinte e cinco centavos'
>>> convert_real_to_text("invalid")
None
```

# Feature Request and Bug Report

If you want to suggest new features or report bugs, simply create
Expand Down
3 changes: 2 additions & 1 deletion brutils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from brutils.cpf import remove_symbols as remove_symbols_cpf

# Currency
from brutils.currency import format_currency
from brutils.currency import convert_real_to_text, format_currency

# Date imports
from brutils.date import convert_date_to_text
Expand Down Expand Up @@ -133,4 +133,5 @@
"is_holiday",
# Currency
"format_currency",
"convert_real_to_text",
]
94 changes: 93 additions & 1 deletion brutils/currency.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from decimal import Decimal, InvalidOperation
from decimal import ROUND_DOWN, Decimal, InvalidOperation
from typing import Union

from num2words import num2words


def format_currency(value): # type: (float) -> str | None
Expand Down Expand Up @@ -38,3 +41,92 @@ def format_currency(value): # type: (float) -> str | None
return formatted_value
except InvalidOperation:
return None


def convert_real_to_text(amount: Decimal) -> Union[str, None]:
"""
Converts a given monetary value in Brazilian Reais to its textual representation.

This function 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.

Args:
amount (decimal): The monetary value to be converted into text.
- The integer part represents Reais.
- The decimal part represents centavos.
- 2 decimal places

Returns:
str: A string with the monetary value written out in Brazilian Portuguese.
- Returns "Zero reais" for a value of 0.00.
- Returns None if the amount is invalid or absolutely greater than 1 quadrillion.
- Handles negative values, adding "Menos" at the beginning of the string.

Limitations:
- This function may lose precision by ±1 cent for cases where the absolute value
is beyond trillions due to floating-point rounding errors.

Example:
>>> convert_real_to_text(1523.45)
"Mil, quinhentos e vinte e três reais e quarenta e cinco centavos"
>>> convert_real_to_text(1.00)
"Um real"
>>> convert_real_to_text(0.50)
"Cinquenta centavos"
>>> convert_real_to_text(0.00)
"Zero reais"
>>> convert_real_to_text(-50.25)
"Menos cinquenta reais e vinte e cinco centavos"
"""

try:
amount = Decimal(str(amount)).quantize(
Decimal("0.01"), rounding=ROUND_DOWN
)
except InvalidOperation:
return None

if amount.is_nan() or amount.is_infinite():
Comment thread
camilamaia marked this conversation as resolved.
return None

if abs(amount) > Decimal("1000000000000000.00"): # 1 quadrillion
return None

negative = amount < 0
amount = abs(amount)

reais = int(amount)
centavos = int((amount - reais) * 100)

parts = []

if reais > 0:
"""
Note:
Although the `num2words` library provides a "to='currency'" feature, it has known
issues with the representation of "zero reais" and "zero centavos". Therefore, this
implementation uses only the traditional number-to-text conversion for better accuracy.
"""
reais_text = num2words(reais, lang="pt_BR")
currency_text = "real" if reais == 1 else "reais"
conector = "de " if reais_text.endswith(("lhão", "lhões")) else ""
parts.append(f"{reais_text} {conector}{currency_text}")

if centavos > 0:
centavos_text = f"{num2words(centavos, lang='pt_BR')} {'centavo' if centavos == 1 else 'centavos'}"
if reais > 0:
parts.append(f"e {centavos_text}")
else:
parts.append(centavos_text)

if reais == 0 and centavos == 0:
parts.append("Zero reais")

result = " ".join(parts)
if negative:
result = f"Menos {result}"

return result.capitalize()
110 changes: 109 additions & 1 deletion tests/test_currency.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from decimal import Decimal
from unittest import TestCase

from brutils.currency import format_currency
from brutils.currency import convert_real_to_text, format_currency


class TestFormatCurrency(TestCase):
Expand All @@ -25,3 +25,111 @@ def test_when_value_is_not_a_valid_currency(self):
assert format_currency("not a number") is None
assert format_currency("09809,87") is None
assert format_currency("897L") is None


class TestConvertRealToText(TestCase):
def test_convert_real_to_text(self):
self.assertEqual(convert_real_to_text(0.00), "Zero reais")
self.assertEqual(convert_real_to_text(0.01), "Um centavo")
self.assertEqual(convert_real_to_text(0.50), "Cinquenta centavos")
self.assertEqual(convert_real_to_text(1.00), "Um real")
self.assertEqual(
convert_real_to_text(-50.25),
"Menos cinquenta reais e vinte e cinco centavos",
)
self.assertEqual(
convert_real_to_text(1523.45),
"Mil, quinhentos e vinte e três reais e quarenta e cinco centavos",
)
self.assertEqual(convert_real_to_text(1000000.00), "Um milhão de reais")
self.assertEqual(
convert_real_to_text(2000000.00), "Dois milhões de reais"
)
self.assertEqual(
convert_real_to_text(1000000000.00), "Um bilhão de reais"
)
self.assertEqual(
convert_real_to_text(2000000000.00), "Dois bilhões de reais"
)
self.assertEqual(
convert_real_to_text(1000000000000.00), "Um trilhão de reais"
)
self.assertEqual(
convert_real_to_text(2000000000000.00), "Dois trilhões de reais"
)
self.assertEqual(
convert_real_to_text(1000000.45),
"Um milhão de reais e quarenta e cinco centavos",
)
self.assertEqual(
convert_real_to_text(2000000000.99),
"Dois bilhões de reais e noventa e nove centavos",
)
self.assertEqual(
convert_real_to_text(1234567890.50),
"Um bilhão, duzentos e trinta e quatro milhões, quinhentos e sessenta e sete mil, oitocentos e noventa reais e cinquenta centavos",
)

# Almost zero values
self.assertEqual(convert_real_to_text(0.001), "Zero reais")
self.assertEqual(convert_real_to_text(0.009), "Zero reais")

# Negative milions
self.assertEqual(
convert_real_to_text(-1000000.00), "Menos um milhão de reais"
)
self.assertEqual(
convert_real_to_text(-2000000.50),
"Menos dois milhões de reais e cinquenta centavos",
)

# billions with cents
self.assertEqual(
convert_real_to_text(1000000000.01),
"Um bilhão de reais e um centavo",
)
self.assertEqual(
convert_real_to_text(1000000000.99),
"Um bilhão de reais e noventa e nove centavos",
)

self.assertEqual(
convert_real_to_text(999999999999.99),
"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",
)

# trillions with cents
self.assertEqual(
convert_real_to_text(1000000000000.01),
"Um trilhão de reais e um centavo",
)
self.assertEqual(
convert_real_to_text(1000000000000.99),
"Um trilhão de reais e noventa e nove centavos",
)
self.assertEqual(
convert_real_to_text(9999999999999.99),
"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",
)

# 1 quadrillion
self.assertEqual(
convert_real_to_text(1000000000000000.00),
"Um quatrilhão de reais",
)

# Edge cases should return None
self.assertIsNone(
convert_real_to_text("invalid_value")
) # invalid value
self.assertIsNone(convert_real_to_text(None)) # None value
self.assertIsNone(
convert_real_to_text(-1000000000000001.00)
) # less than -1 quadrillion
self.assertIsNone(
convert_real_to_text(-1000000000000001.00)
) # more than 1 quadrillion
self.assertIsNone(convert_real_to_text(float("inf"))) # Infinity
self.assertIsNone(
convert_real_to_text(float("nan"))
) # Not a number (NaN)