Abs() - issue with absolute value function in PHP - php

Can anyone explain why this code does this in regards to abs() (absolute value) -
In my code it will display 'GREATER' - although 0.50 is never GREATER than 0.5, am I missing out something here with the abs function?
$logic = abs(1.83333333333 - 2.33333333333); // 0.50
$limit = 0.50;
if ($logic > $limit) {
echo 'IS GREATER';
} else {
echo 'IS NOT GREATER';
}

Passing floating point numbers to abs you will get a floating point number as result. In that case you can experience problems with the floating point representation: a floating point is never absolutely precise, thus you are most likely getting a number that is not exactly 0.50 but something like 0.500000...01. You could try to round the result to the desired precision (in your case I guess it is two) with the php round function.

If you don't want to round as suggested by #Aldo's answer and your server supports the GMP math functions, you could use gmp_abs() instead. This way you don't run into PHP's inherent floating point problems.

Due to the way floating point math works, your absolute value $logic results in this value:
0.50000000000000022204
which is greater than 0.5
NB: above evaluated using Javascript which uses double precision math for all numbers:
Math.abs(1.83333333333 - 2.33333333333).toFixed(20)

Never compare floats by equality - user the epsilon technique instead PHP: Floating Point Numbers
define('EPSILON', 1.0e-8);
$logic = abs(1.83333333333 - 2.33333333333); // 0.50
$limit = 0.50;
$diff = $logic - $limit;
if (abs($diff) < EPSILON)
echo 'IS EQUAL';
else
echo 'IS NOT EQUAL';

Related

Php comparison integer with double

I got a problem with this script
$total = 0;
$expected_total = 1111;
$i = [85.46,85.46,85.46,85.46,85.46,85.46,85.46,85.46,85.46,85.46,85.46,85.46,85.48];
foreach ($i as $item) { $total += $item; }
if($total != $expected_total) {
echo json_encode([$total,$expected_total]);
}
The problem is that at the end of the sum the $total should be equal to the $expected_total.
I thought about different number types, I printed the type of the two vars and I was right, one was double and the other was integer, so I converted the integer to double
$expected_total = 1111.00;
but the result is still the same.
The only solution that I could find was comparing the rappresentation of the two numbers, casting them to a string.
if((string)$total != (string)$expected_total) {
echo json_encode([$total,$expected_total]);
}
But obviously this is kind of a hack.
Have you ever had a similar problem? How have you solved it?
PHP version : 5.5.9
Many Thanks
This is not only PHP problem. It is about representation of floating point numbers in memory. Floating numbers has limited precision. PHP uses IEEE 754. Read carefully the manual page and you will understand.
You can find in manual code snippet of how to do it.
$a = 1.23456789;
$b = 1.23456780;
$epsilon = 0.00001; //very small number
if(abs($a-$b) < $epsilon) {
echo "true";
}
If you want to check them as integers you could round both values.
You should change the if-statement to the following:
if(round($total) != round($expected_total)) {
echo json_encode([$total,$expected_total]);
}
If you do it like this you will compare the rounded values, which will be the same.
There is a big red label in the PHP Manual, that says:
Floating point numbers have limited precision…
So never trust floating number results to the last digit, and do not compare floating point numbers directly for equality. If higher precision is necessary, the arbitrary precision math functions and gmp functions are available.
In this specific case, you could add the floating numbers using bcadd() and compare the totals using bccomp(). Both functions are provided by the BC Math extension:
foreach ($i as $item) {
$total = bcadd((string) $total, (string) $item, 2);
}
if (bccomp((string) $total, (string) $expected_total, 2) == 0) {
echo json_encode([$total,$expected_total]);
}

Floating point rounding error php ...how can I make sure it works correctly?

