DEV Community

Jarosław Szutkowski
Jarosław Szutkowski

Posted on • Edited on

Using Arbitrary Precision In PHP

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 in version 0.11.0. It's based on the GMP and BC Math 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:

use Brick\Math\BigInteger;

echo BigInteger::of(999999999999999999999); // 1000000000000000000000
Enter fullscreen mode Exit fullscreen mode

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:

echo BigInteger::of('999999999999999999999'); // 999999999999999999999
Enter fullscreen mode Exit fullscreen mode

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

BigInteger::of('1.5'); // throws Brick\Math\Exception\RoundingNecessaryException
Enter fullscreen mode Exit fullscreen mode

Calculations

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

echo BigInteger::of('1')
->plus('2')
->multipliedBy('3')
->minus(BigInteger::of('4'))
->dividedBy('5') // 1
Enter fullscreen mode Exit fullscreen mode

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.:

echo BigInteger::of('10')->dividedBy('4', RoundingMode::HALF_UP); // 3
Enter fullscreen mode Exit fullscreen mode

PHPDecimal

Another library that allows you to perform calculations with arbitrary precision is PHPDecimal.
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.

$decimal = new Decimal('999999999999999999999');
echo $decimal; // 999999999999999999999
Enter fullscreen mode Exit fullscreen mode

Despite Decimal cannot be created from float, it can be weakly compared to float:

$decimal = new Decimal('10');
echo $decimal == 10.0 ? 'true' : 'false'; // true
echo $decimal == 7.0 ? 'true' : 'false'; // false
Enter fullscreen mode Exit fullscreen mode

Calculations

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

echo (new Decimal('1'))
->add('2')
->mul('3')
->sub(new Decimal('4'))
->div('5'); // 1
Enter fullscreen mode Exit fullscreen mode

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:

echo ((new Decimal('1') + 2) * 3 - new Decimal('4')) / 5); // 1
Enter fullscreen mode Exit fullscreen mode

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.

echo (new Decimal('10'))->div('4')->round(0, Decimal::ROUND_HALF_UP); // 3
Enter fullscreen mode Exit fullscreen mode

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.

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)