Who can prove float produces inaccurate results for monetary calculations?

Searching the web for the topic of accurate monetary calculations in PHP there are several solutions offered, the most common are:

  1. Doing all calculations in smaller monetary units (for example, cents instead of dollars) so we use integers instead of floats - and then formatting the numbers accordingly for display.

  2. Using exact fixed point arithmetic provided by libraries like bcmath that do calculations using strings without losing any precision.

However, I’ve found I’ve been able to achieve good results doing all calculations in PHP using floats with the added requirement that I round the results as often as possible so that any potential inaccuracies don’t add up and they are small enough not to cause any problems for the final result.

Let’s take an example of simple calculations which are needed for an invoice where we have a net price and we need to add some tax to it, multiply by quantity and then sum everything up:

$total = 0;

// $items comes from the database, where values are strored
// in DECIMAL columns and here they come as strings:
foreach ($items as $item) {
    // calculate price with tax (tax is percentage like 8, 20, 23, etc.)
    $gross = round($item['net_price'] * ((100+$item['tax']) / 100), 2);
    
    // calculate item's gross amount (qty can be fractional, too)
    $gross_amount = round($item['qty'] * $gross, 2);
    
    // add to total
    $total = round($total + $gross_amount, 2);
}

This seems to work well and I haven’t noticed any inaccuracies in practice but I’m wondering if this method is 100% safe. I’m not interested here in any theoretical considerations which method should be used - because theoretically, we know that either integer or fixed point arithmetic are the proper methods. I’m curious if the method I presented above - using float and rounding often - is 100% safe and accurate considering we stay within any reasonable amount range (we are not counting trillions of dollars with cent accuracy). So if anyone can find a flaw in this method by providing specific scenario with numbers - that is what I’m looking for.

I’ve been using similar method in my online store engine for more than 5 years and have never heard about any calculation problems from my clients. So yes, I think it’s safe enough.

floats are only accurate to a set number of places - if the numbers are small enough this will not cause a problem.

it also depends on what you are doing with the numbers - if you start multiplying them together then you start to lose accuracy more quickly.

trillions are probably safe to work with this way on most servers but it depends on how many bits get allocated for what in the float.

This is my take as well.

Rounding always introduces some error in precision.

But unless you’re dealing with something like a 40 year mortgage calculation or global stock commodities or sub-atomic reactions or whatever, maintaining a high level of precision isn’t crucial

Maybe you just haven’t looked hard enough to notice the problem. You could be processing so few sales and that nothing is noticeable.

It’s not hard d at all to use more precision and bcmath. I don’t really know why you would need a reason not to use it. You just install the bcmath extension, and use more precision than necessary like out to 4 or 6 places.

1 Like

If you only do what you showed in your example, and deal with normal product cost numbers you should be good.

The problem with the method used is of course the rounding, but at this use you wont notice it.

However if you later break down the order into a audit table, containing commission (if the store offer commission if someone bring them sales), processing fee, product cost, profit etc. where some of these items is calculated using a percentage (possibly containing a decimal so 3.6%) you can get into problems if you dont verify the starting amount vs. the total ending amount. In a scenario like this it is not uncommon to end up with a cent or several too much, due to multiple sub records has been rounded up.

Good to know! I have also used this technique partially in production and no problems so far, however I don’t know if my use case wasn’t too simple to run into trouble.

What do you mean specifically by multiplying them together? I would round the result of each multiplication so that the precision losses do not accumulate. In which scenario this would not help?

Maybe. That’s why I started this thread so that others can help me find the potential problem.

Yes, it’s not hard but as I wrote in my initial post the advice to use bcmath is not the goal of this thread. If you know of an example where float calculations with rounding produce wrong results then please share it.

Can you clarify what calculations could be problematic? When there are percentage calculated items then often there are cases where small inaccuracies can appear - but they can’t be avoided even using precise arithmetic because you always need to round at some point and rounding means losing precision - and this kind of precision loss is allowed. Let’s assume I round the result of each calculation to two decimal places (because I don’t need more precision in the financial document) - I’d be interested to know of an example of where I might lose precision using floating point arithmetic as compared to fixed point arithmetic.

Maybe.
But there are more than 100 stores created with my engine at the moment.
So I think total sales number is big enough to make some conclusions.

1 Like

I can prove it giving accurate results.

Quite contrary.

The question is not about big numbers precision.
It’s about regular numbers presented in the floating point format.

