Arbitrary-precision decimal arithmetic for PHP 7
This library provides a PHP extension that adds support for correctly-rounded, arbitrary-precision decimal floating point arithmetic. Applications that rely on accurate numbers (ie. money, measurements, or mathematics) can use Decimal
instead of float
or string
to represent numerical values.
The implementation uses the same arbitrary precision library as Python’s decimal, called mpdecimal.
The decimal extension offers several advantages over the float
data type:
- All rational numbers can be represented accurately. In contrast, numbers like
0.2
do not have exact representations in binary floating point. You can read more about this in the floating point guide or in PHP’s documentation. - While a binary floating point value like
0.2
comes close, the small difference prevents reliable equality testing, and inaccuracies may accumulate over time. - Arbitrary precision allows for numbers that are not bound by the same upper and lower limits as
float
- numbers can be as big or small as required. - PHP does a good job of hiding the inaccuracies of binary floating point representation with the precision INI setting. By default,
0.1 + 0.2
will have a string value of0.3
even though the internal Cdouble
can not represent the result accurately. For example:
var_dump(0.1 + 0.2); // float(0.3)
var_dump(0.1 + 0.2 - 0.3); // float(5.5511151231258E-17)
PHP already has arbitrary precision math functions…
The current goto answer for arbitrary precision math in PHP is bcmath. However, the Decimal
class offers multiple advantages over bcmath:
- Decimal values are objects, so you can typehint
Decimal
instead ofstring
. - Arithmetic and comparison operators are supported.
- Precision is defined as the number of significant figures, and scale is the number of digits behind the decimal point. This means that a number like
1.23E-1000
would require a scale of1002
but a precision of3
. This library uses precision; bcmath uses scale. - Scientific notation is supported, so you can use strings like
"1.23E-1000"
to construct aDecimal
. At the time of this writing, you can not do this with bcmath. - Calculations are significantly faster. See #performance for some benchmarks.
Installation
Dependencies
- PHP 7
- libmpdec 2.4+
Composer
Composer can not be used to install the extension. The php-decimal/php-decimal
package can be used to specify the extension as a dependency and provides stubs for IDE integration. If you are using Composer and would like to add this extension as a dependency, require php-decimal/php-decimal
.
Install
The easiest way to install the extension is to use PECL:
pecl install decimal
If you are using phpbrew:
phpbrew ext install decimal
phpbrew ext enable decimal
Enable
Remember to enable to extension in your .ini file.
extension=decimal.so # Unix, OS X
extension=php_decimal.dll # Windows
Verify
You can confirm that the extension is installed with php --re decimal
.
Basic Usage
The Decimal
class is under the Decimal
namespace.
Decimal
objects can be constructed using a Decimal
, string
, or int
value,
and an optional precision which defaults to 28.
Special float
values are also supported (NAN
, INF
and -INF
), but float
is otherwise not a valid argument in order to avoid accidentially using a float
.
If you absolutely must use a float
to construct a decimal you can cast it to a string
first,
but doing so if affected by the precision INI setting.
Projects using this extension should avoid float
entirely, wherever possible. An example workflow is to store values as DECIMAL
in the database, query them as string
, parse to Decimal
, perform calculations, and finally prepare for the database using toFixed.
JSON conversions will automatically convert the decimal to string
using all signficant figures.
A warning will be raised if value was not parsed completely. For example, "0.135"
to a precision of 2
will result in "0.14"
with a warning. Similarly, 123
with a precision of 2
would result in 120
with a warning because data has been lost.
Decimal
is final and immutable. Arithmetic operations always return a new Decimal
using the maximum precision of the object and the operands. The result is therefore accurate up to MAX($this->precision(), $op1->precision(), ...)
significant figures, subject to rounding of the last digit.
For example:
use Decimal\Decimal;
$a = new Decimal("1", 2);
$b = new Decimal("7", 8);
print_r($a / $b);
Decimal\Decimal Object
(
[value] => 0.14285714
[precision] => 8
)
Scalar operands inherit the precision of the Decimal
operand, which avoids the need to
construct a new object for the operation. If a scalar operand must be parsed with a higher precision, you should construct a new Decimal
with an explicit precision. The result of a decimal operation is always a Decimal
.
For example:
use Decimal\Decimal;
$op1 = new Decimal("0.1", 4);
$op2 = "0.123456789";
print_r($op1 + $op2);
use Decimal\Decimal;
/**
* @param int $n The factorial to calculate, ie. $n!
* @param int $p The precision to calculate the factorial to.
*
* @return Decimal
*/
function factorial(int $n, int $p = Decimal::DEFAULT_PRECISION): Decimal
{
return $n < 2 ? new Decimal($n, $p) : $n * factorial($n - 1, $p);
}
echo factorial(10000, 32);
Warning: Loss of data on string conversion in ... on line 1
Decimal\Decimal Object
(
[value] => 0.2235
[precision] => 4
)
Sandbox
This is a limited environment where you can experiment with Decimal
.
Performance
The benchmark performs calculations on string
and int
values, then converts the result to a string at the very end. While this does not represent cases where a single operation is performed and exported, the goal is to simulate a realistic workflow where a number is created, used in a few calculations, and exported at the end.
It is difficult to determine what to use for the scale of bcmath, because it specifies the number of places behind the decimal point, rather than the precision, which is the total number of significant places. This benchmark is therefore arbitrary in itself and serves only to provide a rough idea of what to expect.
The code for this basic benchmark can be found here.
Results are the total runtime to produce a result across many iterations, in seconds. Lower is better.
Results
Type | Add | Subtract | Multiply | Divide | |
---|---|---|---|---|---|
bcmath |
string |
3.5520 |
3.6620 |
6.7272 |
25.8195 |
php-decimal |
string |
2.5652 |
2.6048 |
2.5794 |
5.32710 |
bcmath |
int |
4.2136 |
4.2002 |
5.5506 |
11.5603 |
php-decimal |
int |
1.6846 |
1.6523 |
1.7213 |
4.7780 |
Attributes
Precision
Precision defines the number of significant figures that a decimal is accurate to. For example, a number like 1.23E-200
is very small but only has 3 significant figures. PHP decimal uses a default precision of 28
and does not take the precision setting in the .ini into account (which is for converting float
to string). Increasing the precision will require more memory and might impact runtime significantly for operations like pow
and div
when using a very high precision.
Decimal operations, casting and construction will always preserve precision to avoid data loss:
- You can’t reduce the precision of a decimal.
- You can’t change the value of a decimal after it has been calculated.
- Constructing a decimal using a decimal will preserve the given decimal’s precision.
For example:
$a = new Decimal("0.1", 50);
$b = new Decimal($a); // Precision is 50
$c = new Decimal($b, 6); // Precision is 50
$d = new Decimal($c, 64); // Precision is 64
Arithmetic operations will result in a new decimal using the maximum precision of all operands. The developer’s only responsibility is to define the precision (as a minimum) when constructing a decimal. For example, if you have a DECIMAL(20,6)
column in your database (precision is 20, scale is 6), you would create a decimal instance using 20 for the precision and be assured that all calculations will use and result in a precision of at least 20. When the value is to be written back to the database, you would use $decimal->toFixed(6)
to produce a string rounded accurately to 6 decimal places to match the scale of the SQL data type.
There are three precision constants:
Decimal::MIN_PRECISION
Decimal::MAX_PRECISION
Decimal::DEFAULT_PRECISION
Special Numbers
There are 3 special numbers: INF
, -INF
and NAN
. These correspond to the same float
value constants in PHP. All comparison and arithmetic using these values
match the behaviour of PHP float
values wherever possible, and any case that does not do so is considered a bug.
TRUE
if this decimal is not a defined number.
TRUE
if this decimal represents infinity, FALSE
otherwise.
Integers
TRUE
if this decimal is an integer, ie. does not have significant figures behind the decimal point, otherwise FALSE
.
TRUE
if this decimal is either positive or negative zero.
Sign
0
if zero, -1
if negative, or 1
if positive.
TRUE
if this decimal is positive, FALSE
otherwise.
TRUE
if this decimal is negative, FALSE
otherwise.
Parity
0
if the integer value of this decimal is even, 1
if odd. Special numbers like NAN
and INF
will return 1
.
TRUE
if this decimal is an integer and even, FALSE
otherwise.
TRUE
if this decimal is an integer and odd, FALSE
otherwise.
Rounding
The default rounding mode defined as Decimal::DEFAULT_ROUNDING
is half-even, which is also the default used by IEEE 754, C#, Java, and Python. However, Javascript uses half-up, and both Ruby and PHP use half-away-from-zero.
The reason for this default is to prevent biasing the average upwards or downwards.
This stack exchange answer provides some great examples for further reading.
Rounding Modes
The default rounding mode can not be changed because it affects how values are reduced to a precision. With a fixed internal rounding mode, an input value will always result in the same decimal value for a given precision, regardless of the environment. However, some methods allow you to provide a rounding mode, which can be any of the following constants:
Decimal::ROUND_UP
(away from from)Decimal::ROUND_DOWN
(towards zero)Decimal::ROUND_CEILING
(towards positive infinity)Decimal::ROUND_FLOOR
(towards negative infinity)Decimal::ROUND_HALF_UP
(halfway ties away from zero)Decimal::ROUND_HALF_DOWN
(halfway ties towards zero)Decimal::ROUND_HALF_EVEN
(halfway ties to nearest even number)Decimal::ROUND_HALF_ODD
(halfway ties to nearest odd number)Decimal::ROUND_TRUNCATE
(no rounding)
You can also use the corresponding PHP constants.
Rounding Methods
-
InvalidArgumentException
if the rounding mode is not supported.
1.1.0
Comparing
Decimal objects are equal if their numeric values are equal, as well as their precision. The only value that breaks this rule is NAN
, which is not equal to anything else including itself. Precision is used as the tie-break in cases where the values are equal.
Decimal objects can be compared to any other type to determine equality or relative ordering. Non-decimal values will be converted to decimal first (using the maximum precision). In cases where the type is not supported or comparison is not defined (eg. a decimal compared to "abc"
), the decimal is considered greater and an exception will not be thrown.
While decimal objects can not be constructed from a non-special float
, they can be compared to float
. This is done by implicitly converting the value to a string using the equivalent of a string cast. This conversion is affected by the .ini “precision” setting because an implicit cast should have the same behaviour as an explicit cast.
Decimal objects follow the standard PHP object conventions:
- Always
true
. - Always greater than
NULL
. - Identical (
===
) if they are the same object, even if equal.
There are two methods that you can use to compare:
This method is equivalent to the ==
operator.
TRUE
if this decimal is considered equal to the given value.
This method is equivalent to the <=>
operator.
0
if this decimal is considered equal to $other
,-1
if this decimal should be placed before $other
, 1
if this decimal should be placed after $other
.
Operators
Method | Operators | Description |
---|---|---|
compareTo |
<=> , < , <= , > , >= |
Relative ordering, sorting. |
equals |
== |
Equality, equal precision. |
=== |
Identity, same exact object, even if equal. |
Converting
Decimal objects can be converted to string
, int
, and float
.
This method is equivalent to a cast to int.
-
OverflowException
if the value is greater thanPHP_INT_MAX
.
This method is equivalent to a cast to float, and is not affected by the 'precision' INI setting.
-
OverflowException
if the value is greater thanPHP_FLOAT_MAX
. -
UnderflowException
if the value is smaller thanPHP_FLOAT_MIN
.
This method is equivalent to a cast to string.
Casting
You can also cast a decimal to string
, float
, int
and bool
.
use Decimal\Decimal;
(bool) new Decimal(); // true, by convention
(bool) new Decimal(1); // true
(int) new Decimal("1.5"); // 1
(float) new Decimal("1.5"); // 1.5
(string) new Decimal("1.5"); // 1.5
Important: (string)
or toString
should not be used to produce a canonical
representation of a decimal, because there is more than one way to represent the
same value, and precision is not represented by the value itself. However, it is
guaranteed that the string representation of a decimal can be used to construct
a new decimal with the exact same value, assuming equal precision. If you want to
store a decimal with its precision, you should use serialize
and unserialize
.
Arithmetic
Methods
This method is equivalent to the +
operator.
-
TypeError
if the value is not a decimal, string or integer.
This method is equivalent to the -
operator.
-
TypeError
if the value is not a decimal, string or integer.
This method is equivalent to the *
operator.
-
TypeError
if the given value is not a decimal, string or integer.
This method is equivalent to the /
operator.
-
TypeError
if the value is not a decimal, string or integer. -
DivisionByZeroError
if dividing by zero. -
ArithmeticError
if division is undefined, eg.INF
/ -INF
This method is equivalent to the %
operator.
-
TypeError
if the value is not a decimal, string or integer. -
DivisionByZeroError
if the integer value of $value is zero. -
ArithmeticError
if the operation is undefined, eg.INF
% -INF
-
TypeError
if the value is not a decimal, string or integer. -
DivisionByZeroError
if the integer value of $value is zero. -
ArithmeticError
if the operation is undefined, eg.INF
, -INF
This method is equivalent to the **
operator.
-
TypeError
if the exponent is not a decimal, string or integer.
This method is equivalent in function to PHP's log
.
The precision of the result will be the max of all precisions that were encountered during the calculation. The given precision should therefore be considered the minimum precision of the result. This method is equivalent to adding each value individually.
The precision of the result will be the max of all precisions that were encountered during the calculation. The given precision should therefore be considered the minimum precision of the result. This method is equivalent to adding each value individually, then dividing by the number of values.