PHP 7.2.0 Release Candidate 4 Released

Números de ponto flutuante

Números de ponto flutuante (também conhecidos como "floats", "doubles" ou "números reais"), podem ser especificados utilizando qualquer uma das seguintes sintaxes:

<?php
$a 
1.234;
$b 1.2e3;
$c 7E-10;
?>

Formalmente:

LNUM          [0-9]+
DNUM          ([0-9]*[\.]{LNUM}) | ({LNUM}[\.][0-9]*)
EXPONENT_DNUM [+-]?(({LNUM} | {DNUM}) [eE][+-]? {LNUM})

O tamanho de um número de ponto flutuante depende da plataforma, sendo o máximo de ~1.8e308 com precisão de 14 dígitos decimais um valor comum (número de 64 bits no formato IEEE).

Aviso

Precisão de números de ponto flutuante

Números de ponto flutuante tem precisão limitada. Embora dependa do sistema, o PHP geralmente utiliza o formato de precisão dupla do IEEE 754, que trará uma precisão máxima devida a arredondamentos da ordem de 1.11e-16. Operações matemáticas incomuns poderão ocasionar erros maiores, e, claro, a propagação de erros deve ser considerada quando várias operações forem realizadas.

Além disso, números racionais que tem representação exata em números em base 10, como 0.1 ou 0.7, não possuem representação exata em ponto flutuante na base 2, o formato utilizado internamente, não importando o tamanho da mantissa. Portanto não existe conversão para o formato interno sem uma pequena perda de precisão. Isso pode ocasionar resultados confusos: por exemplo, floor((0.1+0.7)*10) normalmente retornará 7, em vez do resultado esperado 8, porque a representação interna final será algo como 7.9999999999999991118....

Então, nunca confie em resultados com números de ponto flutuante até a última casa, e nunca compare números de ponto flutuante em igualdades. Se você realmente precisar de alta precisão, você pode utilizar as funções matemáticas de precisão arbitrária e as funções gmp estão disponíveis.

Para uma explicação "simples" dessa questão, veja o » guia sobre ponto flutuante, que também tem o título alternativo de "Porque meus números não somam direito?".

Convertendo para float

Para informações sobre a conversão de strings para float, veja a seção Conversão de Strings para números. Para valores de outros tipos, o valor é primeiro convertido para inteiro e então para float. Veja a seção Convertendo para inteiros para mais informações. No PHP 5, um aviso é emitido se você tentar converter um object para um float.

Comparando floats

Como notado acima, testar números de ponto flutuante com igualdade é problemático, por causa da maneira como são representados internamente. Entretanto existem maneiras de fazer comparações com números de ponto flutuante que contornam essas limitações.

Para testar números de ponto flutuante, utilize um "valor de erro máximo" na comparação utilizada. Esse valor é também chamado de epsilon, ou unidade de erro, e deve ser a diferença mínima aceitável no resultado dos cálculos.

$a e $b serão consideradas iguais até o 5º dígito de precisão.

<?php
$a 
1.23456789;
$b 1.23456780;
$epsilon 0.00001;

if(
abs($a-$b) < $epsilon) {
    echo 
"iguais";
}
?>

NaN

Algumas operações numéricas podem resultar em valores representados pela constante NAN. Esse resultado representa um valor desconhecido ou não representável nos cálculos de ponto flutuante. Qualquer comparação frouxa ou restrita deste valor com qualquer outro, inclusive ele mesmo, terá como resultado FALSE.

Como o NAN representa um resultado irrepresentável, NAN não deve ser comparado com outros valores, incluindo si mesmo, em vez disso, deve-se checá-lo utilizando a função is_nan().

add a note add a note

User Contributed Notes 33 notes

up
102
catalin dot luntraru at gmail dot com
3 years ago
$x = 8 - 6.4;  // which is equal to 1.6
$y = 1.6;
var_dump($x == $y); // is not true

PHP thinks that 1.6 (coming from a difference) is not equal to 1.6. To make it work, use round()

var_dump(round($x, 2) == round($y, 2)); // this is true

This happens probably because $x is not really 1.6, but 1.599999.. and var_dump shows it to you as being 1.6.
up
49
www.sarioz.com
14 years ago
just a comment on something the "Floating point precision" inset, which goes: "This is related to .... 0.3333333."

