jordanbrauer/unit-converter

View on GitHub
src/Calculator/AbstractCalculator.php

Summary

Maintainability
A
2 hrs
Test Coverage
F
0%
<?php

declare(strict_types = 1);

/**
 * This file is part of the jordanbrauer/unit-converter PHP package.
 *
 * @copyright 2018 Jordan Brauer <18744334+jordanbrauer@users.noreply.github.com>
 * @license MIT
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace UnitConverter\Calculator;

use Closure;
use TypeError;
use UnitConverter\Calculator\Formula\FormulaInterface;

/**
 * The abstract calculator class that all concrete calculators should
 * extend from.
 *
 * @version 1.0.0
 * @since 0.4.1
 * @author Jordan Brauer <18744334+jordanbrauer@users.noreply.github.com>
 * @codeCoverageIgnore
 */
abstract class AbstractCalculator implements CalculatorInterface
{
    /**
     * The default precision value.
     *
     * @const int DEFAULT_PRECISION
     */
    const DEFAULT_PRECISION = 2;

    /**
     * The default rounding mode for calculators.
     *
     * @const int DEFAULT_ROUNDING_MODE
     */
    const DEFAULT_ROUNDING_MODE = self::ROUND_HALF_UP;

    /**
     * Makes 1.5 into 1 and -1.5 into -1.
     *
     * @const int ROUND_HALF_DOWN
     */
    const ROUND_HALF_DOWN = PHP_ROUND_HALF_DOWN;

    /**
     * Rounds to the nearest even value.
     *
     * @const int ROUND_HALF_EVEN
     */
    const ROUND_HALF_EVEN = PHP_ROUND_HALF_EVEN;

    /**
     * Rounds to the nearest odd value.
     *
     * @const int ROUND_HALF_ODD
     */
    const ROUND_HALF_ODD = PHP_ROUND_HALF_ODD;

    /**
     * Makes 1.5 into 2 and -1.5 into -2.
     *
     * @const int ROUND_HALF_UP
     */
    const ROUND_HALF_UP = PHP_ROUND_HALF_UP;

    /**
     * String value representation of the allowed scalar type(s) for the
     * calculator's inputs.
     */
    protected const SCALAR = 'int|float|string';

    /**
     * A non-persitent stack of events for the current calculator's calculations
     *
     * @var array $history
     */
    protected $history;

    /**
     * The number of decimal places that will calculated
     *
     * @var int $precision
     */
    protected $precision;

    /**
     * The mode in which rounding occurs. Use one of the PHP_ROUND_HALF_* constants.
     *
     * @var int $roundingMode
     */
    protected $roundingMode;

    /**
     * Public constructor for the unit converter calculator. For a list of
     * valid $roundingMode arguments, see the PHP_ROUND_HALF_* constants.
     *
     * @link https://secure.php.net/manual/en/function.round.php
     *
     * @param int $precision The number of decimal digits to round to.
     * @param int $roundingMode The mode in which rounding occurs.
     * @return self
     */
    public function __construct(int $precision = null, int $roundingMode = null)
    {
        $this->init(
            ($precision ?? self::DEFAULT_PRECISION),
            ($roundingMode ?? self::DEFAULT_ROUNDING_MODE)
        );
    }

    /**
     * {@inheritDoc}
     */
    abstract public function add($leftOperand, $rightOperand);

    /**
     * {@inheritDoc}
     */
    abstract public function div($dividend, $divisor);

    /**
     * {@inheritDoc}
     */
    abstract public function mod($dividend, $modulus);

    /**
     * {@inheritDoc}
     */
    abstract public function mul($leftOperand, $rightOperand);

    /**
     * {@inheritDoc}
     */
    abstract public function pow($base, $exponent);

    /**
     * {@inheritDoc}
     */
    abstract public function sub($leftOperand, $rightOperand);

