Revision 148 | Blame | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed
<?phpdeclare(strict_types=1);namespace Brick\Math\Internal;use Brick\Math\Exception\RoundingNecessaryException;use Brick\Math\RoundingMode;/*** Performs basic operations on arbitrary size integers.** Unless otherwise specified, all parameters must be validated as non-empty strings of digits,* without leading zero, and with an optional leading minus sign if the number is not zero.** Any other parameter format will lead to undefined behaviour.* All methods must return strings respecting this format, unless specified otherwise.** @internal** @psalm-immutable*/abstract class Calculator{/*** The maximum exponent value allowed for the pow() method.*/public const MAX_POWER = 1000000;/*** The alphabet for converting from and to base 2 to 36, lowercase.*/public const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';/*** The Calculator instance in use.*/private static ?Calculator $instance = null;/*** Sets the Calculator instance to use.** An instance is typically set only in unit tests: the autodetect is usually the best option.** @param Calculator|null $calculator The calculator instance, or NULL to revert to autodetect.*/final public static function set(?Calculator $calculator) : void{self::$instance = $calculator;}/*** Returns the Calculator instance to use.** If none has been explicitly set, the fastest available implementation will be returned.** @psalm-pure* @psalm-suppress ImpureStaticProperty*/final public static function get() : Calculator{if (self::$instance === null) {/** @psalm-suppress ImpureMethodCall */self::$instance = self::detect();}return self::$instance;}/*** Returns the fastest available Calculator implementation.** @codeCoverageIgnore*/private static function detect() : Calculator{if (\extension_loaded('gmp')) {return new Calculator\GmpCalculator();}if (\extension_loaded('bcmath')) {return new Calculator\BcMathCalculator();}return new Calculator\NativeCalculator();}/*** Extracts the sign & digits of the operands.** @return array{bool, bool, string, string} Whether $a and $b are negative, followed by their digits.*/final protected function init(string $a, string $b) : array{return [$aNeg = ($a[0] === '-'),$bNeg = ($b[0] === '-'),$aNeg ? \substr($a, 1) : $a,$bNeg ? \substr($b, 1) : $b,];}/*** Returns the absolute value of a number.*/final public function abs(string $n) : string{return ($n[0] === '-') ? \substr($n, 1) : $n;}/*** Negates a number.*/final public function neg(string $n) : string{if ($n === '0') {return '0';}if ($n[0] === '-') {return \substr($n, 1);}return '-' . $n;}/*** Compares two numbers.** @return int [-1, 0, 1] If the first number is less than, equal to, or greater than the second number.*/final public function cmp(string $a, string $b) : int{[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);if ($aNeg && ! $bNeg) {return -1;}if ($bNeg && ! $aNeg) {return 1;}$aLen = \strlen($aDig);$bLen = \strlen($bDig);if ($aLen < $bLen) {$result = -1;} elseif ($aLen > $bLen) {$result = 1;} else {$result = $aDig <=> $bDig;}return $aNeg ? -$result : $result;}/*** Adds two numbers.*/abstract public function add(string $a, string $b) : string;/*** Subtracts two numbers.*/abstract public function sub(string $a, string $b) : string;/*** Multiplies two numbers.*/abstract public function mul(string $a, string $b) : string;/*** Returns the quotient of the division of two numbers.** @param string $a The dividend.* @param string $b The divisor, must not be zero.** @return string The quotient.*/abstract public function divQ(string $a, string $b) : string;/*** Returns the remainder of the division of two numbers.** @param string $a The dividend.* @param string $b The divisor, must not be zero.** @return string The remainder.*/abstract public function divR(string $a, string $b) : string;/*** Returns the quotient and remainder of the division of two numbers.** @param string $a The dividend.* @param string $b The divisor, must not be zero.** @return array{string, string} An array containing the quotient and remainder.*/abstract public function divQR(string $a, string $b) : array;/*** Exponentiates a number.** @param string $a The base number.* @param int $e The exponent, validated as an integer between 0 and MAX_POWER.** @return string The power.*/abstract public function pow(string $a, int $e) : string;/*** @param string $b The modulus; must not be zero.*/public function mod(string $a, string $b) : string{return $this->divR($this->add($this->divR($a, $b), $b), $b);}/*** Returns the modular multiplicative inverse of $x modulo $m.** If $x has no multiplicative inverse mod m, this method must return null.** This method can be overridden by the concrete implementation if the underlying library has built-in support.** @param string $m The modulus; must not be negative or zero.*/public function modInverse(string $x, string $m) : ?string{if ($m === '1') {return '0';}$modVal = $x;if ($x[0] === '-' || ($this->cmp($this->abs($x), $m) >= 0)) {$modVal = $this->mod($x, $m);}[$g, $x] = $this->gcdExtended($modVal, $m);if ($g !== '1') {return null;}return $this->mod($this->add($this->mod($x, $m), $m), $m);}/*** Raises a number into power with modulo.** @param string $base The base number; must be positive or zero.* @param string $exp The exponent; must be positive or zero.* @param string $mod The modulus; must be strictly positive.*/abstract public function modPow(string $base, string $exp, string $mod) : string;/*** Returns the greatest common divisor of the two numbers.** This method can be overridden by the concrete implementation if the underlying library* has built-in support for GCD calculations.** @return string The GCD, always positive, or zero if both arguments are zero.*/public function gcd(string $a, string $b) : string{if ($a === '0') {return $this->abs($b);}if ($b === '0') {return $this->abs($a);}return $this->gcd($b, $this->divR($a, $b));}/*** @return array{string, string, string} GCD, X, Y*/private function gcdExtended(string $a, string $b) : array{if ($a === '0') {return [$b, '0', '1'];}[$gcd, $x1, $y1] = $this->gcdExtended($this->mod($b, $a), $a);$x = $this->sub($y1, $this->mul($this->divQ($b, $a), $x1));$y = $x1;return [$gcd, $x, $y];}/*** Returns the square root of the given number, rounded down.** The result is the largest x such that x² ≤ n.* The input MUST NOT be negative.*/abstract public function sqrt(string $n) : string;/*** Converts a number from an arbitrary base.** This method can be overridden by the concrete implementation if the underlying library* has built-in support for base conversion.** @param string $number The number, positive or zero, non-empty, case-insensitively validated for the given base.* @param int $base The base of the number, validated from 2 to 36.** @return string The converted number, following the Calculator conventions.*/public function fromBase(string $number, int $base) : string{return $this->fromArbitraryBase(\strtolower($number), self::ALPHABET, $base);}/*** Converts a number to an arbitrary base.** This method can be overridden by the concrete implementation if the underlying library* has built-in support for base conversion.** @param string $number The number to convert, following the Calculator conventions.* @param int $base The base to convert to, validated from 2 to 36.** @return string The converted number, lowercase.*/public function toBase(string $number, int $base) : string{$negative = ($number[0] === '-');if ($negative) {$number = \substr($number, 1);}$number = $this->toArbitraryBase($number, self::ALPHABET, $base);if ($negative) {return '-' . $number;}return $number;}/*** Converts a non-negative number in an arbitrary base using a custom alphabet, to base 10.** @param string $number The number to convert, validated as a non-empty string,* containing only chars in the given alphabet/base.* @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum.* @param int $base The base of the number, validated from 2 to alphabet length.** @return string The number in base 10, following the Calculator conventions.*/final public function fromArbitraryBase(string $number, string $alphabet, int $base) : string{// remove leading "zeros"$number = \ltrim($number, $alphabet[0]);if ($number === '') {return '0';}// optimize for "one"if ($number === $alphabet[1]) {return '1';}$result = '0';$power = '1';$base = (string) $base;for ($i = \strlen($number) - 1; $i >= 0; $i--) {$index = \strpos($alphabet, $number[$i]);if ($index !== 0) {$result = $this->add($result, ($index === 1)? $power: $this->mul($power, (string) $index));}if ($i !== 0) {$power = $this->mul($power, $base);}}return $result;}/*** Converts a non-negative number to an arbitrary base using a custom alphabet.** @param string $number The number to convert, positive or zero, following the Calculator conventions.* @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum.* @param int $base The base to convert to, validated from 2 to alphabet length.** @return string The converted number in the given alphabet.*/final public function toArbitraryBase(string $number, string $alphabet, int $base) : string{if ($number === '0') {return $alphabet[0];}$base = (string) $base;$result = '';while ($number !== '0') {[$number, $remainder] = $this->divQR($number, $base);$remainder = (int) $remainder;$result .= $alphabet[$remainder];}return \strrev($result);}/*** Performs a rounded division.** Rounding is performed when the remainder of the division is not zero.** @param string $a The dividend.* @param string $b The divisor, must not be zero.* @param int $roundingMode The rounding mode.** @throws \InvalidArgumentException If the rounding mode is invalid.* @throws RoundingNecessaryException If RoundingMode::UNNECESSARY is provided but rounding is necessary.** @psalm-suppress ImpureFunctionCall*/final public function divRound(string $a, string $b, int $roundingMode) : string{[$quotient, $remainder] = $this->divQR($a, $b);$hasDiscardedFraction = ($remainder !== '0');$isPositiveOrZero = ($a[0] === '-') === ($b[0] === '-');$discardedFractionSign = function() use ($remainder, $b) : int {$r = $this->abs($this->mul($remainder, '2'));$b = $this->abs($b);return $this->cmp($r, $b);};$increment = false;switch ($roundingMode) {case RoundingMode::UNNECESSARY:if ($hasDiscardedFraction) {throw RoundingNecessaryException::roundingNecessary();}break;case RoundingMode::UP:$increment = $hasDiscardedFraction;break;case RoundingMode::DOWN:break;case RoundingMode::CEILING:$increment = $hasDiscardedFraction && $isPositiveOrZero;break;case RoundingMode::FLOOR:$increment = $hasDiscardedFraction && ! $isPositiveOrZero;break;case RoundingMode::HALF_UP:$increment = $discardedFractionSign() >= 0;break;case RoundingMode::HALF_DOWN:$increment = $discardedFractionSign() > 0;break;case RoundingMode::HALF_CEILING:$increment = $isPositiveOrZero ? $discardedFractionSign() >= 0 : $discardedFractionSign() > 0;break;case RoundingMode::HALF_FLOOR:$increment = $isPositiveOrZero ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;break;case RoundingMode::HALF_EVEN:$lastDigit = (int) $quotient[-1];$lastDigitIsEven = ($lastDigit % 2 === 0);$increment = $lastDigitIsEven ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;break;default:throw new \InvalidArgumentException('Invalid rounding mode.');}if ($increment) {return $this->add($quotient, $isPositiveOrZero ? '1' : '-1');}return $quotient;}/*** Calculates bitwise AND of two numbers.** This method can be overridden by the concrete implementation if the underlying library* has built-in support for bitwise operations.*/public function and(string $a, string $b) : string{return $this->bitwise('and', $a, $b);}/*** Calculates bitwise OR of two numbers.** This method can be overridden by the concrete implementation if the underlying library* has built-in support for bitwise operations.*/public function or(string $a, string $b) : string{return $this->bitwise('or', $a, $b);}/*** Calculates bitwise XOR of two numbers.** This method can be overridden by the concrete implementation if the underlying library* has built-in support for bitwise operations.*/public function xor(string $a, string $b) : string{return $this->bitwise('xor', $a, $b);}/*** Performs a bitwise operation on a decimal number.** @param 'and'|'or'|'xor' $operator The operator to use.* @param string $a The left operand.* @param string $b The right operand.*/private function bitwise(string $operator, string $a, string $b) : string{[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);$aBin = $this->toBinary($aDig);$bBin = $this->toBinary($bDig);$aLen = \strlen($aBin);$bLen = \strlen($bBin);if ($aLen > $bLen) {$bBin = \str_repeat("\x00", $aLen - $bLen) . $bBin;} elseif ($bLen > $aLen) {$aBin = \str_repeat("\x00", $bLen - $aLen) . $aBin;}if ($aNeg) {$aBin = $this->twosComplement($aBin);}if ($bNeg) {$bBin = $this->twosComplement($bBin);}switch ($operator) {case 'and':$value = $aBin & $bBin;$negative = ($aNeg and $bNeg);break;case 'or':$value = $aBin | $bBin;$negative = ($aNeg or $bNeg);break;case 'xor':$value = $aBin ^ $bBin;$negative = ($aNeg xor $bNeg);break;// @codeCoverageIgnoreStartdefault:throw new \InvalidArgumentException('Invalid bitwise operator.');// @codeCoverageIgnoreEnd}if ($negative) {$value = $this->twosComplement($value);}$result = $this->toDecimal($value);return $negative ? $this->neg($result) : $result;}/*** @param string $number A positive, binary number.*/private function twosComplement(string $number) : string{$xor = \str_repeat("\xff", \strlen($number));$number ^= $xor;for ($i = \strlen($number) - 1; $i >= 0; $i--) {$byte = \ord($number[$i]);if (++$byte !== 256) {$number[$i] = \chr($byte);break;}$number[$i] = "\x00";if ($i === 0) {$number = "\x01" . $number;}}return $number;}/*** Converts a decimal number to a binary string.** @param string $number The number to convert, positive or zero, only digits.*/private function toBinary(string $number) : string{$result = '';while ($number !== '0') {[$number, $remainder] = $this->divQR($number, '256');$result .= \chr((int) $remainder);}return \strrev($result);}/*** Returns the positive decimal representation of a binary number.** @param string $bytes The bytes representing the number.*/private function toDecimal(string $bytes) : string{$result = '0';$power = '1';for ($i = \strlen($bytes) - 1; $i >= 0; $i--) {$index = \ord($bytes[$i]);if ($index !== 0) {$result = $this->add($result, ($index === 1)? $power: $this->mul($power, (string) $index));}if ($i !== 0) {$power = $this->mul($power, '256');}}return $result;}}