Executando verificação de segurança...
29

Saiba porquê não usar floats para representar Reais, Dólares, etc

Você está construindo um sistema que calcula impostos ou um app para finanças pessoais ou qualquer outro sistema que faça cálculos usando valores monetários, como calcular o valor total multiplicando pela quantidade de produtos na cesta de compras. Para essa tarefa, a escolha mais óbvia para um iniciante é representar o dinheiro como um float, pois um inteiro não serviria para representar os centavos de um certo valor em reais.

O problema

Vou usar Python nesses exemplos, mas a maioria das linguagens de programação produziria resultados semelhantes.

Vamos calcular o preço total de 3 salgados que custam R$1,10 cada:

preco_salgado = 1.1
quantidade = 3
total = preco_salgado * quantidade
# Esperava 3.3, mas retorna 3.3000000000000003 :(

Mas o que esta acontecendo aqui? Este resultado ficaria horrível em um carrinho de compras!

Mesmo uma simples adição leva a um resultado bizarro:

base_bonus = 0.2
bonus_adicional = 0.1
bonus_total = bonus_base + bonus_adicional
# Esperava 0.3, mas retorna 0.30000000000000004 :(

Você pode estar pensando que a solução é realmente simples, basta cortar após dois dígitos!

bonus_total = 0.1 + 0.2
# 0.30000000000000004
total_bonus_corrigido = round(bonus_total, 2)
# 0.3

Isso pode funcionar para uma conta realmente simples, mas se você continuar fazendo esses cálculos com floats e arredondando repetidamente, pequenos erros de arredondamento se acumularão e levarão a um grande erro. Isso não será aceitável para um aplicativo que precisa de precisão e o arredondamento pode levar a erros como este:

minimo_aprovacao = 5.0
resultado_partido = 4.97 # o partido estaria fora do parlamento
resultado_partido = round(resultado_partido, 1)
# resultado_partido agora é 5.0
if resultado_partido >= minimo_aprovacao:
     print('Você pode participar do Parlamento')
else:
     print('Você não pode mais se sentar no Parlamento')

# Retorna: Você pode participar do Parlamento

Nas eleições parlamentares alemãs, um partido com menos de 5,0% dos votos não pode participar do parlamento. O partido Verde parecia ter conseguido 5,0%, até que se descobriu (após o anúncio dos resultados) que, na verdade, eles tinham apenas 4,97%. A impressão do resultado tinha um limite de dois dígitos e a porcentagem foi arredondada para 5,0%.

Desastres já aconteceram devido à perda de precisão ao converter inteiros para floats:

Em 25 de fevereiro de 1991, durante a Guerra do Golfo, um sistema de defesa antimísseis americano Patriot acabou deixando passar um míssel iraquiano Scud. Ele atingiu uma base americana, matando 28 pessoas. O problema estava na perda de precisão na conversão de segundos em inteiro para float, que foi o suficiente para que o sistema falhasse em atingir um míssel viajando em alta velocidade. Mais detalhes aqui.

Você pode até falhar em um teste automatizado simples e passar horas tentando descobrir o motivo:

assert 0.1 + 0.2 == 0.3
# Gera AssertionError!

Por que isso acontece?

Os computadores usam zeros e uns para representar números. Um bit pode ser um zero ou um. Computadores modernos costumam usar até 64 bits para representar informações, o que significa que você pode ter até 15 ou 17 dígitos após a vírgula, o que é muito, mas ainda há um limite para os valores que é possível representar.

Como humanos, podemos entender facilmente o conceito de 0,1. É um décimo de alguma coisa ou 10% de alguma coisa. Mas os computadores que usam zeros e uns representarão 0,1 como:

 0.0001100110011...

Em que a parte 0011 estará se repetindo infinitamente.

Mas um computador de 64 bits não consegue armazenar infinitos decimais, de modo que terá apenas 15 ou 17 dígitos após o ponto decimal. Nesse processo de arredondamento de um número infinito para os dígitos de precisão permitidos, ele perde a precisão. Portanto, 0,1 que foi ajustado em dados de 64 bits será uma aproximação, mas não será exatamente 0,1.

Se revelarmos os 20 dígitos que seguem o sinal decimal em um float:

decimal_um = 0.1
print(f'{decimal_one:.20f}') # Na verdade é 0.10000000000000000555
decimal_dois = 0.2
print(f"{decimal_two:.20f}") # Na verdade é 0.20000000000000001110

Podemos ver que, de fato, estamos obtendo apenas o valor aproximado de um número decimal, não o exato. E é por isso que você pode ver esses resultados inesperados usando floats.

Solução 1: Use o módulo Decimal

Python tem um módulo chamado Decimal, que se comporta como um humano faria um cálculo matemático:

from decimal import Decimal

preco_salgado = Decimal('1.1')
quantidade = 3
total = preco_salgado * quantidade # Decimal('3.3')
print(total)
# 3.3

Observe que é possível fazer operações matemáticas com esses objetos Decimais:

Decimal('0.1') + Decimal('0.2')
# decimal('0.3')

