cult3

Avoid arbitrary precision errors in PHP with BC Math

May 14, 2014

Table of contents:

  1. How will this package work?
  2. Creating the package structure
  3. Creating custom exceptions
  4. The Number Interface
  5. The Number Value Object
  6. The PositiveNumber Value Object
  7. The Math class
  8. The Commands
  9. The Add Command
  10. Testing
  11. Using the package
  12. Conclusion

Computers are the perfect tool for running calculations on massive numbers to within a high degree of accuracy. However this can break down in situations where the required digits of precision exceed the available memory of the computer.

In PHP we have the BCMath extension. These functions allow you to run calculations on numbers of any size and precision because they are represented as strings.

The BCMath extensions will work natively out of the box as regular PHP functions, however it provides us with an excellent opportunity to work on a dedicated package for working with BCMath in an object oriented way.

Over the last couple of weeks I’ve been looking at some of the integral parts of building PHP packages. Now that we’ve established an understanding of some of these important concepts, we can use that knowledge to start building PHP packages that solve our problems but can also be made available to the wider PHP community.

In this tutorial I’m going to walk you though how to build a PHP package for working with the BCMath functions in an object oriented way.

How will this package work?

Before I get into writing the code of the package, first I will describe how I intend for it to work.

BCMath is set of PHP functions that allow to you to use Arbitrary-precision arithmetic. This package is basically going to be a wrapper for these functions so that they can be used in an object oriented way.

The class will allow you to input certain values and run one of the BCMath commands. This will return a Value Object in the form of a Number object (What is the difference between Entities and Value Objects?).

The package will have a custom exception incase a developer who uses the package tries to use an input that is not a valid number (When should you use an Exception?).

Each command will extend an Abstract class (What are Abstract classes?) and implement an interface (When should I code to an Interface?).

Creating the package structure

Last week I walked you through How to create a PSR-4 PHP package. I won’t cover the same ground again so I will be skipping setting the files and directories up and instead jumping straight into the code.

For this package I will be using the namespace PhilipBrown\Math and I will be using PSR-4 autoloading.

Creating custom exceptions

An important aspect of this package is ensuring that only valid inputs types are given to the class to work with. There is no point in trying to do calculations on "hello world" because it’s just not going to work. When the package receives an invalid input it is a good opportunity to throw an Exception because this is an exceptional circumstance that cannot be recovered from and it is caused by an external input.

In order to separate the custom exceptions from the rest of the package’s code, I usually create an Exceptions directory under the main src directory.

For this package I will only require one custom exception for when an invalid input has been given to the class:

<?php namespace PhilipBrown\Math\Exceptions;

use Exception;

class InvalidNumberException extends Exception
{
}

As you can see this is a really simple class as we inherit all of the methods from the Exception class and we don’t need any custom methods. Now that this custom exception class has been created, the consumers of this package can target this specific exception by type hinting it.

The Number Interface

This package will revolve around a Number Value Object that will be used to run the calculations. I will also need to define a specific PositiveNumber Value Object to ensure certain numbers are not of a negative value.

For each of these Value Objects, I want to have a consistent way of getting the value of the object. This is a perfect opportunity to use an Interface contract that both of the objects will implement:

<?php namespace PhilipBrown\Math;

interface NumberInterface
{
    /**
     * Get the value
     *
     * @return string
     */
    public function getValue();
}

The Number Value Object

Now that I’ve got the NumberInterface defined I can create the Number Value Object:

<?php namespace PhilipBrown\Math;

use PhilipBrown\Math\Exceptions\InvalidNumberException;

class Number implements NumberInterface
{
    /**
     * The value of the Number
     *
     * @var string
     */
    protected $value;

    /**
     * Create a new instance of Number
     *
     * @param string $value
     * @param void
     */
    public function __construct($value)
    {
        if (!$this->isValid($value)) {
            throw new InvalidNumberException("$value is not a valid number.");
        }

        $this->value = $value;
    }

    /**
     * Get the value
     *
     * @return string
     */
    public function getValue()
    {
        return $this->value;
    }

