SEVERE PHP floating point bug

Try this.


<?php
	echo 34.2 - 33.98;
?>

The answer is .22. PHP returns 0.22000000000001
What The Hell?

Bug is present in PHP 5.3 and 5.2.9

Can’t be those particular two numbers either.

That’s floating point arithmetic for you. This isn’t specific to php, a lot of languages will give similar results.

Maybe if I change the precision settings? I ran across this oddity in a nightmarishly written codebase I’d rather not dissect to switch everything over to integers.

And honestly, if PHP’s floating point arithmetic is indeed THIS BAD then why the Hell would anyone ever use it at all?

I think that’s what crmalibu was implying Michael, no one does. :wink:

Again, this isn’t a php specific problem. This is typical floating point behavior in programming languages. They are approximations of numbers. I’m sure you’ll see similar behavior from almost all programing languages which support floating point arithmetic.

It has something to do with how the numbers are internally represented. You can read up on it if your curious, but it all boils down to that you can’t perfectly represent any number using binary/scientific notation.

I know I don’t unless I must. But I didn’t write this current code base… Here’s a glimpse into my own little corner of PHP Hell - this is what the code I inherited from the previous programmer looked like before I touched it.


		function getProcedureLedger($procid = NULL,$zeros = 0) {
		if (!$procid) { return;};


		$buffer = '';

$head = $this->ledgerManagerProc($procid,$zeros);



$totals = $head['totals'];

$records= $GLOBALS['db']->dbQuery("select *,patientprocedures.id as id from patientprocedures left join cpt on ppcptid  = cpt.id where patientprocedures.id = $procid");

	$record = $records[0];



$date = date('m-d-Y',$record['ppdateofprocedure']);
$id = $record['id'];
$rxno = $record['rxno'];
$posted = $date;

$ledgerRecords = $GLOBALS['db']->dbQuery("select *,patientledgers.id as id from patientledgers left join paytypes on paytypes.id = patientledgers.pltransactiontype where plprocid = $procid order by patientledgers.id,pltransactiontype, pldateposted  ");


		$buffer .= '
				<tr>


<td class="dataviewh">
Date
</td>

<td class="dataviewh">
Description
</td>

<td class="dataviewh">
Transaction Type
</td>

<td class="dataviewh">
Charge
</td>
<td class="dataviewh">
Payment
</td>
<td class="dataviewh">
Balance
</td>
<td class="dataviewh" colspan="2">
Menu

</tr>
';

$balance = array();
$balance['charges'] = 0;
$balance['paid'] = 0;
$y = 0;

		foreach($ledgerRecords as $ledgerRecord) {

$my= $y &#37; 2;
$y++;



$ledid = $ledgerRecord['id'];
$incharge = 0;

		$postdate = date('m-d-Y',$ledgerRecord['pldateposted']);
		$comment = $ledgerRecord['plcomment'];
		$type = $ledgerRecord['paydescrip'];
		$payType = $ledgerRecord['pltransactiontype'];
		$amt = $ledgerRecord['pltransactionamt'];



			switch ($payType) {

			case 8:
			$balance['charges'] += $amt;
				$incharge = 0;
			break;
			case 4:
			$balance['charges'] -= $amt;
			$incharge =1;
			break;
			case 5:
			$balance['charges'] += $amt;
			$incharge = 0;
			break;
			case 2:
			$balance['paid'] += $amt;
			$incharge = 1;
			break;
			case 9:
			$amt = $amt *-1;
			$balance['charges'] += $amt;

			$incharge = 0;
			break;
			}


//print_r($balance);


$currentBalance = $balance['charges'] - $balance['paid'];

		

$currentBalance = sprintf("%.2f",$currentBalance);

$currentBalance = number_format($currentBalance,2);

		
$amt = sprintf("%.2f",$amt);

$amt = number_format($amt,2);

if ($amt < 0){ 

$amt = '<b>'.substr($amt,0,1).'$'.substr($amt,1,5000);

}



		
			$buffer .= "<tr><td class=\\"dataviewr$my\\">$postdate</td>

			<td class=\\"dataviewr$my\\">
			$comment 
			</td>
			<td class=\\"dataviewr$my\\">
			$type
			</td>";;


	if ($incharge == 0) {
$buffer .='
			<td class="charge">
			$'.$amt.'
			</td>
			<td class="subtract">
			$0.00
			</td> 
';
} else {
$buffer .='
			<td class="charge">
			$0.00
			</td> 

			<td class="subtract">
			$'.$amt.'
			</td>
';
}



$url = $_SERVER['PHP_SELF'].'?module=procedures&func=delledger&id='.$ledid;

$delLink = 
"<a href=\\"#\\" class=\\"adminlink\\" onClick=\\"confirmDelTransaction('$comment','$ledid','$url')\\">Delete</a>";

$buffer .="	<td class=\\"dataviewr$my\\">
			$$currentBalance
			</td>
			<td colspan=\\"2\\" class=\\"dataviewr$my\\">
<a href=\\"{$_SERVER['PHP_SELF']}?module=procedures&func=editledger&id=$ledid\\" class=\\"adminlink\\">Edit</a> 
$delLink
		</td>
			</tr>";


}


$addRecord = $GLOBALS['forms']->selectBox("paytypes","paytypes","paydescrip","2","","select * from paytypes","dataview","updateDescriptionBox()");

$buffer .= "<tr><td class=\\"dataview\\">
<form method=\\"post\\" action=\\"{$_SERVER['PHP_SELF']}\\" name=\\"LedgerUpdate\\">
<input type=\\"hidden\\" name=\\"module\\" value=\\"Procedures\\">
<input type=\\"hidden\\" name=\\"func\\" value=\\"addtoledger\\">
<input type=\\"hidden\\" id=\\"procid\\" name=\\"procid\\" value=\\"$procid\\">
$addRecord
</td>
<td class=\\"dataview\\">
Comment: <input type=\\"text\\" size=\\"20\\" name=\\"comment\\" id=\\"comment\\" class=\\"normalinput\\" value=\\"Insurance Payment\\">
</td>
<td class=\\"dataview\\">
Post Date(blank for today): <input type=\\"text\\" id=\\"postdate\\" class=\\"normalinput\\" size=\\"20\\" name=\\"postdate\\" value=\\"$posted\\">
</td>
<td class=\\"dataview\\">
Amount: $<input type=\\"text\\" class=\\"normalinput\\" size=\\"10\\" id=\\"amount\\" value=\\"0.00\\" name=\\"amount\\">
</td>

<td class=\\"dataview\\">
<input type=\\"submit\\" class=\\"butt\\"  value=\\"Add To Ledger\\">
</form>
</td>
  	
	</tr>";

	$buffer .= '<tr><td class=\\"dataviewh\\" colspan="7"><br></td></tr>';

$ret=array();
$ret['meds'] = $buffer;
$ret['head'] = $head['head'];
$ret['totals'] = $head['totals'];
return $ret;

	

	}













	function getMedicineProcLedger($procid = NULL) {
	

	if (!$procid) { return; };

$procRec = $GLOBALS['db']->dbQuery("select *,patientmedications.id as id from patientmedications left join inventory on patientmedications.medicationid = inventory.medicationid left join medicationdb on medicationdb.id = inventory.medicationid where patientmedications.id = $procid");


	//print_r($procRec);




}

