JordanRL/Fermat

View on GitHub
src/Samsara/Fermat/Core/Types/Base/Traits/ArithmeticSelectionTrait.php

Summary

Maintainability
B
4 hrs
Test Coverage
A
100%
<?php

namespace Samsara\Fermat\Core\Types\Base\Traits;

use Samsara\Exceptions\UsageError\IntegrityConstraint;
use Samsara\Fermat\Core\Enums\CalcMode;
use Samsara\Fermat\Core\Enums\CalcOperation;
use Samsara\Fermat\Core\Numbers;
use Samsara\Fermat\Core\Provider\CalculationModeProvider;
use Samsara\Fermat\Core\Types\Decimal;
use Samsara\Fermat\Core\Values\ImmutableDecimal;
use Samsara\Fermat\Core\Values\MutableDecimal;

/**
 * @package Samsara\Fermat\Core
 */
trait ArithmeticSelectionTrait
{

    /**
     * Adds a Decimal number to the current Decimal object.
     *
     * @param Decimal $num The Decimal number to be added.
     *
     * @return string The result of the addition operation as a string in decimal representation.
     *
     * @throws IntegrityConstraint If the addition operation violates any integrity constraints.
     */
    protected function addSelector(Decimal $num): string
    {
        $calcMode = $this->getResolvedMode();
        if ($calcMode == CalcMode::Auto) {
            $value = $this->addGMP($num);

            if ($value !== false) {
                return $value;
            }

            $calcMode = $this->modeSelectorForArithmetic($num, CalcOperation::Addition);
        }

        return match ($calcMode) {
            CalcMode::Native => $this->addNative($num),
            default => $this->addScale($num),
        };
    }

    /**
     * @param Decimal|ImmutableDecimal|MutableDecimal $thisNum
     * @param Decimal|ImmutableDecimal|MutableDecimal $thatNum
     * @param int                                     $scale
     *
     * @return string
     * @throws IntegrityConstraint
     */
    protected function divideSelector(
        Decimal|ImmutableDecimal|MutableDecimal $thisNum,
        Decimal|ImmutableDecimal|MutableDecimal $thatNum,
        int                                     $scale
    ): string
    {

        $calcMode = $this->getResolvedMode();
        if ($calcMode == CalcMode::Auto) {
            $value = $this->divideGMP($thatNum);

            if ($value !== false) {
                return $value;
            }

            $calcMode = $this->modeSelectorForArithmetic($thatNum, CalcOperation::Division);
        }

        return match ($calcMode) {
            CalcMode::Native => $this->divideNative($thatNum),
            default => $this->divideScale($thatNum, $scale),
        };
    }

    /**
     * @param Decimal $num
     *
     * @return CalcMode
     * @throws IntegrityConstraint
     */
    protected function modeSelectorForArithmetic(Decimal $num, CalcOperation $operation): CalcMode
    {
        /**
         * This method is never reached from Fraction, even though it is defined on the class
         *
         * @var ImmutableDecimal|Decimal|MutableDecimal $this
         */

        $thisAbs = Numbers::makeOrDont(Numbers::IMMUTABLE, $this->absValue());
        $thatAbs = Numbers::make(Numbers::IMMUTABLE, $num->absValue());
        /*
         * Floats have variable density depending on where the exponent is. However, the exponent is also
         * base-2, while our scale is base-10. Thus, in order to determine if the requested scale is within
         * the range of float to be accurate, we would ideally need to look at the log2() value of the result.
         *
         * In practice however this is a very expensive operation to perform twice for every calculation, and we
         * might have overflows or underflows that are difficult to determine.
         *
         * So, to compromise we are going to set the maximum scale for native within 'auto' to 10, and then cap
         * result at the maximum value of 10,000, which is one order of magnitude below the maximum rounding error
         * of a double precision float as implemented in PHP.
         */
        $nativeScale = $this->getScale() <= 10 && $num->getScale() <= 10;
        $nativeValues = $this->isLessThan(10000) && $num->isLessThan(10000) && ($this->isFloat() || $num->isFloat());

        /*
         * We still need to check for integers, since it's possible the GMP extension won't be installed.
         */
        $nativeInt = $this->isInt() && $num->isInt();
        $nativeInt = $nativeInt && $thisAbs->isLessThan(CalculationModeProvider::PHP_INT_MAX_HALF);
        $nativeInt = $nativeInt && $thatAbs->isLessThan(CalculationModeProvider::PHP_INT_MAX_HALF);

        if (
            ($nativeInt && ($operation == CalcOperation::Addition || $operation == CalcOperation::Subtraction))
            || ($nativeScale && $nativeValues)
        ) {
            return CalcMode::Native;
        } else {
            return CalcMode::Precision;
        }
    }

    /**
     * @param Decimal $num
     *
     * @return string
     */
    protected function multiplySelector(Decimal $num): string
    {
        $calcMode = $this->getResolvedMode();
        if ($calcMode == CalcMode::Auto) {
            $value = $this->multiplyGMP($num);

            if ($value !== false) {
                return $value;
            }

            $calcMode = $this->modeSelectorForArithmetic($num, CalcOperation::Multiplication);
        }

        return match ($calcMode) {
            CalcMode::Native => $this->multiplyNative($num),
            default => $this->multiplyScale($num),
        };
    }

    /**
     * @param Decimal $num
     *
     * @return string
     * @throws IntegrityConstraint
     */
    protected function powSelector(Decimal $num): string
    {
        $calcMode = $this->getResolvedMode();
        if ($num->isEqual(0)) {
            return '1';
        }

        if ($calcMode == CalcMode::Auto) {
            $value = $this->powGMP($num);

            if ($value !== false) {
                return $value;
            }

            $calcMode = $this->modeSelectorForArithmetic($num, CalcOperation::Power);
        }

        return match ($calcMode) {
            CalcMode::Native => $this->powNative($num),
            default => $this->powScale($num),
        };
    }

    /**
     * @param int|null $scale
     *
     * @return string
     * @throws IntegrityConstraint
     */
    protected function sqrtSelector(?int $scale): string
    {
        $calcMode = $this->getResolvedMode();

        if ($calcMode == CalcMode::Auto) {
            $value = $this->sqrtGMP();


            if ($value !== false) {
                return $value;
            }

            $scale = $scale ?? $this->getScale();

            if ($scale > 10 || $this->isGreaterThan(10000) || $this->isLessThan(0)) {
                $calcMode = CalcMode::Precision;
            } else {
                $calcMode = CalcMode::Native;
            }
        }

        return match ($calcMode) {
            CalcMode::Native => $this->sqrtNative(),
            default => $this->sqrtScale($scale),
        };
    }

    /**
     * @param Decimal $num
     *
     * @return string
     */
    protected function subtractSelector(Decimal $num): string
    {
        $calcMode = $this->getResolvedMode();
        if ($calcMode == CalcMode::Auto) {
            $value = $this->subtractGMP($num);

            if ($value !== false) {
                return $value;
            }

            $calcMode = $this->modeSelectorForArithmetic($num, CalcOperation::SquareRoot);
        }

        return match ($calcMode) {
            CalcMode::Native => $this->subtractNative($num),
            default => $this->subtractScale($num),
        };
    }

}