php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #9288 Math operations give wrong result under certain type-conversion circumstances
Submitted: 2001-02-15 14:29 UTC Modified: 2001-02-15 14:55 UTC
From: tim at timj dot co dot uk Assigned:
Status: Closed Package: Math related
PHP Version: 4.0.4pl1 OS: Linux
Private report: No CVE-ID: None
 [2001-02-15 14:29 UTC] tim at timj dot co dot uk
Please, I believe I have found a serious bug.  I can reproduce it 100%.

Essentially I have a loop (I think this might be significant?) that checks through some rows in a database and if the value of a row (an amount of money) is less than the total ($total2pay) then the ID for that row is inserted into an array to be cancelled, and the total is reduced by the amount of that row.

The loop works fine, unless there are a number of items which add up exactly to the total.  Then, when subtracting the final item (so we get x - x) instead of 0 we get something like 2.7755575615629E-17.

Below is the problem script and underneath that is the output I get. The script has been expanded out to use more variables to make it clearer and prints some debugging stuff. It's not incredibly efficient on SQL requests, but that's irrelevant.
I will gladly give any more information or debug output on request if it's helpful.  NB I do NOT have the Zend Optimiser running for this.

The database structure of table 'debts' is:
id int(10), owed_to varchar(20), owed_by varchar(20), amount decimal (10,2)

// Let there be initially 3 rows in the db which match the query below, // with amount = "0.71", "0.20" and "0.10" respectively
$res = db_query("SELECT * FROM debts WHERE owed_by='$who_is_paying' AND owed_to='$payee' ORDER BY amount DESC");
// Let $total2pay = 1.01
do {
  while ($row = mysql_fetch_array($res)) {
    if ($row[amount] <= $total2pay) { // cancel item
      $cancel[] = $row[id];					
      print "oldtotal:$total2pay,type ".gettype($total2pay)."<br>";
      $new = $row[amount];
      print "amount2delete:$new, type ".gettype($new)."<br>";
      // BELOW LINE IS THE PROBLEM ONE
      $fish= $total2pay - $new;// deduct it from the total owed
      print "newtotal:$fish,type ".gettype($fish)."<br>";
      $total2pay = $fish;
    }
  }
  // delete the marked items from the database
  if ($cancel) {
    reset($cancel);
    while ($item = each($cancel)) {
      db_query("DELETE FROM debts WHERE id='$item[value]'");
    }
  }

  // retrieve the fresh, updated item list from the databas
	mysql_free_result($res);
  $res = db_query("SELECT * FROM debts WHERE owed_by='$who_is_paying' AND owed_to='$payee' ORDER BY amount DESC");
  $rows = db_num_rows($res);
} while (($total2pay > 0) && ($rows > 0) && $cancel);


The output from this is:

oldtotal:1.01,type string
amount2delete:0.71, type string
newtotal:0.3,type double

oldtotal:0.3,type double
amount2delete:0.20, type string
newtotal:0.1,type double

oldtotal:0.1,type double
amount2delete:0.10, type string
newtotal:2.7755575615629E-17,type double

I have played a bit to try to fix it, and found that if I set $new explicitly to type double just before the problem line, the output is exactly the same, i.e. the output is:

oldtotal:1.01,type double
amount2delete:0.71, type double
newtotal:0.3,type double

oldtotal:0.3,type double
amount2delete:0.2, type double
newtotal:0.1,type double

oldtotal:0.1,type double
amount2delete:0.1, type double
newtotal:2.7755575615629E-17,type double

Another thing: if there is only one row in the database and this adds up to the total, it works fine:

oldtotal:1.01,type string
amount2delete:1.01, type double
newtotal:0,type double

I really hope someone can help me get to the bottom of this!

Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2001-02-15 14:51 UTC] derick@php.net
This is very normal in the world of floating point math. All numbers you write and have a fraction are only very good approximations of a number.
 [2001-02-15 14:55 UTC] hholzgra@php.net
short answer: 
never use floats or doubles for financial data!

longer answer: 
the internal number format in modern computers
is binary (base 2) and not decimal (base 10) for performance
and complexity reasons
while it is possible to convert decimal numbers into binaries
and back this does not hold true for fractions
something like 0.3 (decimal) would be a periodic binary 
fraction like 10/3 is 0.33333333333333333333...
in decimal
this leads to loss of precision when calculation with
decimal fractions as you have when storing currency values

solution: 
if you just summ up values then you should store values in
the smalest unit your currency has (pennies?) instead of
what you are used to (Pounds?) to totally avoid fractions 

if you cannot avoid fractions (like when dealing with 
percentage calculations or currency conversions) you
should just be aware of the (usually very small) internal
conversion differences 
(0.000000000000000027755575615629 in your example)
or use the bcmath extension, although for monetary 
values you should go perfectly fine with using round(...,2)
on your final results


 
PHP Copyright © 2001-2024 The PHP Group
All rights reserved.
Last updated: Thu Mar 28 15:01:29 2024 UTC