2

I have the following code in PHP, where I've attempted to overcome the stated issue by type-casting the variables into Integers and also avoiding floating-point errors by multiplying all values by 100 before comparison in order remove the 2 decimal places.

However, the following code still evaluates the expression to true and colours the text in red instead of green but when I echo the two values of $eq_left and $eq_right, they are identical with no decimal point.

Here's the code:

$eq_left    = (int) ($eq_bal_CurrentAssets*100) + ($eq_bal_NonCurrentAssets*100) ;
$eq_right   = (int) ($eq_bal_Liabilities*100) + ($eq_bal_Taxation*100) + ($eq_bal_Equity*100) ;

if ($eq_left !== $eq_right) {
    $color = 'red';
    $diff   = abs($eq_left - $eq_right);
} else {
    $color = 'green';
}

echo "<div style=\"color: $color; font-weight:bold;\">\n";
echo "  " . number_format(($eq_left/100),2,".",",") . " = " . number_format(($eq_right/100),2,".",",") . "<br />\n";
if ($diff) {
    echo "      Difference = " . number_format(($diff/100),2,".",",") . "\n";
}
echo "</div>\n";
echo $eq_left . " | " . $eq_right

Any ideas?

Peter White
  • 1,016
  • 2
  • 14
  • 29
  • 2
    it looks like it's only casting the first variable to int, but I could be wrong. Try surrounding the equitation in a parentheses and cast that. – kennypu Dec 22 '12 at 04:46
  • 2
    Perhaps try casting within the `if` condition, as in `if ((int)($eq_left) !== (int)($eq_right))`? – Dan Nissenbaum Dec 22 '12 at 04:48
  • Did you check the types of each variable using the [`gettype()`](http://php.net/manual/en/function.gettype.php) function? – inhan Dec 22 '12 at 04:54
  • 2
    Also, the first type casting only casts the first parenthesized group. You might want to typecast the whole expression like `(int)($eq_bal_CurrentAssets*100+$eq_bal_NonCurrentAssets*100)` – inhan Dec 22 '12 at 04:56
  • Just use `!=` and php wont check for same type – jmm Dec 22 '12 at 05:00
  • You're all correct! It's only type-casting the first value. Sorted now. Thanks. – Peter White Dec 22 '12 at 05:47

2 Answers2

3

I agree with the recommendation against floating point if you want exact decimal fraction representation.

The reason is that many decimal fractions can only be approximated in float or double. They are based on binary, not decimal, fractions. In general, a rational number a/b, with no common factors in a and b, can be expressed exactly in a radix r representation if, and only if, all prime factors of b are also prime factors of b. For example, in decimal 1/5 is 0.2, but 1/3 is 0.333333333... In a binary system, 1/5 causes the same problem as 1/3 in decimal.

In your code, I suggest rounding to zero decimal places after doing the multiplication by 100. The (int) cast rounds towards zero, which is not what you need. If the input is even slightly less than a positive integer n, the result of the cast is n-1. The result of round is n.

The floating point representation of a decimal fraction that cannot be represented exactly may be either slightly lower or slightly higher than the original decimal fraction. If you start with e.g. 0.29, convert it to the nearest IEEE 754 64 bit float, and multiply by 100 you will actually get the floating point equivalent of 28.999999999999996447286321199499070644378662109375

Converting that to int with rounding towards zero gives 28, not 29. Rounding it to the nearest int would give 29.

Patricia Shanahan
  • 25,849
  • 4
  • 38
  • 75
  • I can't find proper end-to-end explanation of the problem with solution online so can someone tell me if this is a complete solution for currency: When receiving user entered data, multiply by 100, then cast as INT. Then perform any further calculations or comparrisons, store in database, retrieve from database, divide by 100 before display. Does that cut it? Have I missed anything? – Peter White Dec 24 '12 at 07:59
  • No, if you do something like that you must **round** to zero decimal places between the multiplication by 100 and the cast to int. I've expanded my explanation above. – Patricia Shanahan Dec 24 '12 at 09:00
  • So when I receive user supplied input I do the following: "round($v*100,0);", then when I need to display it again, I'll do "number_format($v/100, 2, '.', ',');". Is that correct? Thanks Patricia. – Peter White Dec 24 '12 at 09:23
1

Never use floating point numbers for money. Always store monetary values as integer cents. You store $5.40 as 540 and divide by 100 when you want to display. Floating point numbers cannot accurately represent the decimals that you think they are.

Here are some pages that discuss why floats as money is a terrible idea:

The problems that you are having are inherent with float representations of decimals. The only way to reliably get around them is to use integers.

Community
  • 1
  • 1
Andy Lester
  • 91,102
  • 13
  • 100
  • 152
  • One could also argue that using floats to represent decimal time is a really bad idea. I once had to work with a time recording system that suffered from this problem. Aggregate times didn't correspond to the individual parts. Simply awful. – Ian Atkin Dec 22 '12 at 05:21
  • This doesn't apply to my example, since I have multiplied the values by 100 to turn them into integers, so no floating point numbers involved. – Peter White Dec 22 '12 at 05:49
  • 1
    @PeterSnow: No, you haven't. Multiplying by 100 doesn't turn them into integers. You still have floats. See Patricia Shanahan's explanation below. – Andy Lester Dec 23 '12 at 07:59
  • @AndyLester Thanks Andy for informing me of that. Unfortunately I still can't get my head around why decimal multiplied by 100 is not an integer or why it would still need 'rounding' when 29.00 is already '2900' after multiplying by 100, which I thought made it an integer! At least now I know that I need research this, which I intend to do now. Thanks (to everyone). – Peter White Dec 24 '12 at 03:56
  • @PeterSnow: I provided three links in my answer above. They should help you understand. – Andy Lester Dec 24 '12 at 03:58
  • Thanks @AndyLester I have checked those but they are more related to the database, which is not an issue with me as I'm using a varbinary column type anyway to handle the encryption. What I need to know is how to make the numbers safe for calculation & comparison. – Peter White Dec 24 '12 at 08:13
  • @PeterSnow: The way to make the numbers safe is to not use floats at all. Don't store floats of dollars. Store integers of pennies. – Andy Lester Dec 24 '12 at 13:23