And that’s one of the cleaner blocks… BTW - that’s his tabbing style, such as it is. Imagine reading thousands of lines of code like that… :frowning:

floating point numbers are intended to be used for extremely large or extremely small numbers

in other words, where the exponent is a large number, either positive or negative

example 1

number of water molecules in a cup of water = 7,900,000,000,000,000,000,000,000

this is represented by a mantissa of 7.9 and an exponent of 24

example 2

diameter of a proton, in kilometers = 0.000000000000000001

this is represented by a mantissa of 1.0 and an exponent of -18

floating point numbers are approximate

using floating point numbers for “ordinary” numbers is asking for trouble

I didn’t write the original code. I’m just tasked with fixing it.

Typically I use fixed point numbers - like storing currency as an integer and dividing by 100 at display time.

But honestly I figured PHP would do a better job of adding two fixed point decimals than this.

Oh my… :headbang:

Catch the part where he converts numbers to strings with [fphp]sprint_f[/fphp] and then numerically adds them?

I’m currently writing a new system from scratch for the long term. But there are days like today when one of these bugs comes up and I need to swat it down so that work can get done. I hope to have the new system up in another month. How much of our data can be salvaged by the importer remains to be seen - I reckon a third of it has been corrupted by mis-handlings like the above.

Incidentally, here’s the same function cleaned up as best I could without completely rewriting the surrounding code.