Ao criar objetos Decimais, o valor float deve ser passado como uma string, caso contrário, você sofreria novamente com problemas de precisão que queríamos evitar:

Decimal(0.1) # Decimal('0.1000000000000000055511151231257827021181583404541015625')

Decimal('0.1') # Decimal('0.1')

Ao criar objetos Decimais, é possível customizar o nível de precisão, regras de arredondamento, etc.

Solução 2: use números inteiros

É possível usar números inteiros para armazenar os valores e fazer os cálculos. Somente quando houver a necessidade de mostrar os resultados para um humano que ele será dividido por 100:

preco_salgado = 110
quantidade = 3
total = preco_salgado * quantidade # 330
print(total/100)
# 3.3

Dessa forma, nos manteremos na segurança dos números inteiros.

A desvantagem dessa solução é na hora de calcular um número com um grande número de dígitos. Float é representado como notação científica, por isso é muito rápido fazer cálculos com um enorme número de dígitos, como o tamanho do universo ou átomos que compõem o Sol, mas ao usar números inteiros podemos esperar que o desempenho nos cálculos fique muito mais lento ou até impossível devido ao número de dígitos não ser comportado por um inteiro.

Espero que você tenha gostado do artigo e boa sorte nos cálculos com decimais!

Carregando publicação patrocinada...
5

A segunda solução funciona, desde que você garanta que sempre está trabalhando com inteiros. Uma boneada e aparecer algum float na operação, memso sem você perceber, aí começa dar errado.

Pode usar, tem quem usa, mas é preciso extremo cuidado. Na verdade tenho medo que em alguns casos funcionem. Isso parece algum tratamento especial do Python, mas que você não sabe se vai funcionar ou não. É quase como andar sobre ovos.

Veja acontecendo em algo simples: https://ideone.com/sHrSVt

Obrigado pelo artigo, é sempre importante destacar essas coisas, porque sem aprender de forma estruturada essas coisas passam batido pela maioria das pessoas. E em muito lugar, até mesmo de experientes, você vê o erro sendo cometido.

Pode ajudar: Existe diferença entre “exatidão” e “precisão” em contextos de computação?.

Farei algo que muitos pedem para aprender programar corretamente, gratuitamente. Para saber quando, me segue nas suas plataformas preferidas. Quase não as uso, não terá infindas notificações (links aqui).

3

Outra desvantagem do segundo exemplo é que a partir do momento que vc fez uma divisão, o float já se "infiltrou" no meio do cálculo. E aí tudo pode acontecer.

Por exemplo, se quiser voltar ao valor original, nem sempre dará certo:

total = 255
total_float = total / 100
# multiplica por 100 para voltar ao total original
print(total_float * 100) # 254.99999999999997
print(total_float * 100 == total) # False (são diferentes) - para outros valores, pode dar True

# curiosamente, se imprimir assim, ele arredonda pra duas casas e fica "certo"
print(total_float) # 2.55
# mas basta imprimir mais casas decimais pra ver que o valor está errado
print(f'{total_float:.20f}') # 2.54999999999999982236

Nesse caso, o módulo decimal é o mais indicado. Outras vantagens dele é poder configurar a precisão (quantidade de casas decimais), definir modos de arredondamento que são aplicados automaticamente a todos os cálculos, etc.

1
2

Isso é porque TS usa os mesmos tipos do JS, no qual o tipo Number é sempre um número de ponto flutuante (não há a separação entre int e float que tem em outras linguagens).

0
2
2

É extremamente comum a pessoa fazer um teste e achar que porque funcionou está certo. Muitas pessoas brigaram comigo dizendo que está certo porque viram funcionar. Alguns até dizem que se usar double fica tudo bem porque ele pega um exemplo que o float dava errado e no double não dá mais. O float já é perigoso porque ele não mostra o erro de cara na maioria das vzes, o double esconde por mais tempo ainda. Erro bom é oque o compilador te dá ou no máximo o runtime tem dá com toda clareza na primeira execução.

Fiat 147 todo detonado andando pelas ruas

Eu acho que linguagens mais modernas, algumas pelo menos, que assumem uma pegada mais comercial, mais ainda as que usam um tipo numérico universal, deveriam ter um tipo decimal como padrão e quem sabe ter um de otimização de ponto flutuante binário, já ajudaria muito.

Ao contrário da crença popular algumas implmentações de decimal são tão ou mais rápidos que o binário em boa parte das operações.

2
1
1

Muito bom mano. A gente pensa que não precisa, mas me deparei com esse problema recentemente. Onde estou construindo meu primeiro app comercial, e nele tem um sistema de compras bem simples, mas ainda assim preciso calcular os valores dos itens do pedido, e aí tive que pesquisar um pouco sobre como fazer o storage de valores monetários. Uso o Prisma ORM com Postgres e o prisma já tem um módulo de decimal com a flag @money para representar isso direitinho. Ótimo post.

1

Sempre opto pela solução número 2 quando são dados transacionais, como por exemplo operações financeiras...
Esse post é bem pertinente, pois achamos que as vezes só pequenas empresas sofrem com esse tipo de coisa, o roxinho teve problemas com isso utilizando transações monetárias com vírgula, enfim boa contribuição ❤️