    /**
     * Throw a type error if the given closure does not evaluate to true for one
     * of given values. The name of the method and allowed type string are used
     * to create an error message.
     *
     * @param Closure $assert Type check that must return true, otherwise an error is thrown
     * @param string $method The name of the method that is calling this method, used for error message
     * @param string $allowed The allowed type of the value, used for error message
     * @param mixed ...$value One or more values to test with the given closure
     * @return void
     * @throws TypeError When the given closure does not return true
     */
    protected static function invariant(Closure $assert, string $method, string $allowed, ...$value): void
    {
        foreach ($value as $position => $arg) {
            if ($assert($arg)) {
                continue;
            }

            throw new TypeError(sprintf(
                'Argument %d passed to %s::%s must be of the type %s, %s given',
                1 + $position,
                static::class,
                $method,
                $allowed,
                gettype($arg),
            ));
        }
    }

    /**
     * Syntacital sugar wrapper method for div.
     *
     * @api
     * @uses CalculatorInterface::div
     */
    public function divide(...$params)
    {
        return $this->div(...$params);
    }

    /**
     * Dump the calculator's history. Optionally clear the calculator afterwards.
     *
     * @api
     * @param boolean $clear (optional) Clear the calculator after dumping.
     * @return array
     */
    public function dump(bool $clear = false): array
    {
        $history = $this->history;

        if ($clear) {
            $this->init(self::DEFAULT_PRECISION, self::DEFAULT_ROUNDING_MODE);
        }

        return $history;
    }

    /**
     * A robust calculator method to run formulaic operations, abstracting the
     * logic to a single, contained class.
     *
     * @api
     * @param FormulaInterface $formula The formula to run.
     * @param mixed ...$parameters A variadic set of arguments to pass to the formula.
     * @return int|float|string
     */
    public function exec(FormulaInterface $formula, ...$parameters)
    {
        $formula = (clone $formula)->setCalculator($this);
        $result = $formula->describe(...$parameters);

        $this->history[] = (string) $formula;

        return $result;
    }

    /**
     * {@inheritDoc}
     */
    public function getPrecision(): ?int
    {
        return $this->precision;
    }

    /**
     * {@inheritDoc}
     */
    public function getRoundingMode(): ?int
    {
        return $this->roundingMode;
    }

    /**
     * Syntacital sugar wrapper method for mod
     *
     * @api
     * @uses CalculatorInterface::mod
     */
    public function modulus(...$params)
    {
        return $this->mod(...$params);
    }

    /**
     * Syntacital sugar wrapper method for mul
     *
     * @api
     * @uses CalculatorInterface::mul
     */
    public function multiply(...$params)
    {
        return $this->mul(...$params);
    }

    /**
     * Syntacital sugar wrapper method for pow
     *
     * @api
     * @uses CalculatorInterface::pow
     */
    public function power(...$params)
    {
        return $this->pow(...$params);
    }

    /**
     * {@inheritDoc}
     */
    public function round($value, int $precision = null)
    {
        self::invariant(
            static function ($operand): bool {
                return is_numeric($operand);
            },
            __FUNCTION__,
            static::SCALAR,
            $value,
        );

        return round(
            (float) $value,
            ($precision ?? $this->getPrecision()),
            $this->getRoundingMode()
        );
    }

    /**
     * {@inheritDoc}
     */
    public function setPrecision(int $precision): CalculatorInterface
    {
        $this->precision = $precision;

        return $this;
    }

    /**
     * {@inheritDoc}
     */
    public function setRoundingMode(int $roundingMode): CalculatorInterface
    {
        $this->roundingMode = $roundingMode;

        return $this;
    }

    /**
     * Syntacital sugar wrapper method for sub
     *
     * @api
     * @uses CalculatorInterface::sub
     */
    public function subtract(...$params)
    {
        return $this->sub(...$params);
    }

    /**
     * Helper method to initialize the calculator's settings.
     *
     * @param int $precision The number of decimal digits to round to.
     * @param int $roundingMode The mode in which rounding occurs.
     * @return void
     */
    private function init(int $precision, int $roundingMode): void
    {
        $this->history = [];
        $this->setPrecision($precision);
        $this->setRoundingMode($roundingMode);
    }
}