As "m dot lebkowski+php at gmail dot com" (http://www.php.net/language.types.float#81416) noted 9 comments below :
When PHP converts a float to a string, the decimal separator used depends on the current locale conventions.
However, to declare a floating point number, one must always use a full stop otherwhise the code would be locale dependent (imagine the nightmare):
<?php
$float = 1.5; // float(1.5)
$float = 1,5; // Parse error: syntax error, unexpected ','
$float = (float) '1.5'; // float(1.5)
$float = (float) '1,5'; // float(1)
?>
Now, if you have a string containing a localized number, you can convert it back to a floating point number using the following function:
<?php
/**
* Convert a localized number string into a floating point number
*
* @param string $sNumber The localized number string to convert.
* @return float The converted number.
*/
function str2num($sNumber)
{
$aConventions = localeConv();
$sNumber = trim((string) $sNumber);
$bIsNegative = (0 === $aConventions['n_sign_posn'] && '(' === $sNumber{0} && ')' === $sNumber{strlen($sNumber) - 1});
$sCharacters = $aConventions['decimal_point'].
$aConventions['mon_decimal_point'].
$aConventions['negative_sign'];
$sNumber = preg_replace('/[^'.preg_quote($sCharacters).'\d]+/', '', trim((string) $sNumber));
$iLength = strlen($sNumber);
if (strlen($aConventions['decimal_point']))
{
$sNumber = str_replace($aConventions['decimal_point'], '.', $sNumber);
}
if (strlen($aConventions['mon_decimal_point']))
{
$sNumber = str_replace($aConventions['mon_decimal_point'], '.', $sNumber);
}
$sNegativeSign = $aConventions['negative_sign'];
if (strlen($sNegativeSign) && 0 !== $aConventions['n_sign_posn'])
{
$bIsNegative = ($sNegativeSign === $sNumber{0} || $sNegativeSign === $sNumber{$iLength - 1});
if ($bIsNegative)
{
$sNumber = str_replace($aConventions['negative_sign'], '', $sNumber);
}
}
$fNumber = (float) $sNumber;
if ($bIsNegative)
{
$fNumber = -$fNumber;
}
return $fNumber;
}
?>
Example:
<?php
setLocale(LC_ALL, 'fr_BE.UTF-8'); // decimal separator is now a comma
$float = -123456.789;
$string = (string) $float;
var_dump($float); // float(-123456,789)
var_dump($string); // string(11) "-123456,789"
var_dump((float) $string); // float(-123456)
var_dump(str2num($string)); // float(-123456,789)
?>
It also works with strings returned by the number_format() function:
<?php
setLocale(LC_ALL, 'fr_BE.UTF-8'); // decimal separator is now a comma
$conv = localeconv();
$float = -123456.789;
$string = $conv['int_curr_symbol'].number_format($float, $conv['frac_digits'], $conv['decimal_point'], $conv['thousands_sep']);
var_dump($float); // float(-123456,789)
var_dump($string); // string(15) "EUR -123.456,79"
var_dump((float) $string); // float(0)
var_dump(str2num($string)); // float(-123456,79)
?>
浮動小数点数
浮動小数点数 (あるいは "float", "double", "実数") は、次の構文により指定できます。
<?php
$a = 1.234;
$b = 1.2e3;
$c = 7E-10;
?>
規約:
LNUM [0-9]+
DNUM ([0-9]*[\.]{LNUM}) | ({LNUM}[\.][0-9]*)
EXPONENT_DNUM [+-]?(({LNUM} | {DNUM}) [eE][+-]? {LNUM})
float の大きさはプラットフォーム依存です。ただし、通常はおよそ 10 進数で 14 桁の精度があり、最大値は ~1.8e308 (これは 64ビット IEEE フォーマットです) となります。
浮動小数点数の精度
0.1 や 0.7 のようなシンプルな小数であっても、 それを内部的な二進数表現に変換する際には、どうしても多少精度が落ちてしまいます。 その結果、不思議な結果を引き起こすことがあります。たとえば、 floor((0.1+0.7)*10) の結果はたいてい 7 となるでしょう。おそらくは 8 を想定していらっしゃるでしょうが、そのようにはなりません。 これは、(この計算結果の) 内部的な値が 7.9999999999... のようになっているからです。
こうなる理由のひとつとして、「有限小数に変換できない分数がある」 という事実があります。たとえば 1/3 を小数で表そうとすると 0.3333333. . . となります。
よって、小数の最後の桁を信用してはいけませんし、 小数が等しいという比較を行ってはいけません。より高い精度が必要な場合には、 任意精度数学関数または gmp 関数を代わりに使用してください。
float への変換
文字列型がどのようにして浮動小数点数に変換されるかに関する詳細な情報は、 文字列の数値型への変換 のセクションをご覧ください。 そのほかの型の浮動小数点数への変換については、整数型への変換と同様です。 詳細は整数型への変換 のセクションをご覧ください。 PHP 5 以降、オブジェクトを不動小数点数に変換しようとした場合には、 通知がスローされます。
浮動小数点数
13-Aug-2009 12:49
03-Jun-2009 01:18
PHP will parse ".123" with no leading digit; just a decimal point. For a command-line example:
php -r "echo 1 + .123;"
The regular expression provided above does not parse it.
My correction is:
EXPONENT_DNUM = "[+-]?({DNUM} | ({LNUM} | {DNUM}) [eE][+-]? {LNUM})"
NOTE: {LNUM} by itself is an integer, not a floating point.
08-May-2009 02:04
Here is a function to convert an exponential-format float to a decimal-format float; e.g. 1.6e+12 to 1600000000000.
It will help addressing the problem specified by kjohnson above.
I have tested it, but not in any real world situation so any feedback/improvements/bug-reports would be appreciated.
<?php
function exp_to_dec($float_str)
// formats a floating point number string in decimal notation, supports signed floats, also supports non-standard formatting e.g. 0.2e+2 for 20
// e.g. '1.6E+6' to '1600000', '-4.566e-12' to '-0.000000000004566', '+34e+10' to '340000000000'
// Author: Bob
{
// make sure its a standard php float string (i.e. change 0.2e+2 to 20)
// php will automatically format floats decimally if they are within a certain range
$float_str = (string)((float)($float_str));
// if there is an E in the float string
if(($pos = strpos(strtolower($float_str), 'e')) !== false)
{
// get either side of the E, e.g. 1.6E+6 => exp E+6, num 1.6
$exp = substr($float_str, $pos+1);
$num = substr($float_str, 0, $pos);
// strip off num sign, if there is one, and leave it off if its + (not required)
if((($num_sign = $num[0]) === '+') || ($num_sign === '-')) $num = substr($num, 1);
else $num_sign = '';
if($num_sign === '+') $num_sign = '';
// strip off exponential sign ('+' or '-' as in 'E+6') if there is one, otherwise throw error, e.g. E+6 => '+'
if((($exp_sign = $exp[0]) === '+') || ($exp_sign === '-')) $exp = substr($exp, 1);
else trigger_error("Could not convert exponential notation to decimal notation: invalid float string '$float_str'", E_USER_ERROR);
// get the number of decimal places to the right of the decimal point (or 0 if there is no dec point), e.g., 1.6 => 1
$right_dec_places = (($dec_pos = strpos($num, '.')) === false) ? 0 : strlen(substr($num, $dec_pos+1));
// get the number of decimal places to the left of the decimal point (or the length of the entire num if there is no dec point), e.g. 1.6 => 1
$left_dec_places = ($dec_pos === false) ? strlen($num) : strlen(substr($num, 0, $dec_pos));
// work out number of zeros from exp, exp sign and dec places, e.g. exp 6, exp sign +, dec places 1 => num zeros 5
if($exp_sign === '+') $num_zeros = $exp - $right_dec_places;
else $num_zeros = $exp - $left_dec_places;
// build a string with $num_zeros zeros, e.g. '0' 5 times => '00000'
$zeros = str_pad('', $num_zeros, '0');
// strip decimal from num, e.g. 1.6 => 16
if($dec_pos !== false) $num = str_replace('.', '', $num);
// if positive exponent, return like 1600000
if($exp_sign === '+') return $num_sign.$num.$zeros;
// if negative exponent, return like 0.0000016
else return $num_sign.'0.'.$zeros.$num;
}
// otherwise, assume already in decimal notation and return
else return $float_str;
}
?>
08-May-2009 12:34
In MySQL, many floating point number types can have a range specified using 2 values, the "precision" and the "scale" E.g. 'float(precision,scale)' for the datatype. This syntax means a number may be <precision> bits long, but may only have <scale> bits after the decimal point. E.g. a 'float(5,2)' field may have the values -999.99 to 999.99.
Here is a function to validate a PHP float using this syntax:
<?php
function validate_float($float, $precision, $scale)
{
$max = (float)str_pad("", $precision-$scale, '9').'.'.str_pad("", $scale, '9');
$min = (float)"-$max";
if(($float < $min) || ($float > $max)) return false;
else return true;
}
?>
27-Mar-2009 06:21
Converting IEEE754 64 bit binary to PHP float:
<?php
$v = hexdec('402E1CAC083126E9');
$x = (($v & ((1 << 52) - 1)) + ( 1 << 52)) * ($v >> 63 | 1);
$exp = ($v >> 52 & 0x7FF) - 1075;
$float = $x * pow(2, $exp);
?>
26-Mar-2009 03:06
I had trouble with comparing floats. Basicly, I was reading floats from a database, and comparing with other floats from a database, and ended up with (5.0 != 5.0) being true.
Anyway, i made some simple functions which fix the problem:
<?php
function floatcmp($f1,$f2,$precision = 10) // are 2 floats equal
{
$e = pow(10,$precision);
$i1 = intval($f1 * $e);
$i2 = intval($f2 * $e);
return ($i1 == $i2);
}
function floatgtr($big,$small,$precision = 10) // is one float bigger than another
{
$e = pow(10,$precision);
$ibig = intval($big * $e);
$ismall = intval($small * $e);
return ($ibig > $ismall);
}
function floatgtre($big,$small,$precision = 10) // is on float bigger or equal to another
{
$e = pow(10,$precision);
$ibig = intval($big * $e);
$ismall = intval($small * $e);
return ($ibig >= $ismall);
}
?>
11-Feb-2009 05:55
My BIN to FLOAT (IEEE754), the first one doesn't work for me:
<?php
function binToFloat($bin) {
if(strlen($bin) > 32) {
return false;
} else if(strlen($bin) < 32) {
$bin = str_repeat('0', (32 - strlen($bin))) . $bin;
}
$sign = 1;
if(intval($bin[0]) == 1) {
$sign = -1;
}
$binExponent = substr($bin, 1, 8);
$exponent = -127;
for($i = 0; $i < 8; $i++) {
$exponent += (intval($binExponent[7 - $i]) * pow(2, $i));
}
$binBase = substr($bin, 9);
$base = 1.0;
for($x = 0; $x < 23; $x++) {
$base += (intval($binBase[$x]) * pow(0.5, ($x + 1)));
}
$float = (float) $sign * pow(2, $exponent) * $base;
return $float;
}
?>
09-Jun-2008 01:59
Converting IEEE754 binary representation to php float:
function bin2float ($bin) {
if((ord($bin[0])>>7)==0) $sign=1;
else $sign=-1;
if((ord($bin[0])>>6)%2==1) $exponent=1;
else $exponent=-127;
$exponent+=(ord($bin[0])%64)*2;
$exponent+=ord($bin[1])>>7;
$base=1.0;
for($k=1;$k<8;$k++) {
$base+=((ord($bin[1])>>(7-$k))%2)*pow(0.5,$k);
}
for($k=0;$k<8;$k++) {
$base+=((ord($bin[2])>>(7-$k))%2)*pow(0.5,$k+8);
}
for($k=0;$k<8;$k++) {
$base+=((ord($bin[3])>>(7-$k))%2)*pow(0.5,$k+16);
}
$float=(float)$sign*pow(2,$exponent)*$base;
return $float;
}
03-Jun-2008 04:23
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.
27-Feb-2008 06:18
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.
23-Oct-2007 04:10
Floating point values have a limited precision. Hence a value might not have the same string representation after any processing. That also includes writing a floating point value in your script and directly printing it without any mathematical operations.
If you would like to know more about "floats" and what IEEE 754 is read this: http://docs.sun.com/source/806-3568/ncg_goldberg.html
15-Nov-2006 01:04
<?php
define('EPSILON', 1.0e-8);
function real_cmp($r1, $r2)
{
$diff = $r1 - $r2;
if( abs($diff) < EPSILON )
return 0;
else
return $diff < 0 ? -1 : 1;
}
function real_lt($r1, $r2)
{
return real_cmp($r1, $r2) < 0;
}
echo "raw compare\n";
$n = 0;
for($i = 0.1; $i < 1.0; $i += 0.1) {
$n++;
echo "$i\t$n\n";
}
echo "\nepsilon compare\n";
$n = 0;
for($i = 0.1; real_lt($i, 1.0); $i += 0.1) {
$n++;
echo "$i\t$n\n";
}
/*
Outputs:
raw compare
0.1 1
0.2 2
0.3 3
0.4 4
0.5 5
0.6 6
0.7 7
0.8 8
0.9 9
1 10
epsilon compare
0.1 1
0.2 2
0.3 3
0.4 4
0.5 5
0.6 6
0.7 7
0.8 8
0.9 9
*/
?>
So moral of this program? "Never compare floating point numbers for equality" solves only half of the problem. As seen above, even raw comparing of floats for less than (or grater than) is dangerous and epsilon (round, etc.) must be used.
28-Jul-2006 08:02
An update regarding the james dot cridland at virginradio dot co dot uk note below, I recently tried his formula using PHP 5 and it is necessary to specify the integer precision when using the round function, otherwise the output will be 0.
<? echo round((69.1-floor(69.1))); ?> // prints 0
<? echo round((69.1-floor(69.1)), 1); ?> // prints 0.1
Also, it appears that "small numbers" include everything up to 64.0. So that
<? echo (63.1-floor(63.1)); ?>
will print 0.1 and
<? echo (64.0-floor(64.0)); ?>
will print 0, but
<? echo round(64.1-floor(64.1)); ?>
will print 0.099999999999994.
26-Mar-2006 06:48
Re: rick at ninjafoo dot com
There is no need to “always” use the BCMath functions. We just need to heed the documentation and “never compare floating point numbers for equality”.
The reason (19.6*100) !== (double)1960, is because inside a computer they are not equal.
Try this:
<?php
printf("%.15f", (19.6*100));
?>
Outputs: 1960.000000000000227 (not 1960 as somewhat expected)
If comparison is required a few options come to mind (other than BCMath):
1) Round numbers before comparison:
<?php
$sig_figs = 5;
echo (round((19.6*100), $sig_figs) !== round((double)1960, $sig_figs)) ? 'not equal' : 'equal';
?>
Outputs: equal
2) Another method is to use a tolerance value, and consider numbers equal if their difference is less than the tolerance.
17-Nov-2005 05:03
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!
24-Sep-2005 11:01
Here is a simple formula to break down a number and get rid of the decimal values. I built this to take a number in seconds and convert it to a readable value for Server Uptimes.
<?php
$day = floor(($uptime / 86400)*1.0) ;
$calc1 = $day * 86400 ;
$calc2 = $uptime - $calc1 ;
$hour = floor(($calc2 / 3600)*1.0) ;
if ($hour < 10) {
$hour = "0".$hour ;
}
$calc3 = $hour * 3600 ;
$calc4 = $calc2 - $calc3 ;
$min = floor(($calc4 / 60)*1.0) ;
if ($min < 10) {
$min = "0".$min ;
}
$calc5 = $min * 60 ;
$sec = floor(($calc4 - $calc5)*1.0) ;
if ($min < 10) {
$sec = "0".$sec ;
}
$uptime2 = $day." Days, ".$hour.":".$min.":".$sec ;
?>
Place this where you want the results to be seen:
<?php echo $uptime2 ; ?>
For a Value of 1455587 seconds the results will show as followed:
16 Days, 20:19:47
Enjoy
06-Jul-2005 05:04
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.
13-Aug-2004 10:36
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.
08-Sep-2003 12:34
To complete the thread about testing two floating point numbers for equality, here's the way it works for *every* programming language:
<?php
// two fp numbers should be considered equal if their absolute
// difference does not exceed a certain value epsilon:
$epsilon = 0.0001; // this defines the precision of your comparision
// check their absolute difference
if (abs($one_float - $another_float) < $epsilon)
// what to be done in case the numbers are equal goes here
?>
28-Apr-2003 11:44
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.
16-Apr-2003 03:27
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.
In response to "...the author probably knows what they are talking about..." above:
Of course the author knows what they're talking about. The previous poster missunderstood the semantics of the author's example of the decimal representation of 1/3. The author is not suggesting that some property of decimal numbers causes the behaviour, but that the property of finite binary representations of real numbers which does cause the problem is shared by finite decimal representations. To paraphrase, the author is saying "10*(0.1+0.7) gives 7.99999... because of the binary equivalent of the fact that 1/3+2/3 gives 0.99999... when using finite decimal representations (where 1/3 == 0.33333... and 2/3 == 0.66666..., so 1/3+2/3 == (0.33333...)+(0.66666...) == 0.99999... instead of 1)."
The problem occurs with finite representations of real numbers, regardless of base of the number system used.
27-Mar-2003 03:35
Just to mention ....
$something = "12.20";
$value = (float) $something;
Depending you locale settings (see setlocale) this will return a float number 12.2 or 12 (without decimal part, if you locale uses another symbol than dot for decimal part)
Be aware if u are working with PHP using one locale setting (by setlocale) and a SQL database with other locale ....
10-Mar-2003 11:22
Never never never compare floats for equality! Even a >= is asking too much of any binary computer (that's pretty much all of them ;-). It will sometimes work, but the best you can hope for is a subtle bug that will occasionally cause non-deterministic behaviour.
Floats must only ever be used for proper inequalities.
06-Mar-2003 06:16
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?
05-Feb-2003 03:49
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.
much easier:
e.g. round(3.1415927,2) => 3.14
round(1092,-2) => 1100
24-May-2001 11:13
If you want to round a floating point number to the nearest multiple of some number n, use the following trick:
$rounded = round($number / n) * n
For example, to round 12874.49 to the nearest 100-multiple (i.e. 12900), use
$rounded = round($number / 100) * 100
Use ceil() or floor() if you want to round down/up.
