Feed e Twitter

Feed RSS Twitter

Busca

Por Lustosa em 05/04/2009 às 16:11

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.

Comparação em ponto flutuante

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”.

Números começados com zero

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.

Busca de strings com strpos()

É 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.

Artigos relacionados

Arquivado em dicas, programação
Tags:

Feed RSS para os comentários deste artigo.


Um comentário em “Pegadinhas no PHP”

  1. Rondson Lima comentou:

    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!


Copyright 2009 Ataraxia!   Sinopse