* @license https://opensource.org/licenses/MIT MIT License */ namespace PrestaShop\Decimal\Operation; use PrestaShop\Decimal\DecimalNumber; /** * Computes the subtraction of two decimal numbers */ class Subtraction { /** * Maximum safe string size in order to be confident * that it won't overflow the max int size when operating with it * * @var int */ private $maxSafeIntStringSize; /** * Constructor */ public function __construct() { $this->maxSafeIntStringSize = strlen((string) PHP_INT_MAX) - 1; } /** * Performs the subtraction * * @param DecimalNumber $a Minuend * @param DecimalNumber $b Subtrahend * * @return DecimalNumber Result of the subtraction */ public function compute(DecimalNumber $a, DecimalNumber $b) { if (function_exists('bcsub')) { return $this->computeUsingBcMath($a, $b); } return $this->computeWithoutBcMath($a, $b); } /** * Performs the subtraction using BC Math * * @param DecimalNumber $a Minuend * @param DecimalNumber $b Subtrahend * * @return DecimalNumber Result of the subtraction */ public function computeUsingBcMath(DecimalNumber $a, DecimalNumber $b) { $precision1 = $a->getPrecision(); $precision2 = $b->getPrecision(); return new DecimalNumber((string) bcsub($a, $b, max($precision1, $precision2))); } /** * Performs the subtraction without using BC Math * * @param DecimalNumber $a Minuend * @param DecimalNumber $b Subtrahend * * @return DecimalNumber Result of the subtraction */ public function computeWithoutBcMath(DecimalNumber $a, DecimalNumber $b) { if ($a->isNegative()) { if ($b->isNegative()) { // if both minuend and subtrahend are negative // perform the subtraction with inverted coefficients position and sign // f(x, y) = |y| - |x| // eg. f(-1, -2) = |-2| - |-1| = 2 - 1 = 1 // e.g. f(-2, -1) = |-1| - |-2| = 1 - 2 = -1 return $this->computeWithoutBcMath($b->toPositive(), $a->toPositive()); } else { // if the minuend is negative and the subtrahend is positive, // we can just add them as positive numbers and then invert the sign // f(x, y) = -(|x| + y) // eg. f(1, 2) = -(|-1| + 2) = -3 // eg. f(-2, 1) = -(|-2| + 1) = -3 return $a ->toPositive() ->plus($b) ->toNegative(); } } elseif ($b->isNegative()) { // if the minuend is positive subtrahend is negative, perform an addition // f(x, y) = x + |y| // eg. f(2, -1) = 2 + |-1| = 2 + 1 = 3 return $a->plus($b->toPositive()); } // optimization: 0 - x = -x if ('0' === (string) $a) { return (!$b->isNegative()) ? $b->toNegative() : $b; } // optimization: x - 0 = x if ('0' === (string) $b) { return $a; } // pad coefficients with leading/trailing zeroes list($coeff1, $coeff2) = $this->normalizeCoefficients($a, $b); // compute the coefficient subtraction if ($a->isGreaterThan($b)) { $sub = $this->subtractStrings($coeff1, $coeff2); $sign = ''; } else { $sub = $this->subtractStrings($coeff2, $coeff1); $sign = '-'; } // keep the bigger exponent $exponent = max($a->getExponent(), $b->getExponent()); return new DecimalNumber($sign . $sub, $exponent); } /** * Normalizes coefficients by adding leading or trailing zeroes as needed so that both are the same length * * @param DecimalNumber $a * @param DecimalNumber $b * * @return array An array containing the normalized coefficients */ private function normalizeCoefficients(DecimalNumber $a, DecimalNumber $b) { $exp1 = $a->getExponent(); $exp2 = $b->getExponent(); $coeff1 = $a->getCoefficient(); $coeff2 = $b->getCoefficient(); // add trailing zeroes if needed if ($exp1 > $exp2) { $coeff2 = str_pad($coeff2, strlen($coeff2) + $exp1 - $exp2, '0', STR_PAD_RIGHT); } elseif ($exp1 < $exp2) { $coeff1 = str_pad($coeff1, strlen($coeff1) + $exp2 - $exp1, '0', STR_PAD_RIGHT); } $len1 = strlen($coeff1); $len2 = strlen($coeff2); // add leading zeroes if needed if ($len1 > $len2) { $coeff2 = str_pad($coeff2, $len1, '0', STR_PAD_LEFT); } elseif ($len1 < $len2) { $coeff1 = str_pad($coeff1, $len2, '0', STR_PAD_LEFT); } return [$coeff1, $coeff2]; } /** * Subtracts $number2 to $number1. * For this algorithm to work, $number1 has to be >= $number 2. * * @param string $number1 * @param string $number2 * @param bool $fractional [default=false] * If true, the numbers will be treated as the fractional part of a number (padded with trailing zeroes). * Otherwise, they will be treated as the integer part (padded with leading zeroes). * * @return string */ private function subtractStrings($number1, $number2, $fractional = false) { // find out which of the strings is longest $maxLength = max(strlen($number1), strlen($number2)); // add leading or trailing zeroes as needed $number1 = str_pad($number1, $maxLength, '0', $fractional ? STR_PAD_RIGHT : STR_PAD_LEFT); $number2 = str_pad($number2, $maxLength, '0', $fractional ? STR_PAD_RIGHT : STR_PAD_LEFT); $result = ''; $carryOver = 0; for ($i = $maxLength - 1; 0 <= $i; --$i) { $operand1 = $number1[$i] - $carryOver; $operand2 = $number2[$i]; if ($operand1 >= $operand2) { $result .= $operand1 - $operand2; $carryOver = 0; } else { $result .= 10 + $operand1 - $operand2; $carryOver = 1; } } return strrev($result); } }