Depois de quase ficar maluco por causa de problemas com arredondamento de números com ponto flutuante, decidi escrever sobre alguns problemas que provavelmente muita gente já passou, e que continuam pegando muita gente.
As variáveis do tipo float sofrem de um problema de precisão, pois dependendo do número, ele pode não ter uma representação binária (em potências de 2) exata. Para nos resolver, o PHP (e a grande maioria das linguagens) faz o arredondamento do número. Por exemplo, se fizermos o seguinte teste:
<?php var_dump(0.1 + 0.2 == 0.3); // saída: bool(false) ?>
Claro que, matematicamente falando, 0.1 + 0.2 é igual a 0.3. Porém, a forma como o PHP guarda esses números internamente não é exata. Para vermos como o PHP guarda cada um desses números, podemos chamar a função serialize():
<?php print serialize(0.1); // d:0.1000000000000000055511151231257827021181583404541015625; print serialize(0.2); // d:0.200000000000000011102230246251565404236316680908203125; print serialize(0.3); // d:0.299999999999999988897769753748434595763683319091796875; ?>
E fazendo a conta, fica evidente que 0.1 + 0.2 não dá 0.3. E então, como fazer para comparar variáveis do tipo float sem chance de errarmos?
Uma forma é definir uma constante, que por convenção chamarei de EPSILON, que terá o valor do menor número fracionário possível. Qual valor colocar? Depende da aplicação! Se sua aplicação mexe com valores financeiros, normalmente você precisa se preocupar com frações de 2 ou 3 casas decimais no máximo. Podemos então definir o EPSILON, com uma boa margem de segurança, para 1e-5, ou seja, 0.00001.
Tendo esta constante, se queremos checar se dois números são iguais, podemos simplesmente checar se a diferença entre eles é menor que EPSILON. Caso seja menor, podemos considerar os números iguais. Da mesma forma, podemos fazer a comparação entre floats da mesma forma.
Uma função simples, que tem o retorno semelhante à função de comparação de strings strcmp(), mas que serve para comparar floats poderia ser definida assim:
<?php define("EPSILON", 1e-5); function floatcmp($a, $b) { $diff = abs($a-$b); if ($diff < EPSILON) { return 0; } return ($a<$b) ? -1 : 1; } ?>
Esta função recebe dois números como parâmetro. Se os números forem iguais, ou seja, se a diferença entre eles for menor que a nossa margem de erro aceitável EPSILON, a função retorna 0, caso contrário retorna -1 se o primeiro número for menor, ou então 1 caso o segundo seja o menor. Bastante simples e eficaz.
Para fazermos o teste acima, podemos agora proceder assim:
<?php if (floatcmp(0.1+0.2, 0.3) == 0) { print "são iguais"; } else { print "são diferentes"; } ?>
O resultado, como esperado, será “são iguais”.
Outro problema muito comum, principalmente quando estamos lidando com datas, é o da comparação de números iniciados com 0. Por exemplo:
<?php var_dump(09 == 9); // saída: bool(false) ?>
Isto ocorre porque o PHP trata qualquer número iniciado com 0 como um número octal, ou seja, na base 8. Caso o número não seja um octal válido, a conversão para no primeiro dígito inválido. No caso acima, 09 vira 0 (pois não existe 9 na base octal).
Para fazer esta comparação, uma das formas seria a seguinte:
<?php var_dump("09" == 9); // saída: bool(true) ?>
Agora, temos a string “09″, que será convertida no inteiro 9 (base 10 mesmo) antes da comparação, e aí sim, o resultado será o esperado.
É muito comum usarmos a função strpos() para buscarmos strings. O formato da função é:
int strpos (string $haystack, mixed $needle [, int $offset=0 ] )
onde haystack é onde estamos fazendo a busca, e needle é o que queremos encontrar. O retorno desta função é a posição onde a string foi encontrada, ou false caso ela não seja encontrada. O terceiro parâmetro, opcional, serve para informar a partir de que posição queremos fazer a busca.
Façamos o seguinte teste:
<?php if (strpos('abcdefg','abc')) { print "encontrei"; } else { print "não encontrei"; } ?>
A saída deste exemplo é “não encontrei”, mesmo com a string “abc” tendo sido encontrada na string “abcdefg”. O grande problema, neste exemplo, é que a string foi encontrada na posição 0. Logo, a função strpos() retorna 0. E o valor 0, em uma comparação booleana, é equivalente a false, e por isto o resultado acaba não sendo o esperado.
A forma correta de se fazer este teste é utilizando o operador ===, que além de checar o valor, também checa o tipo do retorno. Podemos fazer assim:
<?php if (strpos('abcdefg','abc') !== false) { print "encontrei"; } else { print "não encontrei"; } ?>
E agora, teremos o resultado esperado “encontrei”.
Existem várias outras “pegadinhas” no PHP (já escrevi por exemplo, sobre os operadores and e or, no artigo sobre precedência), que muitos podem considerar como bug ou algum problema sério da linguagem, mas a maior parte dos problemas tem uma explicação lógica.
Retirei parte dos exemplos de um post na Zend Developer Network, e como passei por problemas com floats recentemente, achei que seria interessante compartilhar.
Caso alguem tenha mais algumas “pegadinhas” para compartilhar, fique a vontade para fazer um comentário.
Feed RSS para os comentários deste artigo.
November 29th, 2009 às 10:22
Grande Lustosa,
Na minha última tentativa em busca de uma resposta quanto à comparação de números flutuantes, descobri esse post. Cara, foi de grande utilidade, realmente, uma PEGADINHA!