I have the following function that determines if I sale is fully paid for. I don't remember why I did it this way, but it has been working so far and I don't remember why I had to do it this way.
function _payments_cover_total()
{
//get_payments is a list of payment amounts such as:
//10.20, 10.21, or even 10.1010101101 (10 decimals max)
$total_payments = 0;
foreach($this->sale_lib->get_payments() as $payment)
{
$total_payments += $payment['payment_amount'];
}
//to_currency_no_money rounds total to 2 decimal places
if (to_currency_no_money($this->sale_lib->get_total()) - $total_payments ) > 1e-6 ) )
{
return false;
}
return true;
}
I am wondering if there is ever a case where due to a rounding error that this function would return false when it shouldn't.
The main part I have a question about is:
> 1e-6
I think before I had, but it was causing problems in some cases.
> 0
I think you are doing what is mentioned on php floating help page. To quote it directly :
To test floating point values for equality, an upper bound on the
relative error due to rounding is used. This value is known as the
machine epsilon, or unit roundoff, and is the smallest acceptable
difference in calculations.
$a and $b are equal to 5 digits of precision.
<?php
$a = 1.23456789;
$b = 1.23456780;
$epsilon = 0.00001;
if(abs($a-$b) < $epsilon) {
echo "true";
}
?>
So in your case:
(to_currency_no_money($this->sale_lib->get_total()) - $total_payments) > 1e-6
relative error due to rounding should not be great than 1e-6 or 0.000001
if you are not sure about left operand being greater than right 100% time,then you should add abs() e.g for correctness.
$relative_error=to_currency_no_money($this->sale_lib->get_total()) - $total_payments;
if(abs($relative_error) > 1e-6){
return false
}
return true;
$x = (1.333-1.233)-(1.334-1.234);
echo $x;
//result = $x = -2.2204460492503E-16 - close to zero
//but (1.333-1.233)-(1.334-1.234) = 0.1 - 0.1 = 0 (in calculator)
if($x === 0){
echo "|zero";
}
else {
echo "|non zero"; //<== this is result
}
//screen = -2.2204460492503E-16|non zero
//how to get to zero?
if($x > 1e-6){//1e-6 mathematical constant
echo "|non zero";
}
else {
echo "|zero";//this is result
}
//screen -2.2204460492503E-16|non zero|zero
if ($x > 1e-6 )
{
echo " false";
//echo "|non zero";
//return false;
}
else{
echo " true";//<== this resut
//echo "|zero";
//return true;
}
//screen -2.2204460492503E-16|non zero|zero true
printf("%.1f<br />", 1e-1);
printf("%.2f<br />", 1e-2);
printf("%.3f<br />", 1e-3);
printf("%.4f<br />", 1e-4);
printf("%.5f<br />", 1e-5);
printf("%.6f<br />", 1e-6);
printf("%.7f<br />", 1e-7);
printf("%.8f<br />", 1e-8);
printf("%.9f<br />", 1e-9);
printf("%.10f<br />", 1e-10);
printf("%.11f<br />", 1e-11);
printf("%.12f<br />", 1e-12);
printf("%.29f<br />", -2.2204460492503E-16);
//0.1
//0.01
//0.001
//0.0001
//0.00001
//0.000001
//0.0000001
//0.00000001
//0.000000001
//0.0000000001
//0.00000000001
//0.000000000001
//-0.00000000000000022204460492503
I am sorry, but when dealing with currency, you shouldn't really be using PHP floats, as IMSoP stated. The reason is also from PHP float help pages:
Additionally, rational numbers that are exactly representable as
floating point numbers in base 10, like 0.1 or 0.7, do not have an
exact representation as floating point numbers in base 2, which is
used internally, no matter the size of the mantissa. Hence, they
cannot be converted into their internal binary counterparts without a
small loss of precision. This can lead to confusing results: for
example, floor((0.1+0.7)*10) will usually return 7 instead of the
expected 8, since the internal representation will be something like
7.9999999999999991118....
So never trust floating number results to the last digit, and do not
compare floating point numbers directly for equality. If higher
precision is necessary, the arbitrary precision math functions and gmp
functions are available.
Note that the help page specifically says you can't trust float results to the last digit, no matter how short (after comma) it is.
So while you do have very short floats (just 2 digits after comma), the float precision (1e-6) doesn't enter into it really, and you can't trust them 100%.
Since it is a question of money, in order to avoid angry customers and lawsuits accusing of penny shaving (https://en.wikipedia.org/wiki/Salami_slicing), the real solutions are:
1) either use PHP BC math, which works with string representation of numbers
http://php.net/manual/en/book.bc.php
2) as IMSoP suggested, use integers and store the amounts in smallest denomination (cents, eurocents or whatever you have) internally.
First solution might be a bit more resource intense: I haven't used BC math myself much, but storing strings and doing arbitrary precision math (which might be a bit of an overkill in this case) are by definition more RAM and CPU intense than working with integers.
It might, however, need less changes in other parts of the code.
The second solution requires changes to represenation, so that wherever user sees the amounts, they are normalized to dollars,cents (or whatever have you).
Note, however, that in this case also you run problems with rounding risks at some point, as when you do:
float shown_amount; // what we show to customer: in dollars,cents
int real_amount; // what we use internally: in cents
shown_amount = cent_amount / 100;
you may reintroduce the rounding problems as you have floats again and possibilities for rounding errors, so tread carefully and be sure to make calculations and roundings only on real_amount, never on shown_amount

