Na maioria das (senão em todas as) linguagens mainstream, os números de ponto flutuante seguem a norma IEEE 754.
Este padrão possui uma série de problemas de imprecisão, por causa da forma como foi definido. No caso de um double
, o formato é:
- 1 bit para o sinal
- expoente: inteiro de 11 bits, para indicar o valor do expoente deslocado 1023 unidades, ou então um dos dois valores com significados especiais: 0x000 para valores subnormais e zero; 0x7FF para representar infinito e NaN. (i.e. 2x - 1023, onde x é o valor inteiro do campo)
- mantissa: 52 bits, para valores normais do expoente, representa um valor racional que vai de 1.0 inclusive até 2.0 exclusive, matematicamente [1, 2[. Para o valor do expoente 0x000, então representa valores subnormais (i.e. menores que o menor valor normal representável) ou zero; Para o valor do expoente 0x7FF representa infinito se for 0, ou NaN se for diferente de 0.
O problema é que a norma IEEE 754 tenta representar as frações na base 2, e isso não é suficiente para representar todos os valores possíveis na base 10. O máximo que ele pode fazer são aproximações.
Por exemplo, o número 0.1
na base decimal é representado como 1 x 10-1. Mas na base 2, não tem como representá-lo com exatidão. Se usarmos uma casa decimal, (0.1
, que seria 1 x 2-1) o resultado equivale a 0.5
em decimal (nada próximo de 0.1
). Se usarmos 8 casas decimais, ficaria 1.10011001 x 2-4, que equivale a 0.099853515625
em decimal. Mais próximo de 0.1
, mas ainda não é exato. Mesmo se usarmos 23 casas decimais, teremos 1.10011001100110011001101 × 2-4, que é equivalente a 0.100000001490116119384765625
em decimal. Próximo de 0.1
, mas ainda sim inexato.
E não adianta, por mais que adicionemos casas decimais na base 2, o máximo que conseguimos é aproximar um pouco mais o valor. Mas muitos valores jamais poderão ser representados com exatidão.
Não tem jeito de "resolver", é algo intrínseco à norma. Se quer mais precisão, precisa usar bibliotecas dedicadas. Muitas linguagens possuem tipos como Decimal
, BigDecimal
, etc. Outras não têm, e só é possível com bibliotecas externas.
Para saber mais, veja aqui, aqui e aqui.
Como vc mencionou Python, uma alternativa é usar o módulo decimal
, feito justamente para contornar os problemas de precisão do float
:
from decimal import Decimal as d
print(d('128.23') - (d('118.23') + d('10'))) # 0.00
Em PHP e JavaScript não tem nada nativo, então somente com bibliotecas externas.