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.2do 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.2comes 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.2will have a string value of0.3even though the internal Cdoublecan 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
Decimalinstead 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-1000would require a scale of1002but 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_PRECISIONDecimal::MAX_PRECISIONDecimal::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 zero)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
-
InvalidArgumentExceptionif 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.
-
OverflowExceptionif 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.
-
OverflowExceptionif the value is greater thanPHP_FLOAT_MAX. -
UnderflowExceptionif 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.
-
TypeErrorif the value is not a decimal, string or integer.
This method is equivalent to the - operator.
-
TypeErrorif the value is not a decimal, string or integer.
This method is equivalent to the * operator.
-
TypeErrorif the given value is not a decimal, string or integer.
This method is equivalent to the / operator.
-
TypeErrorif the value is not a decimal, string or integer. -
DivisionByZeroErrorif dividing by zero. -
ArithmeticErrorif division is undefined, eg.INF/ -INF
This method is equivalent to the % operator.
-
TypeErrorif the value is not a decimal, string or integer. -
DivisionByZeroErrorif the integer value of $value is zero. -
ArithmeticErrorif the operation is undefined, eg.INF% -INF
-
TypeErrorif the value is not a decimal, string or integer. -
DivisionByZeroErrorif the integer value of $value is zero. -
ArithmeticErrorif the operation is undefined, eg.INF, -INF
This method is equivalent to the ** operator.
-
TypeErrorif 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.