    /**
     * Check to see if the number is valid
     *
     * @param string $number
     * @return bool
     */
    protected function isValid($number)
    {
        return (bool) preg_match('/^\-?\d+(\.\d+)?$/', $number);
    }

    /**
     * Return the value when cast as a string
     *
     * @return string
     */
    public function __toString()
    {
        return $this->value;
    }
}

When a new Number object is created, the value of the number is passed in through the constructor and passed to the isValid() method. If the isValid() method returns false, the InvalidNumberException will be thrown.

The getValue() method is implemented by simply returning the $this->value class property.

Also notice I’ve implemented the __toString() magic method so when this number is cast as a string it will return $this->value.

The PositiveNumber Value Object

Next I need a Value Object that will ensure that the value is a positive number:

<?php namespace PhilipBrown\Math;

class PositiveNumber extends Number implements NumberInterface
{
    /**
     * Check to see if the number is valid
     *
     * @param string $number
     * @return bool
     */
    public function isValid($number)
    {
        return (bool) preg_match('/^\d+$/', $number);
    }
}

The PositiveNumber class can extend the Number class so all of the methods are inherited. The only thing we need to overwrite is the method to ensure that the value is valid.

The Math class

The main entry point to this package will be the Math class. This class will be do the majority of the leg work coordination and will be used as the central API:

<?php namespace PhilipBrown\Math;

class Math
{
    /**
     * The scale to use
     *
     * @var integer
     */
    protected $scale;

    /**
     * Create a new instance of Math
     *
     * @return void
     */
    public function __construct()
    {
        $this->scale = new PositiveNumber(0);
    }

    /**
     * Create a new Number instance
     *
     * @param $value int
     * @return PhilipBrown\Math\Number
     */
    public function create($value)
    {
        return new Number($value);
    }

    /**
     * Set default scale parameter for all bc math functions
     *
     * @param $scale int
     * @return int
     */
    public function setScale($scale)
    {
        if (!$scale instanceof PositiveNumber) {
            $scale = new PositiveNumber($scale);
        }

        return $this->scale = $scale;
    }
}

When a new instance of the Math class is created, the $this->scale class property defaults to 0. I’ve also added a helper method to allow a consumer of this package to easily create() a new Number instance.

The setScale() method allows the developer to override the $this->scale property. The developer can either pass in an instance of PositiveNumber, or if not, the Value Object will be created from the passed in value.

The Commands

Each of the bc_math functions accept arguments and return a value. In order to standardise how these different functions are implemented in the class, I will turn them into “commands” that can be called through the Math object.

First I will define an interface for each of the commands:

<?php namespace PhilipBrown\Math\Command;

interface CommandInterface
{
    /**
     * Run the command
     *
     * @return PhilipBrown\Math\Number
     */
    public function run();
}

Next I will define an abstract class that each command will inherit from:

<?php namespace PhilipBrown\Math\Command;

use PhilipBrown\Math\Number;

abstract class AbstractCommand
{
    /**
     * Ensure the value is an instance of Number
     *
     * @param $number integer
     * @return PhilipBrown\Math\Number
     */
    public function isNumber($number)
    {
        if ($number instanceof Number) {
            return $number;
        }

        return new Number($number);
    }
}

Each of the commands will require a method to ensure that the numbers we are working with are instances of the Number Value Object. It makes sense to abstract this method to an abstract class instead of repeating it in every class.

The Add Command

Next I can create the Add command class:

<?php namespace PhilipBrown\Math\Command;

use PhilipBrown\Math\Number;

class Add extends AbstractCommand implements CommandInterface
{
    /**
     * The left operand, as a string
     *
     * @var string
     */
    protected $left;

    /**
     * The right operand, as a string.
     *
     * @var string
     */
    protected $right;

    /**
     * This optional parameter is used to set the number
     * of digits after the decimal place in the result.
     *
     * @var int
     */
    protected $scale;