In this case even simplest comparison may fail, as per PHP manual,

$a = (0.1+0.7)*10;
var_dump($a==8);

will give you FALSE.

So, as long as the OP is doing it following the exact advise from the manual, not trusting the last digit but rounding the number to just two decimal digits, there should be no error.

So, one can tell then in this case rounding is rather correcting the possible error, leveling this “last digit” the manual is talking about:

$a = (0.1+0.7)*10;
$a = round($a,2);
var_dump($a==8);
// TRUE

Very good examples.

The first example demonstrates the potential “gotcha” of not using BC Math functions and the “round” example demonstrates the potential “gotcha” of using loose type comparison. i.e.

$a = bcmul(bcadd('0.1', '0.7', 1), '10');
var_dump($a==='8');
// TRUE

and

$a = (0.1+0.7)*10;
$a = round($a,2);
var_dump($a===8);
// FALSE

If you want to use strict comparison, you have to cast operands to a common type.
So, for the second example the code should be

$a = (0.1+0.7)*10;
$a = round($a,2);
var_dump($a===(float)8);

But type casting questions are a bit off topic.

By the way, in your first example casting numbers to strings looks a bit unnatural and may introduce some problems

Now I remember a few years ago I met a programmer who wrote applications that powered financial and fiscal devices (like fiscal printers) - he made a java program for a company I worked for and I saw he was using plain floating point arithmetic for calculating prices. I asked him if a library for exact decimal calculations shouldn’t be used instead - he replied the precision errors never happen and the precision is good enough. He simply rounded the result and that was it. The application has processed thousands of transactions so far and no one has spotted a problem.

Yes, but the second example demonstrates how rounding gets rid of the gotcha.

So far it seems like using floats with rounding is safe for monetary calculations as no one has provided evidence to the contrary and there’s some empirical evidence it is actually safe. At this moment I assume I can use this method in production code.

Try this:

$num = 10000000;

for ($i = 0; $i < 10000000 * 100; $i++) {
	$num -= 0.01;
}

//Should be zero
var_dump(round($num, 2));

Prints: float(0.17)

Where this is potentially more of an issue is when you’re trying to do some sort of calculation. E.g.


$num = 10000000;

$count = 0;

while ($num > 0) {
	$num -= 0.01;
	$count++;
}


//$count should be the starting value of $num * 100
var_dump($count == 10000000 * 100);

Which gives false instead of true. Probably not the kind of thing you’d notice if it was running hundreds of times and occasionally not giving the correct result.

Of course, in a loop like this the bias compounds because 0.01 is actually a slightly larger number internally - and I don’t expect such calculations to be error free. That’s why I use rounding after every calculation. Simply replace $num -= 0.01; with:

$num = round($num - 0.01, 2);

and the problem is solved.

3 Likes

But is the problem truly solved, or simply masked?

The key issue is trying to do decimal math (humans) using binary math (computers)

So there is going to be some discrepancies that unavoidably creep in.

Luckily, as you pointed out, these descrepancys are insignificant until one starts to work with larger numbers / where the errors accrue to a point where they become significant.

As a contrived off-topic example, Google maps has lat long values like
41.8947400, 12.4839000
Good enough for placing a pin on a map, but not for a drone air strike.

For any practical reasons it is solved because rounding keeps the discrepancies so small that they are discarded when formatting the numbers for display or using them as a base for further calculations.

In fact you asked the most important question in this thread - so far I believe the problem is solved with rounding. However, I’m still open to find out of a use case when this solution fails - if anyone can find one!

PHP’s float supposedly can store 17 significant digits, which is more than enough for most financial calculations.

So the solution fails once there are more than 15 significant digits in front of the decimal point - so once you need ten quintillion dollars you lose precision on exact cents.

Yes, that would be right. It is very rare that anyone would need to work with such high monetary values - but yes, that is the only failure point that I’m now sure that exists.

lol

Off-Topic
… at this very moment there are hundreds of Microsoft stock holders that don’t realize their 4.5M account is a penny short.
Obscured gain for Microsoft… $7.36

There has always been a traditional rounding problem with calculating interest - the banks have traditionally solved it by allocating the extra cents at random. A traditional computer scam (pre internet) was to update the rounding process to allocate all the rouding cents to your account rather than randomly. With small balances the odd one cent rounding could add to hundreds if not thousands of dollars.