sum of numbers returns odd low decimals?

When summing a group of numbers sometimes I end up with some low decimals? Why can it happen when the numbers are parsed as strings? I know there is some %"&! about floats
function parse(){
foreach($_SESSION['import_csv_posts']['result']['csv'] as $key => $post){
$amount = $this->parse_amount($post[$this->param['amount']]);
if($this->param['vat_amount']){
$amount += $this->parse_amount($post[$this->param['vat_amount']]);
}
$this->balance += $amount;
echo "$amount\n";
}
echo "\nbalance = ".$this->balance;
}
function parse_amount($amount){
$amount = strval($amount);
if(strstr($amount, '.') && strstr($amount, ',')){
preg_match('/^\-?\d+([\.,]{1})/', $amount, $match);
$amount = str_replace($match[1], '', $amount);
}
return str_replace(',', '.', $amount);
}
result
-87329.00
-257700.00
-11400.00
-9120.00
-47485.00
-15504.00
122800.00
1836.00
1254.00
200.00
360.00
31680.00
361.60
1979.20
1144.00
7520.00
6249.49
balance = -399.00000000003
The "%"&! about floats" is that floats are simply not precise. There's a certain inaccuracy inherent in how infinite numbers are stored in finite space. Therefore, when doing math with floats, you won't get 100% accurate results.
Your choice is to either round, format numbers to two decimal places upon output, or use strings and the BC Math package, which is slower, but accurate.
Floating point arithmetic is done by the computer in binary, while the results are displayed in decimal. There are many numbers that cannot be represented equally precisely in both systems, therefore there is almost always some difference between what you as a human expect the result to be and what the result actually is when seen as bits (this is the reason that you cannot reliably compare floats for equality).
It does not matter that your numbers are produced through parsing strings, as soon as PHP sees an arithmetic operator it internally converts the strings to numbers.
If you do not require absolute precision (which it looks like you do not, as you are simply displaying stuff) then simply use printf with a format string such as %.2f to limit the number of decimal places.

What is the most efficient way to make and odds system?

