Using Arbitrary Precision In PHP
Tags: php
Published: 2024-05-10 19:59:11

Performing various calculations is an integral part of software development. Their accuracy often matters a lot. How to make sure that the calculations are correct? How to avoid rounding error? In PHP, when using float type, even simple calculations can lead to unexpected results. Not to look far, try to run the following code: `echo 0.1 + 0.2 - 0.3;`. You'll probably be surprised by the result. The problem is that the float type is not precise enough to perform some calculations.

However, there is a solution to this problem. In this article, I'll compare two external libraries that allow you to perform calculations with arbitrary precision. I'll also show you how to use them in your project.

# Brick Math

The first library that allows to perform calculations with arbitrary precision is [Brick Math](https://github.com/brick/math) in version `0.11.0`. It's based on the [GMP](https://www.php.net/manual/en/book.gmp.php) and [BC Math](https://www.php.net/manual/en/book.bc.php) extensions. They are not required, but if they are installed, the library will use them to perform calculations and make them faster. If none of them is installed, the library will use its own implementation of the algorithms.

## Basic usage

We have a few value objects available in the library, depending on the type of calculations we want to make. We have: 
- `BigDecimal` - for calculations with decimal numbers,
- `BigRational` - for calculations with rational numbers,
- `BigInteger` - for calculations with integers,
- `BigNumber` - for calculations with numbers of any type. This class is abstract but has a static method `of()` that allows you to create an instance of the appropriate class based on the passed value.

The method `of()` in all classes accepts instance of BigNumber objects, string, float and int values.
Moreover, each of the class has it's own named constructors, like `BigInteger::fromBase`.

## Instantiation of value objects

First of all, we have to create an appropriate value object. Let's create `BigInteger` object from integer value:

```php
use Brick\Math\BigInteger;

echo BigInteger::of(999999999999999999999); // 1000000000000000000000
```

In the above example we exceeded the maximum value of the native integer type, so the native int was implicitly converted to float. The `BigInteger` object can handle such large numbers without any problems. We just have to create them with a string parameter instead of an integer:

```php
echo BigInteger::of('999999999999999999999'); // 999999999999999999999
```

We should be careful when creating `BigInteger` object from non-int value:

```php
BigInteger::of('1.5'); // throws Brick\Math\Exception\RoundingNecessaryException
```

## Calculations

Now let's see how to perform calculations with `BigInteger` objects:

```php
echo BigInteger::of('1')
->plus('2')
->multipliedBy('3')
->minus(BigInteger::of('4'))
->dividedBy('5') // 1
```

## Rounding

When using BigInteger, if for example a result of division is not an integer, the `RoundingNecessaryException` exception will be thrown. We can avoid this by using the `dividedBy()` method with the second parameter of RoundingMode const value, e.g.:

```php
echo BigInteger::of('10')->dividedBy('4', RoundingMode::HALF_UP); // 3
```

# PHPDecimal

Another library that allows you to perform calculations with arbitrary precision is [PHPDecimal](https://github.com/php-decimal/php-decimal).
Contrary to the previous lib, this one is not standalone - it requires `decimal` extension to be installed. It's worth mentioning as it's a very powerful tool and has some pros that the previous lib doesn't have.

## Basic usage
It has one type of value object - `Decimal` which is a wrapper for the `decimal` extension.

To create a `Decimal` object, we use `new` operator which takes string, int or other Decimal object as a parameter.
```php
$decimal = new Decimal('999999999999999999999');
echo $decimal; // 999999999999999999999
```

Despite Decimal cannot be created from float, it can be weakly compared to float:
```php
$decimal = new Decimal('10');
echo $decimal == 10.0 ? 'true' : 'false'; // true
echo $decimal == 7.0 ? 'true' : 'false'; // false
```

## Calculations

The example from the previous lib can be rewritten as follows:

```php
echo (new Decimal('1'))
->add('2')
->mul('3')
->sub(new Decimal('4'))
->div('5'); // 1
```

One of the advantages of this lib over `brick/math` is that operator overload can be used to make calculations. That definitely makes the code more readable when it comes to more complex calculations. The above example can be converted to:

```php
echo ((new Decimal('1') + 2) * 3 - new Decimal('4')) / 5); // 1
```

## Rounding

`Decimal` has default rounding mode set to half-even (`Decimal::DEFAULT_ROUNDING`). To change it, we can use variety of consts starting with `ROUND_` prefix, e.g. `Decimal::ROUND_HALF_UP`.

```php
echo (new Decimal('10'))->div('4')->round(0, Decimal::ROUND_HALF_UP); // 3
```

# Which one to choose?

Both libraries are very powerful and allow to perform calculations with arbitrary precision. However, they have some differences that may be important when choosing the right one for your project.

`Brick Math` can be standalone and doesn't require any additional extensions to be installed (however installing one will speed up calculations). It's also more up-to-date. On the other hand, `PHPDecimal` has some advantages over `Brick Math` - it allows to use operator overload and is a bit simpler to use.

# Usage in the code

Instead of using any of the above libraries in the code directly, it may be better to encapsulate it into a custom value object class. This way, we can easily change the library in the future without having to change the code in the whole project. This will make PHPDecimal's operator overload feature useless, but in case of changing a lib this will be much easier as no external code will leak out of the value object.

```php
class Decimal
{
    private function __construct(
        private readonly BigDecimal $amount
    ) {}

    public static function fromString(string $value): self
    {
        return new self(BigDecimal::of($value));
    }

    public function plus(Decimal|string $amount): self
    {
        if ($amount instanceof self) {
            $amount = $amount->amount;
        }
        
        return new self($this->amount->plus($amount));
    }

    public function minus(Decimal|string $amount): self
    {
        if ($amount instanceof self) {
            $amount = $amount->amount;
        }
        
        return new self($this->amount->minus($amount));
    }

    public function toString(): string
    {
        return (string) $this->amount;
    }
}
```