<?
	/**
	 * Prepares the html for a patient proceedure ledger
	 *
	 * @param unknown_type $procid
	 * @param unknown_type $zeros
	 * @return unknown
	 */
	function getProcedureLedger($procid = NULL,$zeros = 0) {
		global $db;
		
		if (!$procid) { 
			return;
		}

		$head = $this->ledgerManagerProc();

		$totals = $head['totals'];

		$records = $db->query("
			SELECT *, 
				patientledgers.id AS id,
				EXTRACT(DAY FROM FROM_UNIXTIME(pldateposted)) AS sortdate 
			FROM patientledgers 
			LEFT JOIN paytypes ON paytypes.id = patientledgers.pltransactiontype 
			WHERE plprocid = $procid 
			ORDER BY sortdate, patientledgers.id ASC
		");
		
		
		/**
		 * First Loop, construct the display rows.
		 */
		$rows = array();
		
		$balance = 0;
		$posted = 0;
	
		foreach ($records as $record) {
			if (!isset($rows[$record['id']] )) {
				$rows[$record['id']] = array(
					'date' => $record['pldateposted'],
					'description' => $record['plcomment'],
					'transType' => $record['paydescrip'],
					'charge' => 0,
					'payment' => 0,
					'adjustment' => 0,
					'balance' => 0
				);
			}
			
			/**
			 * We want the date of the first record to fall into the
			 * default post date for new records.
			 */
			if (!$posted) {
				$posted = $record['pldateposted'];
			}
			
			/**
			 * The codes that follow are, in order
			 * 
			 * 1  -> Appeal Claim 		-- DO NOTHING
			 * 2  -> Payment 			-- SUBTRACT 
			 * 3  -> Copay 				-- SUBTRACT 
			 * 4  -> Debit Adjustment	-- ADD
			 * 5  -> Credit Adjustment	-- SUBTRACT
			 * 6  -> Write Off			-- SUBTRACT
			 * 7  -> Mistake			-- SUBTRACT
			 * 8  -> Charge				-- ADD
			 * 9  -> Error				-- SUBTRACT
			 */
			
			switch ($record['pltransactiontype']) {
				case 2:
				case 3:
					$rows[$record['id']]['payment'] = $record['pltransactionamt'];
					$balance -= $record['pltransactionamt'];
				break;
				case 8:
					$rows[$record['id']]['charge'] = $record['pltransactionamt'];
					$balance += $record['pltransactionamt'];
				break;
				case 5:
				case 6:
				case 7:
				case 9:
					$rows[$record['id']]['adjustment'] = $record['pltransactionamt'] * -1;
					$balance -= $record['pltransactionamt'];
				break;
				case 4:
					$rows[$record['id']]['adjustment'] = $record['pltransactionamt'];
					$balance += $record['pltransactionamt'];
				break;
			}
			
			$rows[$record['id']]['balance'] = $balance;			
		}
		
		/**
		 * Now display
		 */
		$modulus = 0;
		ob_start() ?>
		<tr>
			<td class="dataviewh">Date</td>
			<td class="dataviewh">Description</td>
			<td class="dataviewh">Transaction Type</td>
			<td class="dataviewh">Charge</td>			
			<td class="dataviewh">Payment</td>
			<td class="dataviewh">Adjustment</td>
			<td class="dataviewh">Balance</td>
			<td class="dataviewh" colspan="2">Menu</td>
		</tr>
		<? foreach($rows as $key => $row): ?>
			<tr>
				<td class="dataviewr<?= (++$modulus &#37; 2 == '0') ? '0' : '1' ?>"><?= date('m-d-Y',$row['date']) ?></td>
				<td class="dataviewr<?= ($modulus % 2 == '0') ? '0' : '1' ?>"><?= $row['description'] ?></td>
				<td class="dataviewr<?= ($modulus % 2 == '0') ? '0' : '1' ?>"><?= $row['transType'] ?></td>
				<td class="charge">$<?= number_format($row['charge'], 2) ?></td>
				<td class="subtract">$<?= number_format($row['payment'], 2) ?></td>
				<td class="dataviewr<?= ($modulus % 2 == '0') ? '0' : '1' ?>">$<?= number_format($row['adjustment'], 2) ?></td>
				<td class="dataviewr<?= ($modulus % 2 == '0') ? '0' : '1' ?>">$<?= number_format($row['balance'], 2) ?></td>
				<td colspan="2" class="dataviewr<?= ($modulus % 2 == '0') ? '0' : '1' ?>">
					<a href="<?= $_SERVER['PHP_SELF'] ?>?module=procedures&func=editledger&id=<?= $key ?>" class="adminlink">Edit</a> 
					<a href="#" class="adminlink" onClick="confirmDelTransaction('','<?= $key ?>','<?= $_SERVER['PHP_SELF'] ?>?module=procedures&func=delledger&id=<?= $key ?>')">Delete</a>
				</td>
			</tr>
		<?php endforeach ?>
		<tr>
			<td class="dataview">
				<form method="post" action="<?= $_SERVER['PHP_SELF'] ?>" name="LedgerUpdate">
					<input type="hidden" name="module" value="Procedures">
					<input type="hidden" name="func" value="addtoledger">
					<input type="hidden" id="procid" name="procid" value="<?= $procid ?>">
					<?= $GLOBALS['forms']->selectBox("paytypes","paytypes","paydescrip","2","","select * from paytypes where id != 8","dataview","updateDescriptionBox()"); ?>
			</td>
			<td class="dataview">
				Comment: <input type="text" size="20" name="comment" id="comment" class="normalinput" value="Insurance Payment">
			</td>
			<td class="dataview">
				Post Date(blank for today): <input type="text" id="postdate" class="normalinput" size="20" name="postdate" value="<?= date('m-d-Y',$posted ) ?>">
			</td>
			<td class="dataview">
				Amount: $<input type="text" class="normalinput" size="10" id="amount" value="" name="amount">
			</td>
			<td class="dataview" colspan="5">
				<input type="submit" class="butt"  value="Add To Ledger">
			</form>
			</td>
  	
		</tr>
		<tr>
			<td class="dataviewh" colspan="7"><br></td>
		</tr>		
		<?
	
		return array(
			'meds' =>   ob_get_clean(),
			'head' =>   $head['head'],
			'totals' => $head['totals']
		);
	}
?>

The reason the whole thing has a first level indent is that this function is in a class (yeah, he doesn’t bother to indent for that either). Curiously, while the guy used classes he did not make use of them - none of his classes inherits from any of the others in any way, which makes things a bit easier to debug than if he’d misused them (I’ve yet to see him properly use anything).