* @license https://opensource.org/licenses/MIT MIT License */ namespace PrestaShop\Decimal\Operation; use PrestaShop\Decimal\DecimalNumber; /** * Computes the addition of two decimal numbers */ class Addition { /** * 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 addition * * @param DecimalNumber $a Base number * @param DecimalNumber $b Addend * * @return DecimalNumber Result of the addition */ public function compute(DecimalNumber $a, DecimalNumber $b) { if (function_exists('bcadd')) { return $this->computeUsingBcMath($a, $b); } return $this->computeWithoutBcMath($a, $b); } /** * Performs the addition using BC Math * * @param DecimalNumber $a Base number * @param DecimalNumber $b Addend * * @return DecimalNumber Result of the addition */ public function computeUsingBcMath(DecimalNumber $a, DecimalNumber $b) { $precision1 = $a->getPrecision(); $precision2 = $b->getPrecision(); return new DecimalNumber((string) bcadd($a, $b, max($precision1, $precision2))); } /** * Performs the addition without BC Math * * @param DecimalNumber $a Base number * @param DecimalNumber $b Addend * * @return DecimalNumber Result of the addition */ public function computeWithoutBcMath(DecimalNumber $a, DecimalNumber $b) { if ($a->isNegative()) { if ($b->isNegative()) { // if both numbers are negative, // 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 $this ->computeWithoutBcMath($a->toPositive(), $b->toPositive()) ->invert(); } // if the number is negative and the addend positive, // perform an inverse subtraction by inverting the terms // f(x, y) = y - |x| // eg. f(-2, 1) = 1 - |-2| = -1 // eg. f(-1, 2) = 2 - |-1| = 1 // eg. f(-1, 1) = 1 - |-1| = 0 return $b->minus( $a->toPositive() ); } if ($b->isNegative()) { // if the number is positive and the addend is negative // perform subtraction instead: 2 - 1 // f(x, y) = x - |y| // f(2, -1) = 2 - |-1| = 1 // f(1, -2) = 1 - |-2| = -1 // f(1, -1) = 1 - |-1| = 0 return $a->minus( $b->toPositive() ); } // optimization: 0 + x = x if ('0' === (string) $a) { return $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 sum $sum = $this->addStrings($coeff1, $coeff2); // both signs are equal, so we can use either $sign = $a->getSign(); // keep the bigger exponent $exponent = max($a->getExponent(), $b->getExponent()); return new DecimalNumber($sign . $sum, $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]; } /** * Adds two integer numbers as strings. * * @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 addStrings($number1, $number2, $fractional = false) { // optimization - numbers can be treated as integers as long as they don't overflow the max int size if ('0' !== $number1[0] && '0' !== $number2[0] && strlen($number1) <= $this->maxSafeIntStringSize && strlen($number2) <= $this->maxSafeIntStringSize ) { return (string) ((int) $number1 + (int) $number2); } // 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) { $sum = $number1[$i] + $number2[$i] + $carryOver; $result .= $sum % 10; $carryOver = (int) ($sum >= 10); } if ($carryOver > 0) { $result .= '1'; } return strrev($result); } }