Money pattern in PHP: the problem
Rubén Rubio
Posted on November 15, 2023
Introduction
When we work with numbers, we may face moments when we lose precision, maybe because the number is gigantic or maybe because it has infinite decimals. The problem is representing infinite numbers in a finite system; no matter how much memory you have, it will always be finite. This number representation is known as floating point (IEEE 754).
Depending on the case, the problem might be serious. For instance, in an e-commerce, a precision error may result in charging less money to a client, resulting in a loss of money for our company. Or we may charge her more, starting a possible legal problem.
I encountered such a problem on a project I collaborated on. Imagine we have two products in this store: one with a final price of €5.50 and the other with a final price of €5.30. If a client buys five units of each, we would expect a bill like the following one:
Base price (€) | VAT (21%) (€) | Final price (€) |
---|---|---|
4.55 | 0.95 | 5.50 |
4.55 | 0.95 | 5.50 |
4.55 | 0.95 | 5.50 |
4.55 | 0.95 | 5.50 |
4.55 | 0.95 | 5.50 |
4.38 | 0.92 | 5.30 |
4.38 | 0.92 | 5.30 |
4.38 | 0.92 | 5.30 |
4.38 | 0.92 | 5.30 |
4.38 | 0.92 | 5.30 |
44.65 | 9.35 | 54.00 |
For the legal bill, each product had to show the price broken down. Thus, to calculate the final price, the software used the base price and then calculated the VAT and the final price. Trying to prevent the problem with floating point numbers, up until three decimal values were stored.
However, these calculations were not done in one place and stored for the rest of the flow; they were done in every place the price was shown. And this calculation was not performed in the same way in all places: in one place, the base price was rounded first, in others, it was rounded to the final price…
Therefore, we found that the summary the client saw before paying was
Base price (€) | VAT (21%) (€) | Final price (€) |
---|---|---|
4.545 | 0.9545 | 5.4995 |
4.545 | 0.9545 | 5.4995 |
4.545 | 0.9545 | 5.4995 |
4.545 | 0.9545 | 5.4995 |
4.545 | 0.9545 | 5.4995 |
4.380 | 0.9198 | 5.2998 |
4.380 | 0.9198 | 5.2998 |
4.380 | 0.9198 | 5.2998 |
4.380 | 0.9198 | 5.2998 |
4.380 | 0.9198 | 5.2998 |
44.625 | 9.372 | 53.997 |
Instead, the bill the client received was the following one:
Base price (€) | VAT (21%) (€) | Final price (€) |
---|---|---|
4.550 | 0.9555 | 5.5055 |
4.550 | 0.9555 | 5.5055 |
4.550 | 0.9555 | 5.5055 |
4.550 | 0.9555 | 5.5055 |
4.550 | 0.9555 | 5.5055 |
4.380 | 0.9198 | 5.2998 |
4.380 | 0.9198 | 5.2998 |
4.380 | 0.9198 | 5.2998 |
4.380 | 0.9198 | 5.2998 |
4.380 | 0.9198 | 5.2998 |
44.65 | 9.38 | 54.03 |
Beyond the inconsistencies in the calculations, a result of ignorance of good practices, the real problem was that the expected result was never obtained. How could we have performed the calculations? Where did we have to round the values?
Rounding in PHP
In PHP, we have several functions to round numbers:
-
floor($amount)
: Returns the next lowest integer value (as float) by rounding down value if necessary. -
ceil($value)
: Returns the next highest integer value by rounding up value if necessary. -
round($amount, $precision, $mode)
: Returns the rounded value of val to specified precision (number of digits after the decimal point). Precision can also be negative or zero (default). -
number_format($amount, $decimals)
: Formats a number with grouped thousands and optionally decimal digits.
Which of these functions did we need to use? And how? The answer is none of them. In any case, we would have ended up losing precision anyway.
Money pattern
Money pattern
Actually, we are not facing the problem correctly. Besides an amount, a price also has a currency. When we say a product costs 10, what do we mean? €10? $10? ¥10? The price will vary if we do not take the currency into account. Precisely, the currency indicates the number of decimal values for an amount, i.e., the minor unit. This is something we can use to store prices.
In that line of thought, there is the money pattern. It consists of using a value object with two attributes: the currency and the amount in the minor unit. Thus, €89.99 would be stored as 8999.
Libraries
In PHP, there are open-source libraries that implement the money pattern, thus solving both problems at once: working with big numbers (up to a limit) and having prices with amount and currency.
The most important ones are:
Internally, both use PHP's BCMath
extension to perform calculations, representing numbers as string
or int
, so there is no losing of decimal values.
There is a comparison between both libraries. However, any of the two is a good option to perform calculations safely.
Conclusions
We saw the problems that arise when working with floating point numbers. Neither of PHP’s native functions for rounding is a solution.
To work with prices, the solution consists of using the money pattern. There are libraries in PHP that implement it using the BCMath extension, so the calculations are safely done (up to a limit).
In the next post, we will see an implementation that correctly solves the example we saw in the introduction.
Posted on November 15, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.