PHP Decimal Arbitrary-precision decimal arithmetic for PHP 7

Arbitrary-precision decimal arithmetic for PHP 7

Github Build Status PECL

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:

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:

Installation

Dependencies

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.

<?php use Decimal\Decimal; $a = new Decimal("0.1"); $b = new Decimal("7.0"); echo $a / $b;

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:

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:

->precision(): int
Returns: 
int,  the precision of this decimal.

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.

->isNaN(): bool
Returns: 
bool,  TRUE if this decimal is not a defined number.
->isInf(): bool
Returns: 
bool,  TRUE if this decimal represents infinity, FALSE otherwise.

Integers

->isInteger(): bool
Returns: 
bool,  TRUE if this decimal is an integer, ie. does not have significant figures behind the decimal point, otherwise FALSE.
->isZero(): bool
Returns: 
bool,  TRUE if this decimal is either positive or negative zero.

Sign

->abs(): Decimal
Returns: 
Decimal,  the absolute (positive) value of this decimal.
->negate(): Decimal
Returns: 
Decimal,  the same value as this decimal, but the sign inverted.
->signum(): int
Returns: 
int,  0 if zero, -1 if negative, or 1 if positive.
->isPositive(): bool
Returns: 
bool,  TRUE if this decimal is positive, FALSE otherwise.
->isNegative(): bool
Returns: 
bool,  TRUE if this decimal is negative, FALSE otherwise.

Parity

->parity(): int
Returns: 
int,  0 if the integer value of this decimal is even, 1 if odd. Special numbers like NAN and INF will return 1.
->isEven(): bool
Returns: 
bool,  TRUE if this decimal is an integer and even, FALSE otherwise.
->isOdd(): bool
Returns: 
bool,  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:

You can also use the corresponding PHP constants.

Rounding Methods

->floor(): Decimal
Returns: 
Decimal,  the closest integer towards negative infinity.
->ceil(): Decimal
Returns: 
Decimal,  the closest integer towards positive infinity.
->truncate(): Decimal
Returns: 
Decimal,  the result of discarding all digits behind the decimal point.
->round(int $places = 0int $mode = Decimal::ROUND_HALF_EVEN): Decimal
Returns: 
Decimal,  the value of this decimal with the same precision, rounded according to the specified number of decimal places and rounding mode.
Throws:
  • InvalidArgumentException if the rounding mode is not supported.
->toFixed(int $places = 0bool $commas = falseint $mode = Decimal::ROUND_HALF_EVEN): string
Returns: 
string,  the value of this decimal formatted to a fixed number of decimal places, with thousands comma-separated, using a given rounding mode.

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:

There are two methods that you can use to compare:

->equals(mixed $other): bool

This method is equivalent to the == operator.

Returns: 
bool,  TRUE if this decimal is considered equal to the given value.
->compareTo(mixed $other): int

This method is equivalent to the <=> operator.

Returns: 
int,  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.

->toInt(): int

This method is equivalent to a cast to int.

Returns: 
int,  the integer value of this decimal.
Throws:
  • OverflowException if the value is greater than PHP_INT_MAX.
->toFloat(): float

This method is equivalent to a cast to float, and is not affected by the 'precision' INI setting.

Returns: 
float,  the native PHP floating point value of this decimal.
Throws:
  • OverflowException if the value is greater than PHP_FLOAT_MAX.
  • UnderflowException if the value is smaller than PHP_FLOAT_MIN.
->toString(): string

This method is equivalent to a cast to string.

Returns: 
string,  the value of this decimal represented exactly, in either fixed or scientific form, depending on the value.

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

->add(Decimal|string|int $value): Decimal

This method is equivalent to the + operator.

Returns: 
Decimal,  the result of adding this decimal to the given value.
Throws:
  • TypeError if the value is not a decimal, string or integer.
->sub(Decimal|string|int $value): Decimal

This method is equivalent to the - operator.

Returns: 
Decimal,  the result of subtracting a given value from this decimal.
Throws:
  • TypeError if the value is not a decimal, string or integer.
->mul(Decimal|string|int $value): Decimal

This method is equivalent to the * operator.

Returns: 
Decimal,  the result of multiplying this decimal by the given value.
Throws:
  • TypeError if the given value is not a decimal, string or integer.
->div(Decimal|string|int $value): Decimal

This method is equivalent to the / operator.

Returns: 
Decimal,  the result of dividing this decimal by the given value.
Throws:
  • TypeError if the value is not a decimal, string or integer.
  • DivisionByZeroError if dividing by zero.
  • ArithmeticError if division is undefined, eg. INF / -INF
->mod(Decimal|string|int $value): Decimal

This method is equivalent to the % operator.

Returns: 
Decimal,  the remainder after dividing the integer value of this decimal by the integer value of the given value.
Throws:
  • 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
->rem(Decimal|string|int $value): Decimal

Returns: 
Decimal,  the remainder after dividing this decimal by a given value.
Throws:
  • 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
->pow(Decimal|string|int $exponent): Decimal

This method is equivalent to the ** operator.

Returns: 
Decimal,  the result of raising this decimal to a given power.
Throws:
  • TypeError if the exponent is not a decimal, string or integer.
->shift(int $places): Decimal
Returns: 
Decimal,  A copy of this decimal with its decimal place shifted.
->ln(): Decimal

This method is equivalent in function to PHP's log.

Returns: 
Decimal,  the natural logarithm of this decimal (log base e), with the same precision as this decimal.
->exp(): Decimal
Returns: 
Decimal,  the exponent of this decimal, ie. e to the power of this, with the same precision as this decimal.
->log10(): Decimal
Returns: 
Decimal,  the base-10 logarithm of this decimal, with the same precision as this decimal.
->sqrt(): Decimal
Returns: 
Decimal,  the square root of this decimal, with the same precision as this decimal.
Decimal::sum(array|Traversable $valuesint $precision = 28): Decimal

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.

Returns: 
Decimal,  the average of all given values.
Decimal::avg(array|Traversable $valuesint $precision = 28): Decimal

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.

Returns: 
Decimal,  the average of all given values.