2

O problema do "banco roxinho" foi usar float, descobriram na prática os problemas de arredondamento que ele possui:

https://tecnoblog.net/noticias/2022/02/16/nubank-tem-bug-que-nao-deixa-transferir-r-1799-e-mais-3-valores-via-pix/


O que mais me assusta nesse caso é ter uma grande fintech cometendo esse tipo de erro básico (afinal, operações financeiras são o core business dela, uma falha dessa é inadmissível). Se fosse o "sistema do sobrinho" que só roda na vendinha do bairro, seria menos grave (com todo respeito à vendinha, inclusive a do meu bairro eu gosto bastante).

1

Muito bom! Pra JavaScript tem essa biblioteca https://mikemcl.github.io/decimal.js/. Muito boa também.

const Decimal = require('decimal.js');

const preco_salgado = new Decimal('1.1');
const quantidade = 3;
const total = preco_salgado.times(quantidade); // new Decimal('3.3') - método "times" para multiplicar
console.log(total.toString());
1
1

Que conhecimento massa, eu já tinha notado os pequenos erros que aconteciam com essa questão de arredondar, mas nunca tinha de fato procurado uma explicação e solução pra isso.

1
3

Tem sim, leia mais abaixo outra resposta que e o kht e eu escrevemos. Certamente ele tem um meio de não cair nessa armadilha, qualquer libguagem tem, e a maioria tem algo pronto pelo menos na biblioteca padrão ou tem alguma externa. Algumas linguagens adotam um padrão diferente que pode ajudar ou adotam alguma forma para tentar compensar, mas uma hora estoura, e a pessoa fica sem saber.

Não vi detalhes mas pode ser que o Delphi adote um ponto flutuante decimal em vez de bunário por padrão, só isso. Mas o mais provável é que o site engane todo mundo fazendo uma análise muito superficial. Algumas linguagens tem uma compensação em certos pontos, como a função de impressão por exemplo, mas não para cálculos.

Até fiquei curioso e vou ver se aina acho que programador de Deplhi que explique o que acontece ali com certeza.

Conheço bem C# e o segundo exemplo acontece só na impressão, em qualquer situação que exija exatidão tem que usar o primeiro ou terceiro.

Conclusão, esse site é um perigo, só perpetua o mito. Como muita coisa na internet.

Como visto na Internet - logo

2

Pois é, tem linguagens que quando vc imprime o valor, ele acaba sendo truncado/arredondado e isso acaba mascarando o problema (e dando a falsa impressão de que ele "não existe").

É o que aconteceu neste exemplo que fiz em Python: o problema só apareceu quando fui usar o valor, seja fazendo cálculos, ou comparando com outro número. E para descobrir o que de fato acontece, precisei imprimir mais casas decimais.

Esse é o perigo de se fazer um teste simples e achar que tudo bem só porque "funcionou". O mais correto é consultar a documentação da linguagem: se lá é dito que ela usa números de ponto flutuante binários (e principalmente se citar a norma IEEE 754), então os problemas já citados podem acontecer sim. Pode não ter acontecido em um teste específico (ou o teste foi mal feito porque usou uma situação na qual a linguagem mascarou o erro), mas não quer dizer que nunca vai acontecer.

1

Maniero, você me salva mt lá no SO kkkkkkkkk, enfim, o que eu quis dizer é que não há necessidade de utilizar uma biblioteca como a Decimal do js, pois o Delphi já se encarrega disso.

1

Sim pode ser sim, mas eu só vou confirmar isso com uma informação que demonstre claramente, e não seja só impressão, como acontece com o C#.

Obrigado.

1
1
2

De certa forma, foi resolvido sim.

Muitas linguagens possuem algum tipo nativo, geralmente chamado Decimal, BigDecimal, Money, Currency, etc, que não possui esses problemas. Quando não tem, geralmente já existe alguma lib que implementa esses tipos.

1

Perfeito!

Eu desenvolvo com javascript e vi que tem uma lib para isso, acharia interessante se tivesse alguma solução nativa, ainda assim, é muito bom saber desse problema, obrigado pelo post :)

1
1
1

Nossa, amigo. Quer artigo top. Todas as vezes que ia construir algo relacionado a valores flutuantes sempre usava double ou float. Agora vou passar a tratar esses dados corretamente

Obrigado por compartilhar esse conhecimento!

1

É nessas horas que eu percebo o quanto ainda sou leigo mesmo depois de quase 6 anos trabalhando na área, eu sabia que o float tinha alguns pontos que demandavam atenção, principalmente no JS, porém não sabia que era tão comum assim. Sempre ouvi e recomendei que quando se tratasse da manipulação de valores monetário, fazer a divisão por 100 e tratar como integer, que aí estaria manipulando diretamente os centavos, porém em operações complexas poderia resultar um float novamente. Enfim, ficarei ainda mais atento a esse ponto de manipulação de floats, parabéns ao criador pelo artigo.