Accurate decimal numbers in PHP
Most web applications deal with decimal numbers at some stage — currency, coordinates, measurements, scientific arithmetic — but PHP does not provide a standard or clear recommendation for dealing with numbers when accuracy is important. There are of course cases where accuracy is not important, or at least not as important as performance.
PHP’s float is an IEEE 754–1985 double under the hood, which is a binary floating-point number, and is therefore not capable of representing certain numbers, such as 0.2. This is because the value is usually stored as an integer multiplied by 2 to the power of an exponent: a * 2^b = c, where a and b are integers. When there are no integer solutions for a and b, the resulting number will be an approximation. This results in counter-intuitive behavior:
The number system we are taught in school and use in our day-to-day lives is of course decimal. When we use float, we are using a binary type to represent a decimal number, because we can perform binary calculations very quickly with hardware. To calculate a power of 2, all we need to do is set a single bit:
In order to represent decimal numbers accurately, we would need to use a power of 10 instead of 2. Unfortunately, most architectures do not support native base-10 arithmetic, so we would need to calculate that power.
The trade-off here is performance for accuracy.
When accuracy is important, this trade-off is easy to make because you can allocate more CPU power to offset the performance hit, but you can not change the underlying architectural limitations. Most PHP applications will have a bottleneck at the filesystem or network layer anyway. ¯\_(ツ)_/¯
How can we achieve accurate decimal representation in PHP?
There are a few existing solutions that have been a part of PHP for a long time: bcmath and gmp. These extensions provide an interface to create and manipulate very large, accurate numbers.
You can read more about these at the floating-point cheat sheet for PHP.
A common transport type is string, (which is what bcmath accepts and returns), and there are some nice abstractions that take care of it all for you. Searching for “decimal” on Packagist shows a handful of results, but there is no clear winner here. What this tells me is that the majority of applications that require accurate numbers either use these extensions directly, or use their own abstractions.
I would like to propose a new standard for handling decimal numbers in PHP, which does not have to be the one and only way, but at least a basis for the best-practice recommendation.
Introducing the decimal extension. 🎉
This extension provides support for correctly-rounded, arbitrary-precision, decimal floating point arithmetic. Applications that rely on accurate numbers can use the Decimal class instead of float or string.
- Decimal values are immutable objects, so you can type-hint as Decimal.
- Arithmetic and comparison operators are supported, which is something that non-extension abstractions can not do (at least not yet).
- Precision is defined as the number of significant digits, and scale is the number of digits behind the decimal point. This means that a number like “1.23E-1000” would require a scale of 1002 but a precision of 3. Decimal uses precision, where bcmath uses scale. I don’t believe that one approach is better than the other, but it is an important distinction to make.
- Scientific notation is supported, so you can use strings like “1.23E-1000”. At the time of this writing, you can not do this with bcmath.
- Calculations are significantly faster than bcmath, even though performance should probably not be a deciding factor here.
See the #performance section of the documentation for more details.
- There is an online #sandbox that you can use to try it out.
- It uses the same internal C library as Python 3’s decimal module.
An analog for SQL’s DECIMAL
If you are building an application with PHP, there is a good chance that you are also using MySQL or PostgreSQL, among others. The decimal extension’s interface was designed with the DECIMAL type in mind. The goal was to make it easy to transport and translate decimal values between the database and the application.
To illustrate this, let’s use an example where we want to store a dollar value. If we want to support 20 digits to the left of the decimal point, and 4 to the right (for fractional cents), we would use
DECIMAL(24,4) for the column and use 24 as the precision for the decimal. PDO would then provide and accept the value as a string, which avoids using float entirely. For example:
I don’t want to include an extension as a dependency.
This has so far been the most common response to recommending the decimal extension as a solution. While I understand and respect the caution towards non-default extensions, it is becoming easier and easier to manage these as dependencies in the same way you would any other dependency: Composer, paired with container-based environments.
Require “php-decimal/php-decimal” as a dependency, which indirectly adds a dependency on the extension and provides stubs for auto-completion.
Composer does not install extensions, so this requires a few additional steps.
For more information about this and the rest of the project, you can take a look at the documentation and API at php-decimal.io