Viewing file: NativeCalculator.php (13.28 KB) -rw-r--r-- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
<?php
declare(strict_types=1);
namespace Brick\Math\Internal\Calculator;
use Brick\Math\Internal\Calculator;
/** * Calculator implementation using only native PHP code. * * @internal * * @psalm-immutable */ class NativeCalculator extends Calculator { /** * The max number of digits the platform can natively add, subtract, multiply or divide without overflow. * For multiplication, this represents the max sum of the lengths of both operands. * * In addition, it is assumed that an extra digit can hold a carry (1) without overflowing. * Example: 32-bit: max number 1,999,999,999 (9 digits + carry) * 64-bit: max number 1,999,999,999,999,999,999 (18 digits + carry) */ private readonly int $maxDigits;
/** * @codeCoverageIgnore */ public function __construct() { $this->maxDigits = match (PHP_INT_SIZE) { 4 => 9, 8 => 18, default => throw new \RuntimeException('The platform is not 32-bit or 64-bit as expected.') }; }
public function add(string $a, string $b) : string { /** * @psalm-var numeric-string $a * @psalm-var numeric-string $b */ $result = $a + $b;
if (is_int($result)) { return (string) $result; }
if ($a === '0') { return $b; }
if ($b === '0') { return $a; }
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
$result = $aNeg === $bNeg ? $this->doAdd($aDig, $bDig) : $this->doSub($aDig, $bDig);
if ($aNeg) { $result = $this->neg($result); }
return $result; }
public function sub(string $a, string $b) : string { return $this->add($a, $this->neg($b)); }
public function mul(string $a, string $b) : string { /** * @psalm-var numeric-string $a * @psalm-var numeric-string $b */ $result = $a * $b;
if (is_int($result)) { return (string) $result; }
if ($a === '0' || $b === '0') { return '0'; }
if ($a === '1') { return $b; }
if ($b === '1') { return $a; }
if ($a === '-1') { return $this->neg($b); }
if ($b === '-1') { return $this->neg($a); }
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
$result = $this->doMul($aDig, $bDig);
if ($aNeg !== $bNeg) { $result = $this->neg($result); }
return $result; }
public function divQ(string $a, string $b) : string { return $this->divQR($a, $b)[0]; }
public function divR(string $a, string $b): string { return $this->divQR($a, $b)[1]; }
public function divQR(string $a, string $b) : array { if ($a === '0') { return ['0', '0']; }
if ($a === $b) { return ['1', '0']; }
if ($b === '1') { return [$a, '0']; }
if ($b === '-1') { return [$this->neg($a), '0']; }
/** @psalm-var numeric-string $a */ $na = $a * 1; // cast to number
if (is_int($na)) { /** @psalm-var numeric-string $b */ $nb = $b * 1;
if (is_int($nb)) { // the only division that may overflow is PHP_INT_MIN / -1, // which cannot happen here as we've already handled a divisor of -1 above. $q = intdiv($na, $nb); $r = $na % $nb;
return [ (string) $q, (string) $r ]; } }
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
[$q, $r] = $this->doDiv($aDig, $bDig);
if ($aNeg !== $bNeg) { $q = $this->neg($q); }
if ($aNeg) { $r = $this->neg($r); }
return [$q, $r]; }
public function pow(string $a, int $e) : string { if ($e === 0) { return '1'; }
if ($e === 1) { return $a; }
$odd = $e % 2; $e -= $odd;
$aa = $this->mul($a, $a);
/** @psalm-suppress PossiblyInvalidArgument We're sure that $e / 2 is an int now */ $result = $this->pow($aa, $e / 2);
if ($odd === 1) { $result = $this->mul($result, $a); }
return $result; }
/** * Algorithm from: https://www.geeksforgeeks.org/modular-exponentiation-power-in-modular-arithmetic/ */ public function modPow(string $base, string $exp, string $mod) : string { // special case: the algorithm below fails with 0 power 0 mod 1 (returns 1 instead of 0) if ($base === '0' && $exp === '0' && $mod === '1') { return '0'; }
// special case: the algorithm below fails with power 0 mod 1 (returns 1 instead of 0) if ($exp === '0' && $mod === '1') { return '0'; }
$x = $base;
$res = '1';
// numbers are positive, so we can use remainder instead of modulo $x = $this->divR($x, $mod);
while ($exp !== '0') { if (in_array($exp[-1], ['1', '3', '5', '7', '9'])) { // odd $res = $this->divR($this->mul($res, $x), $mod); }
$exp = $this->divQ($exp, '2'); $x = $this->divR($this->mul($x, $x), $mod); }
return $res; }
/** * Adapted from https://cp-algorithms.com/num_methods/roots_newton.html */ public function sqrt(string $n) : string { if ($n === '0') { return '0'; }
// initial approximation $x = \str_repeat('9', \intdiv(\strlen($n), 2) ?: 1);
$decreased = false;
for (;;) { $nx = $this->divQ($this->add($x, $this->divQ($n, $x)), '2');
if ($x === $nx || $this->cmp($nx, $x) > 0 && $decreased) { break; }
$decreased = $this->cmp($nx, $x) < 0; $x = $nx; }
return $x; }
/** * Performs the addition of two non-signed large integers. */ private function doAdd(string $a, string $b) : string { [$a, $b, $length] = $this->pad($a, $b);
$carry = 0; $result = '';
for ($i = $length - $this->maxDigits;; $i -= $this->maxDigits) { $blockLength = $this->maxDigits;
if ($i < 0) { $blockLength += $i; /** @psalm-suppress LoopInvalidation */ $i = 0; }
/** @psalm-var numeric-string $blockA */ $blockA = \substr($a, $i, $blockLength);
/** @psalm-var numeric-string $blockB */ $blockB = \substr($b, $i, $blockLength);
$sum = (string) ($blockA + $blockB + $carry); $sumLength = \strlen($sum);
if ($sumLength > $blockLength) { $sum = \substr($sum, 1); $carry = 1; } else { if ($sumLength < $blockLength) { $sum = \str_repeat('0', $blockLength - $sumLength) . $sum; } $carry = 0; }
$result = $sum . $result;
if ($i === 0) { break; } }
if ($carry === 1) { $result = '1' . $result; }
return $result; }
/** * Performs the subtraction of two non-signed large integers. */ private function doSub(string $a, string $b) : string { if ($a === $b) { return '0'; }
// Ensure that we always subtract to a positive result: biggest minus smallest. $cmp = $this->doCmp($a, $b);
$invert = ($cmp === -1);
if ($invert) { $c = $a; $a = $b; $b = $c; }
[$a, $b, $length] = $this->pad($a, $b);
$carry = 0; $result = '';
$complement = 10 ** $this->maxDigits;
for ($i = $length - $this->maxDigits;; $i -= $this->maxDigits) { $blockLength = $this->maxDigits;
if ($i < 0) { $blockLength += $i; /** @psalm-suppress LoopInvalidation */ $i = 0; }
/** @psalm-var numeric-string $blockA */ $blockA = \substr($a, $i, $blockLength);
/** @psalm-var numeric-string $blockB */ $blockB = \substr($b, $i, $blockLength);
$sum = $blockA - $blockB - $carry;
if ($sum < 0) { $sum += $complement; $carry = 1; } else { $carry = 0; }
$sum = (string) $sum; $sumLength = \strlen($sum);
if ($sumLength < $blockLength) { $sum = \str_repeat('0', $blockLength - $sumLength) . $sum; }
$result = $sum . $result;
if ($i === 0) { break; } }
// Carry cannot be 1 when the loop ends, as a > b assert($carry === 0);
$result = \ltrim($result, '0');
if ($invert) { $result = $this->neg($result); }
return $result; }
/** * Performs the multiplication of two non-signed large integers. */ private function doMul(string $a, string $b) : string { $x = \strlen($a); $y = \strlen($b);
$maxDigits = \intdiv($this->maxDigits, 2); $complement = 10 ** $maxDigits;
$result = '0';
for ($i = $x - $maxDigits;; $i -= $maxDigits) { $blockALength = $maxDigits;
if ($i < 0) { $blockALength += $i; /** @psalm-suppress LoopInvalidation */ $i = 0; }
$blockA = (int) \substr($a, $i, $blockALength);
$line = ''; $carry = 0;
for ($j = $y - $maxDigits;; $j -= $maxDigits) { $blockBLength = $maxDigits;
if ($j < 0) { $blockBLength += $j; /** @psalm-suppress LoopInvalidation */ $j = 0; }
$blockB = (int) \substr($b, $j, $blockBLength);
$mul = $blockA * $blockB + $carry; $value = $mul % $complement; $carry = ($mul - $value) / $complement;
$value = (string) $value; $value = \str_pad($value, $maxDigits, '0', STR_PAD_LEFT);
$line = $value . $line;
if ($j === 0) { break; } }
if ($carry !== 0) { $line = $carry . $line; }
$line = \ltrim($line, '0');
if ($line !== '') { $line .= \str_repeat('0', $x - $blockALength - $i); $result = $this->add($result, $line); }
if ($i === 0) { break; } }
return $result; }
/** * Performs the division of two non-signed large integers. * * @return string[] The quotient and remainder. */ private function doDiv(string $a, string $b) : array { $cmp = $this->doCmp($a, $b);
if ($cmp === -1) { return ['0', $a]; }
$x = \strlen($a); $y = \strlen($b);
// we now know that a >= b && x >= y
$q = '0'; // quotient $r = $a; // remainder $z = $y; // focus length, always $y or $y+1
for (;;) { $focus = \substr($a, 0, $z);
$cmp = $this->doCmp($focus, $b);
if ($cmp === -1) { if ($z === $x) { // remainder < dividend break; }
$z++; }
$zeros = \str_repeat('0', $x - $z);
$q = $this->add($q, '1' . $zeros); $a = $this->sub($a, $b . $zeros);
$r = $a;
if ($r === '0') { // remainder == 0 break; }
$x = \strlen($a);
if ($x < $y) { // remainder < dividend break; }
$z = $y; }
return [$q, $r]; }
/** * Compares two non-signed large numbers. * * @psalm-return -1|0|1 */ private function doCmp(string $a, string $b) : int { $x = \strlen($a); $y = \strlen($b);
$cmp = $x <=> $y;
if ($cmp !== 0) { return $cmp; }
return \strcmp($a, $b) <=> 0; // enforce -1|0|1 }
/** * Pads the left of one of the given numbers with zeros if necessary to make both numbers the same length. * * The numbers must only consist of digits, without leading minus sign. * * @return array{string, string, int} */ private function pad(string $a, string $b) : array { $x = \strlen($a); $y = \strlen($b);
if ($x > $y) { $b = \str_repeat('0', $x - $y) . $b;
return [$a, $b, $x]; }
if ($x < $y) { $a = \str_repeat('0', $y - $x) . $a;
return [$a, $b, $y]; }
return [$a, $b, $x]; } }
|