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 methodof()
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
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
We should be careful when creating BigInteger
object from non-int value:
BigInteger::of('1.5'); // throws Brick\Math\Exception\RoundingNecessaryException
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
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
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
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
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
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
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
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;
}
}
Top comments (0)