While the author probably knows what they are talking about, this loss of precision has nothing to do with decimal notation, it has to do with representation as a floating-point binary in a finite register, such as while 0.8 terminates in decimal, it is the repeating 0.110011001100... in binary, which is truncated.  0.1 and 0.7 are also non-terminating in binary, so they are also truncated, and the sum of these truncated numbers does not add up to the truncated binary representation of 0.8 (which is why (floor)(0.8*10) yields a different, more intuitive, result).  However, since 2 is a factor of 10, any number that terminates in binary also terminates in decimal.
up
26
feline at NOSPAM dot penguin dot servehttp dot com
13 years ago
General computing hint: If you're keeping track of money, do yourself and your users the favor of handling everything internally in cents and do as much math as you can in integers. Store values in cents if at all possible. Add and subtract in cents. At every operation that wii involve floats, ask yourself "what will happen in the real world if I get a fraction of a cent here" and if the answer is that this operation will generate a transaction in integer cents, do not try to carry fictional fractional accuracy that will only screw things up later.
up
2
lwiwala at gmail dot com
7 months ago
To compare two numbers use:

$epsilon = 1e-6;

if(abs($firstNumber-$secondNumber) < $epsilon){
   // equals
}
up
16
backov at spotbrokers-nospamplz dot com
14 years ago
I'd like to point out a "feature" of PHP's floating point support that isn't made clear anywhere here, and was driving me insane.

This test (where var_dump says that $a=0.1 and $b=0.1)

if ($a>=$b) echo "blah!";

Will fail in some cases due to hidden precision (standard C problem, that PHP docs make no mention of, so I assumed they had gotten rid of it). I should point out that I originally thought this was an issue with the floats being stored as strings, so I forced them to be floats and they still didn't get evaluated properly (probably 2 different problems there).

To fix, I had to do this horrible kludge (the equivelant of anyway):

if (round($a,3)>=round($b,3)) echo "blah!";

THIS works. Obviously even though var_dump says the variables are identical, and they SHOULD BE identical (started at 0.01 and added 0.001 repeatedly), they're not. There's some hidden precision there that was making me tear my hair out. Perhaps this should be added to the documentation?
up
9
Luzian
11 years ago
Be careful when using float values in strings that are used as code later, for example when generating JavaScript code or SQL statements. The float is actually formatted according to the browser's locale setting, which means that "0.23" will result in "0,23". Imagine something like this:

$x = 0.23;
$js = "var foo = doBar($x);";
print $js;

This would result in a different result for users with some locales. On most systems, this would print:

var foo = doBar(0.23);

but when for example a user from Germany arrives, it would be different:

var foo = doBar(0,23);

which is obviously a different call to the function. JavaScript won't state an error, additional arguments are discarded without notice, but the function doBar(a) would get 0 as parameter. Similar problems could arise anywhere else (SQL, any string used as code somewhere else). The problem persists, if you use the "." operator instead of evaluating the variable in the string.

So if you REALLY need to be sure to have the string correctly formatted, use number_format() to do it!
up
8
zelko at mojeime dot com
6 years ago
<?php
   $binarydata32
= pack('H*','00000000');
  
$float32 = unpack("f", $binarydata32); // 0.0

  
$binarydata64 = pack('H*','0000000000000000');
  
$float64 = unpack("d", $binarydata64); // 0.0
?>

I get 0 both for 32-bit and 64-bit numbers.

But, please don't use your own "functions" to "convert" from float to binary and vice versa. Looping performance in PHP is horrible. Using pack/unpack you use processor's encoding, which is always correct. In C++ you can access the same 32/64 data as either float/double or 32/64 bit integer. No "conversions".

To get binary encoding:
<?php
   $float32
= pack("f", 5300231);
  
$binarydata32 =unpack('H*',$float32); //"0EC0A14A"

  
$float64 = pack("d", 5300231);
  
$binarydata64 =unpack('H*',$float64); //"000000C001385441"
?>

And my example from half a year ago:
<?php
    $binarydata32
= pack('H*','0EC0A14A');
   
$float32 = unpack("f", $binarydata32); // 5300231
  
   
$binarydata64 = pack('H*','000000C001385441');
   
$float64 = unpack("d", $binarydata64); // 5300231
?>

And please mind the Big and Little endian boys...
up
8
magicaltux at php dot net
7 years ago
In some cases you may want to get the maximum value for a float without getting "INF".

var_dump(1.8e308); will usually show: float(INF)

I wrote a tiny function that will iterate in order to find the biggest non-infinite float value. It comes with a configurable multiplicator and affine values so you can share more CPU to get a more accurate estimate.

I haven't seen better values with more affine, but well, the possibility is here so if you really thing it's worth the cpu time, just try to affine more.

Best results seems to be with mul=2/affine=1. You can play with the values and see what you get. The good thing is this method will work on any system.

<?php
 
function float_max($mul = 2, $affine = 1) {
   
$max = 1; $omax = 0;
    while((string)
$max != 'INF') { $omax = $max; $max *= $mul; }

    for(
$i = 0; $i < $affine; $i++) {
     
$pmax = 1; $max = $omax;
      while((string)
$max != 'INF') {
       
$omax = $max;
       
$max += $pmax;
       
$pmax *= $mul;
      }
    }
    return
$omax;
  }