    /**
     * Create a new instance of the Add command
     *
     * @param $left string
     * @param $right string
     * @param $scale integer
     */
    public function __construct($left, $right, $scale)
    {
        $this->left = $left;
        $this->right = $right;
        $this->scale = $scale;
    }

    /**
     * Run the command
     *
     * @return PhilipBrown\Math\Number
     */
    public function run()
    {
        $left = $this->isNumber($this->left);
        $right = $this->isNumber($this->right);

        return new Number(
            bcadd(
                $left->getValue(),
                $right->getValue(),
                $this->scale->getValue()
            )
        );
    }
}

As you can see, the Add class extends the AbstractCommand class and implements the CommandInterface.

The values that we are interested in are passed in through the constructor.

The run() method is where the magic happens. First each value is checked to ensure that they are instances of the Number Value Object.

The values are then passed into the bcadd() function which is then passed into a new Number Value Object. Remember, when the properties of a Value Object change, the Value Object must be destroyed.

This new Number Value Object is then returned.

In order to provide a clean API, I will then add the add() method to the Math object like this:

/**
 * Add two arbitrary precision numbers
 *
 * @param $left int
 * @param $right int
 * @return PhilipBrown\Math\Number
 */
public function add($left, $right)
{
    $command = new Add($left, $right, $this->scale);

    return $command->run();
}

Each of the remaining BCMath functions can be implemented by creating child command classes and implementing them using the same blueprint of the Add class. By creating this blueprint, we’ve made it really easy to add additional “commands” whilst also making the code really easy to understand.

I won’t go through each of the commands because there is not much difference between them. If you want to have a go at implementing, take a look at the list of BCMath functions to see how you would use this blueprint to create each of them.

Testing

In all honesty, there is nothing much to test in this package. We are simply creating a wrapper around an existing set of functions, so we can be pretty confident that the return values will be correct.

However, we can write some simply tests to ensure our API is acting as it should:

use PhilipBrown\Math\Math;

class MathTest extends PHPUnit_Framework_TestCase {

    public function m($scale = 0)
    {
        $m = new Math;
        $m->setScale($scale);
        return $m;
    }

    public function testSetScale()
    {
        $this->assertInstanceOf('PhilipBrown\Math\PositiveNumber', $this->m()->setScale(2));
    }

    public function testCreatingNumber()
    {
        $this->assertInstanceOf('PhilipBrown\Math\Number', $this->m()->create(2));
    }

    /**
     * @expectedException PhilipBrown\Math\Exceptions\InvalidNumberException
     * @expectedExceptionMessage what up is not a valid number.
     */
    public function testCreateInvalidNumber()
    {
        $this->m()->create('what up');
    }

    public function testAdd()
    {
        $this->assertEquals(3, $this->m()->add(1, 2)->getValue());
        $this->assertEquals(3.4, $this->m(1)->add(1, 2.4)->getValue());
        $this->assertEquals(3.44, $this->m(2)->add(1.04, 2.4)->getValue());
        $this->assertEquals(3.446, $this->m(4)->add(1.04, 2.406)->getValue());
        $this->assertEquals(3.4467, $this->m(4)->add(1.0407, 2.406)->getValue());
    }
}

Using the package

Now that I’ve finished creating the package, I can use it to run arbitrary precision mathematic calculations:

$m = new PhilipBrown\Math\Math();

// $n is an instance of PhilipBrown\Math\Number
$n = $m->add(2, 2);

The returned value $n is an instance of PhilipBrown\Math\Number and can therefore by used safely within your application as a Value Object.

To see the full source code of the package, take a look at the GitHub repository.

Conclusion

In this tutorial we’ve taken the theory from the last couple of weeks and applied it to a real world package. Hopefully as you can see, by implementing abstract classes and interfaces, using custom exceptions and value objects, we can create a robust and reliable PHP package that solves a problem and is easy to understand and use.

This PHP package only uses the BCMath functions, but it could be easily extended to offer additional mathematical “commands”. By keeping each individual component small and concise, we can use these individual building blocks to build powerful packages that solve a real problem.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.