I'm trying to design an odds system that goes from 1-100, however it also uses 0-1 for rarer odds.
I was told I should use a floating point format, but I don't know that.
Basically I have..
if (mt_rand(1,1000)/100 == $odds) {} else if (mt_rand(1,100) == $odds) {}
however that only yields the same probability.
I looked up floating point format in the PHP manual, but the answers there couldn't help me.
See Odds to understand how to convert your odds to a probability. If you have odds of 4:1 then there is a 1/5 == 0.2 probability of the event. If your odds are .2:1 then there is a 5/6 (about .833) probability of the event happening. In general, if the odds are m:n against then the probability is n/(m+n).
Now, if you want to simulate whether an event occurs or not, you need to get a random floating point number between 0 and 1 then check if this is less than the probability of the event. You can use something like mt_rand(0,1000)/1000 to get a random number between 0 and 1.
Examples:
$odds1 = 4; // 4:1
$prob1 = 1/($odds1+1); // 1/5
if( mt_rand(0,1000)/1000 <= $prob1 ) {
// event happened
}
$odds2 = .2; // .2:1
$prob2 = 1/($odds2+1); // 5/6
if( mt_rand(0,1000)/1000 <= $prob2 ) {
// event happened
}
Floating point values are inexact. (See Why does `intval(19.9 * 100)` equal `1989`? and search: php floating point inexact.)
You cannot use == for floating point values. A simple 5/10 == 0.5 might already be wrong due to inherent precision loss.
You can either round numbers before comparison, your what I'd advise in your case, pre-convert floats into integers:
# 52 == 100*0.52
if (mt_rand(1,100) == round(100*$odds)) {
Instead of comparing 0.99 with another float, you convert your odds into an integer 99 and compare it with an integer random in the range 1 to 100. If odds already was an integer, not a float, then the *100 multiplication will already cut it out of that first (faux float) comparison.
if($odds < 1){
// floating point math here
if((float)mt_rand(0,100) / 100.0 < $odds){
echo "you're a float winner, harry";
}
}else{
if(mt_rand(0,100) < $odds){
echo "you're an int winner, harry!";
}
}

PHP does not find two equal numbers to be equal? bug? [duplicate]

This question already has answers here:
Closed 12 years ago.
Possible Duplicate:
compare floats in php
i have a condition:
if($x <= .3)
echo 1;
it will echo 1 only if x is less than .3
if $x is EQUAL to .3, i do not get a 1.
i have tried wrapping the x in floatval(), but no luck
i tried to echo the $x and i get "0.3"
i have tried if ($x == .3) - nothing
if i have tried if (.3 == .3) which obviously works
any ideas? is this a PHP bug?
It's all about binary representation of floating point numbers :
var_dump(sprintf("%.40f", 0.3));
// string(42) "0.2999999999999999888977697537484345957637"
Basically, 0.3 can't be represented exactly in base 2, so it gets truncated after a few digits. It's basically like 1/3 in base 10 : you can type 0.3, but 0.33 is more precise, so is 0.333, 0.3333, etc. You can't represent it exactly.
Floating point values are not exact. You can check to see if it's roughly <= 0.3 like this:
if ($x <= 0.3000001) {
echo 'yay';
}
Here you go, big, red and fat: Floating Point Numbers:
It is typical that simple decimal
fractions like 0.1 or 0.7 cannot be
converted into their internal binary
counterparts without a small loss of
precision. This can lead to confusing
results: for example,
floor((0.1+0.7)*10) will usually
return 7 instead of the expected 8,
since the internal representation will
be something like 7.9.
This is due to the fact that it is
impossible to express some fractions
in decimal notation with a finite
number of digits. For instance, 1/3 in
decimal form becomes 0.3.
So never trust floating number results
to the last digit, and never compare
floating point numbers for equality.
If higher precision is necessary, the
arbitrary precision math functions and
gmp functions are available.
PS: There is even more fun:
INF == INF => false
INF < INF => true
INF > INF => true
So infinity is not infinity and infinity is smaller than infinity and greater than infinity at the same time. If you think about it, it does actually make some sense...
hmmm, i have to assume that your var $x is really not equal to .3.
i just tested this:
<?
$var = 0.3;
if( $var <= .3 )
{
echo 'yay';
}
else
{
echo 'boo';
}
?>
and it outputs yay
$x = .4;
if ($x <= .3) { echo 1; }
Works fine here...tried different values at $x
however... where are you getting $x ?? you might need to "round" the value before comparing.
Floating points are not 100% accurate. In short, the fractional component is generally stored by adding 1/(2^n) together. e.g., 1/2 + 1/4 is how 3/4 would be stored. So this isn't a bug, nor is it a specific PHP question.
However, this should always be true:
$x = 0.3;
if ($x == 0.3) echo "true";
because the same inaccuracy would be present in both.
But this is not guaranteed to be true:
$x = 0.1;
$y = 0.2;
if ($x + $y == 0.3) echo "true";
A simple way to work around this is to use a delta:
if (abs($a - $b) < $delta) echo "true"
where $delta is a very small number.
If you need accuracy, then check out something like the BCMath extension.
If this is for money, then it's usually easier to just do calculations in whole cents (integers), where $1.23 = 123.

Categories