?>
up
6
m dot lebkowski+php at gmail dot com
9 years ago
Just another note about the locales. Consider the following code:

<?php
   
// in polish locale decimal separator is ","
   
setlocale(LC_ALL, "pl_PL");
   
$a = 5/2;
    echo (float)(string)
$a;
   
/// prints "2", so the decimal part is dropped
?>

This causes very serious problems in my opinion. In some locale combination the typecasting can be destructive.
Maybe when locale decimal separator is ",", then (float)"2,5" should be recognized as "two and a half"?
Anyway - bare that in mind and be very careful when casting floats to strings and back.
up
7
dev at maintainfit dot com
14 years ago
I was programming an accounting application in MySql that required me to sum a collection of floats and ensure that they equal zero before commiting a transaction, but as seen above a sum of floats cannot always be trusted (as was my case).  I kept getting a very small remainder (like 1.4512431231e-14).  Since I had used number_format(num,2) to set the precision of the numbers in the database to only two (2) decimal places, when the time comes to calculate the sum I simply multiply every number by ten (10), therby eliminating and decimal places and leaving me with integers to preform my sum.  This worked great.
up
4
kjohnson at zootweb dot com
9 years ago
PHP switches from the standard decimal notation to exponential notation for certain "special" floats. You can see a partial list of such "special" values with this:

<?php
for( $tmp = 0, $i = 0; $i < 100; $i++ ) {
   
$tmp += 100000;
    echo
round($tmp),"\n";
}
?>

So, if you add two floats, end up with a "special" value, e.g. 1.2E+6, then put that value unmodified into an update query to store the value in a decimal column, say, you will likely get a failed transaction, since the database will see "1.2E+6" as varchar data, not decimal. Likewise, you will likely get an XSD validation error if you put the value into xml.

I have to be honest: this is one of the strangest things I have seen in any language in over 20 years of coding, and it is a colossal pain to work around.
up
3
Adam H
3 years ago
I've just come across this issue with floats when writing a function for pricing. When converting from string to a float, with 2 digits of precision, the issue with comparing floats can pop up and give inconsistent results due to the conversion process.

An easier way rather than relying on the mentioned epsilon method is to use number_format (at least for me as I'll remember it!).

Example function that can return an unexpected result:

if((float)$a == (float)$b) {
echo true;
} else {
echo false;
}

echo's false in this example.

Using number format here to trim down the precision (2 point precision being mostly used for currencies etc, although higher precisions should be correctly catered for by number_format), will return an expected result:

if(number_format((float)$a, 2) == number_format((float)$b, 2)) {
echo true;
} else {
echo false;
}

Correctly echo's true.
up
4
zelko at mojeime dot com
7 years ago
The was talk about "converting" 32 and 64 bit IEEE754 binary numbers to PHP float. The issue isn't as much converting, since they are already in binary form, as it is casting. PHP doesn't allow direct accessing of memory, but you can still get around a bit.

The right was to read floats (32 and 64 bit) is this:

<?php
    $binarydata32
= pack('H*','0EC0A14A');
   
$float32 = unpack("f", $binarydata32);
   
   
$binarydata64 = pack('H*','000000C001385441');
   
$float64 = unpack("d", $binarydata64);
   
   
var_dump($float32,$float64,$float32==$float64);  
?>

The result of dump():
<?php
array(1) {
  [
1]=>
 
float(5300231)
}
array(
1) {
  [
1]=>
 
float(5300231)
}
bool(true)
?>

Note: mind the Big and Little endian boys
up
4
james dot cridland at virginradio dot co dot uk
14 years ago
The 'floating point precision' box in practice means:

<? echo (69.1-floor(69.1)); ?>
Think this'll return 0.1?
It doesn't - it returns 0.099999999999994

<? echo round((69.1-floor(69.1))); ?>
This returns 0.1 and is the workaround we use.

Note that
<? echo (4.1-floor(4.1)); ?>
*does* return 0.1 - so if you, like us, test this with low numbers, you won't, like us, understand why all of a sudden your script stops working, until you spend a lot of time, like us, debugging it.

So, that's all lovely then.
up
5
rick at ninjafoo dot com
12 years ago
Concider the following:

(19.6*100) != 1960 

echo gettype(19.6*100) returns 'double', However even .....

(19.6*100) !== (double)1960

19.6*100 cannot be compaired to anything without manually
casting it as something else first.

(string)(19.6*100) == 1960

Rule of thumb, if it has a decimal point, use the BCMath functions.