Calculator.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685
  1. <?php
  2. declare(strict_types=1);
  3. namespace Brick\Math\Internal;
  4. use Brick\Math\Exception\RoundingNecessaryException;
  5. use Brick\Math\RoundingMode;
  6. use function chr;
  7. use function ltrim;
  8. use function ord;
  9. use function str_repeat;
  10. use function strlen;
  11. use function strpos;
  12. use function strrev;
  13. use function strtolower;
  14. use function substr;
  15. /**
  16. * Performs basic operations on arbitrary size integers.
  17. *
  18. * Unless otherwise specified, all parameters must be validated as non-empty strings of digits,
  19. * without leading zero, and with an optional leading minus sign if the number is not zero.
  20. *
  21. * Any other parameter format will lead to undefined behaviour.
  22. * All methods must return strings respecting this format, unless specified otherwise.
  23. *
  24. * @internal
  25. */
  26. abstract readonly class Calculator
  27. {
  28. /**
  29. * The maximum exponent value allowed for the pow() method.
  30. */
  31. public const MAX_POWER = 1_000_000;
  32. /**
  33. * The alphabet for converting from and to base 2 to 36, lowercase.
  34. */
  35. public const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
  36. /**
  37. * Returns the absolute value of a number.
  38. *
  39. * @pure
  40. */
  41. final public function abs(string $n): string
  42. {
  43. return ($n[0] === '-') ? substr($n, 1) : $n;
  44. }
  45. /**
  46. * Negates a number.
  47. *
  48. * @pure
  49. */
  50. final public function neg(string $n): string
  51. {
  52. if ($n === '0') {
  53. return '0';
  54. }
  55. if ($n[0] === '-') {
  56. return substr($n, 1);
  57. }
  58. return '-' . $n;
  59. }
  60. /**
  61. * Compares two numbers.
  62. *
  63. * Returns -1 if the first number is less than, 0 if equal to, 1 if greater than the second number.
  64. *
  65. * @return -1|0|1
  66. *
  67. * @pure
  68. */
  69. final public function cmp(string $a, string $b): int
  70. {
  71. [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
  72. if ($aNeg && ! $bNeg) {
  73. return -1;
  74. }
  75. if ($bNeg && ! $aNeg) {
  76. return 1;
  77. }
  78. $aLen = strlen($aDig);
  79. $bLen = strlen($bDig);
  80. if ($aLen < $bLen) {
  81. $result = -1;
  82. } elseif ($aLen > $bLen) {
  83. $result = 1;
  84. } else {
  85. $result = $aDig <=> $bDig;
  86. }
  87. return $aNeg ? -$result : $result;
  88. }
  89. /**
  90. * Adds two numbers.
  91. *
  92. * @pure
  93. */
  94. abstract public function add(string $a, string $b): string;
  95. /**
  96. * Subtracts two numbers.
  97. *
  98. * @pure
  99. */
  100. abstract public function sub(string $a, string $b): string;
  101. /**
  102. * Multiplies two numbers.
  103. *
  104. * @pure
  105. */
  106. abstract public function mul(string $a, string $b): string;
  107. /**
  108. * Returns the quotient of the division of two numbers.
  109. *
  110. * @param string $a The dividend.
  111. * @param string $b The divisor, must not be zero.
  112. *
  113. * @return string The quotient.
  114. *
  115. * @pure
  116. */
  117. abstract public function divQ(string $a, string $b): string;
  118. /**
  119. * Returns the remainder of the division of two numbers.
  120. *
  121. * @param string $a The dividend.
  122. * @param string $b The divisor, must not be zero.
  123. *
  124. * @return string The remainder.
  125. *
  126. * @pure
  127. */
  128. abstract public function divR(string $a, string $b): string;
  129. /**
  130. * Returns the quotient and remainder of the division of two numbers.
  131. *
  132. * @param string $a The dividend.
  133. * @param string $b The divisor, must not be zero.
  134. *
  135. * @return array{string, string} An array containing the quotient and remainder.
  136. *
  137. * @pure
  138. */
  139. abstract public function divQR(string $a, string $b): array;
  140. /**
  141. * Exponentiates a number.
  142. *
  143. * @param string $a The base number.
  144. * @param int $e The exponent, validated as an integer between 0 and MAX_POWER.
  145. *
  146. * @return string The power.
  147. *
  148. * @pure
  149. */
  150. abstract public function pow(string $a, int $e): string;
  151. /**
  152. * @param string $b The modulus; must not be zero.
  153. *
  154. * @pure
  155. */
  156. public function mod(string $a, string $b): string
  157. {
  158. return $this->divR($this->add($this->divR($a, $b), $b), $b);
  159. }
  160. /**
  161. * Returns the modular multiplicative inverse of $x modulo $m.
  162. *
  163. * If $x has no multiplicative inverse mod m, this method must return null.
  164. *
  165. * This method can be overridden by the concrete implementation if the underlying library has built-in support.
  166. *
  167. * @param string $m The modulus; must not be negative or zero.
  168. *
  169. * @pure
  170. */
  171. public function modInverse(string $x, string $m): ?string
  172. {
  173. if ($m === '1') {
  174. return '0';
  175. }
  176. $modVal = $x;
  177. if ($x[0] === '-' || ($this->cmp($this->abs($x), $m) >= 0)) {
  178. $modVal = $this->mod($x, $m);
  179. }
  180. [$g, $x] = $this->gcdExtended($modVal, $m);
  181. if ($g !== '1') {
  182. return null;
  183. }
  184. return $this->mod($this->add($this->mod($x, $m), $m), $m);
  185. }
  186. /**
  187. * Raises a number into power with modulo.
  188. *
  189. * @param string $base The base number; must be positive or zero.
  190. * @param string $exp The exponent; must be positive or zero.
  191. * @param string $mod The modulus; must be strictly positive.
  192. *
  193. * @pure
  194. */
  195. abstract public function modPow(string $base, string $exp, string $mod): string;
  196. /**
  197. * Returns the greatest common divisor of the two numbers.
  198. *
  199. * This method can be overridden by the concrete implementation if the underlying library
  200. * has built-in support for GCD calculations.
  201. *
  202. * @return string The GCD, always positive, or zero if both arguments are zero.
  203. *
  204. * @pure
  205. */
  206. public function gcd(string $a, string $b): string
  207. {
  208. if ($a === '0') {
  209. return $this->abs($b);
  210. }
  211. if ($b === '0') {
  212. return $this->abs($a);
  213. }
  214. return $this->gcd($b, $this->divR($a, $b));
  215. }
  216. /**
  217. * Returns the square root of the given number, rounded down.
  218. *
  219. * The result is the largest x such that x² ≤ n.
  220. * The input MUST NOT be negative.
  221. *
  222. * @pure
  223. */
  224. abstract public function sqrt(string $n): string;
  225. /**
  226. * Converts a number from an arbitrary base.
  227. *
  228. * This method can be overridden by the concrete implementation if the underlying library
  229. * has built-in support for base conversion.
  230. *
  231. * @param string $number The number, positive or zero, non-empty, case-insensitively validated for the given base.
  232. * @param int $base The base of the number, validated from 2 to 36.
  233. *
  234. * @return string The converted number, following the Calculator conventions.
  235. *
  236. * @pure
  237. */
  238. public function fromBase(string $number, int $base): string
  239. {
  240. return $this->fromArbitraryBase(strtolower($number), self::ALPHABET, $base);
  241. }
  242. /**
  243. * Converts a number to an arbitrary base.
  244. *
  245. * This method can be overridden by the concrete implementation if the underlying library
  246. * has built-in support for base conversion.
  247. *
  248. * @param string $number The number to convert, following the Calculator conventions.
  249. * @param int $base The base to convert to, validated from 2 to 36.
  250. *
  251. * @return string The converted number, lowercase.
  252. *
  253. * @pure
  254. */
  255. public function toBase(string $number, int $base): string
  256. {
  257. $negative = ($number[0] === '-');
  258. if ($negative) {
  259. $number = substr($number, 1);
  260. }
  261. $number = $this->toArbitraryBase($number, self::ALPHABET, $base);
  262. if ($negative) {
  263. return '-' . $number;
  264. }
  265. return $number;
  266. }
  267. /**
  268. * Converts a non-negative number in an arbitrary base using a custom alphabet, to base 10.
  269. *
  270. * @param string $number The number to convert, validated as a non-empty string,
  271. * containing only chars in the given alphabet/base.
  272. * @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum.
  273. * @param int $base The base of the number, validated from 2 to alphabet length.
  274. *
  275. * @return string The number in base 10, following the Calculator conventions.
  276. *
  277. * @pure
  278. */
  279. final public function fromArbitraryBase(string $number, string $alphabet, int $base): string
  280. {
  281. // remove leading "zeros"
  282. $number = ltrim($number, $alphabet[0]);
  283. if ($number === '') {
  284. return '0';
  285. }
  286. // optimize for "one"
  287. if ($number === $alphabet[1]) {
  288. return '1';
  289. }
  290. $result = '0';
  291. $power = '1';
  292. $base = (string) $base;
  293. for ($i = strlen($number) - 1; $i >= 0; $i--) {
  294. $index = strpos($alphabet, $number[$i]);
  295. if ($index !== 0) {
  296. $result = $this->add(
  297. $result,
  298. ($index === 1) ? $power : $this->mul($power, (string) $index),
  299. );
  300. }
  301. if ($i !== 0) {
  302. $power = $this->mul($power, $base);
  303. }
  304. }
  305. return $result;
  306. }
  307. /**
  308. * Converts a non-negative number to an arbitrary base using a custom alphabet.
  309. *
  310. * @param string $number The number to convert, positive or zero, following the Calculator conventions.
  311. * @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum.
  312. * @param int $base The base to convert to, validated from 2 to alphabet length.
  313. *
  314. * @return string The converted number in the given alphabet.
  315. *
  316. * @pure
  317. */
  318. final public function toArbitraryBase(string $number, string $alphabet, int $base): string
  319. {
  320. if ($number === '0') {
  321. return $alphabet[0];
  322. }
  323. $base = (string) $base;
  324. $result = '';
  325. while ($number !== '0') {
  326. [$number, $remainder] = $this->divQR($number, $base);
  327. $remainder = (int) $remainder;
  328. $result .= $alphabet[$remainder];
  329. }
  330. return strrev($result);
  331. }
  332. /**
  333. * Performs a rounded division.
  334. *
  335. * Rounding is performed when the remainder of the division is not zero.
  336. *
  337. * @param string $a The dividend.
  338. * @param string $b The divisor, must not be zero.
  339. * @param RoundingMode $roundingMode The rounding mode.
  340. *
  341. * @throws RoundingNecessaryException If RoundingMode::UNNECESSARY is provided but rounding is necessary.
  342. *
  343. * @pure
  344. */
  345. final public function divRound(string $a, string $b, RoundingMode $roundingMode): string
  346. {
  347. [$quotient, $remainder] = $this->divQR($a, $b);
  348. $hasDiscardedFraction = ($remainder !== '0');
  349. $isPositiveOrZero = ($a[0] === '-') === ($b[0] === '-');
  350. $discardedFractionSign = function () use ($remainder, $b): int {
  351. $r = $this->abs($this->mul($remainder, '2'));
  352. $b = $this->abs($b);
  353. return $this->cmp($r, $b);
  354. };
  355. $increment = false;
  356. switch ($roundingMode) {
  357. case RoundingMode::UNNECESSARY:
  358. if ($hasDiscardedFraction) {
  359. throw RoundingNecessaryException::roundingNecessary();
  360. }
  361. break;
  362. case RoundingMode::UP:
  363. $increment = $hasDiscardedFraction;
  364. break;
  365. case RoundingMode::DOWN:
  366. break;
  367. case RoundingMode::CEILING:
  368. $increment = $hasDiscardedFraction && $isPositiveOrZero;
  369. break;
  370. case RoundingMode::FLOOR:
  371. $increment = $hasDiscardedFraction && ! $isPositiveOrZero;
  372. break;
  373. case RoundingMode::HALF_UP:
  374. $increment = $discardedFractionSign() >= 0;
  375. break;
  376. case RoundingMode::HALF_DOWN:
  377. $increment = $discardedFractionSign() > 0;
  378. break;
  379. case RoundingMode::HALF_CEILING:
  380. $increment = $isPositiveOrZero ? $discardedFractionSign() >= 0 : $discardedFractionSign() > 0;
  381. break;
  382. case RoundingMode::HALF_FLOOR:
  383. $increment = $isPositiveOrZero ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
  384. break;
  385. case RoundingMode::HALF_EVEN:
  386. $lastDigit = (int) $quotient[-1];
  387. $lastDigitIsEven = ($lastDigit % 2 === 0);
  388. $increment = $lastDigitIsEven ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
  389. break;
  390. }
  391. if ($increment) {
  392. return $this->add($quotient, $isPositiveOrZero ? '1' : '-1');
  393. }
  394. return $quotient;
  395. }
  396. /**
  397. * Calculates bitwise AND of two numbers.
  398. *
  399. * This method can be overridden by the concrete implementation if the underlying library
  400. * has built-in support for bitwise operations.
  401. *
  402. * @pure
  403. */
  404. public function and(string $a, string $b): string
  405. {
  406. return $this->bitwise('and', $a, $b);
  407. }
  408. /**
  409. * Calculates bitwise OR of two numbers.
  410. *
  411. * This method can be overridden by the concrete implementation if the underlying library
  412. * has built-in support for bitwise operations.
  413. *
  414. * @pure
  415. */
  416. public function or(string $a, string $b): string
  417. {
  418. return $this->bitwise('or', $a, $b);
  419. }
  420. /**
  421. * Calculates bitwise XOR of two numbers.
  422. *
  423. * This method can be overridden by the concrete implementation if the underlying library
  424. * has built-in support for bitwise operations.
  425. *
  426. * @pure
  427. */
  428. public function xor(string $a, string $b): string
  429. {
  430. return $this->bitwise('xor', $a, $b);
  431. }
  432. /**
  433. * Extracts the sign & digits of the operands.
  434. *
  435. * @return array{bool, bool, string, string} Whether $a and $b are negative, followed by their digits.
  436. *
  437. * @pure
  438. */
  439. final protected function init(string $a, string $b): array
  440. {
  441. return [
  442. $aNeg = ($a[0] === '-'),
  443. $bNeg = ($b[0] === '-'),
  444. $aNeg ? substr($a, 1) : $a,
  445. $bNeg ? substr($b, 1) : $b,
  446. ];
  447. }
  448. /**
  449. * @return array{string, string, string} GCD, X, Y
  450. *
  451. * @pure
  452. */
  453. private function gcdExtended(string $a, string $b): array
  454. {
  455. if ($a === '0') {
  456. return [$b, '0', '1'];
  457. }
  458. [$gcd, $x1, $y1] = $this->gcdExtended($this->mod($b, $a), $a);
  459. $x = $this->sub($y1, $this->mul($this->divQ($b, $a), $x1));
  460. $y = $x1;
  461. return [$gcd, $x, $y];
  462. }
  463. /**
  464. * Performs a bitwise operation on a decimal number.
  465. *
  466. * @param 'and'|'or'|'xor' $operator The operator to use.
  467. * @param string $a The left operand.
  468. * @param string $b The right operand.
  469. *
  470. * @pure
  471. */
  472. private function bitwise(string $operator, string $a, string $b): string
  473. {
  474. [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
  475. $aBin = $this->toBinary($aDig);
  476. $bBin = $this->toBinary($bDig);
  477. $aLen = strlen($aBin);
  478. $bLen = strlen($bBin);
  479. if ($aLen > $bLen) {
  480. $bBin = str_repeat("\x00", $aLen - $bLen) . $bBin;
  481. } elseif ($bLen > $aLen) {
  482. $aBin = str_repeat("\x00", $bLen - $aLen) . $aBin;
  483. }
  484. if ($aNeg) {
  485. $aBin = $this->twosComplement($aBin);
  486. }
  487. if ($bNeg) {
  488. $bBin = $this->twosComplement($bBin);
  489. }
  490. $value = match ($operator) {
  491. 'and' => $aBin & $bBin,
  492. 'or' => $aBin | $bBin,
  493. 'xor' => $aBin ^ $bBin,
  494. };
  495. $negative = match ($operator) {
  496. 'and' => $aNeg and $bNeg,
  497. 'or' => $aNeg or $bNeg,
  498. 'xor' => $aNeg xor $bNeg,
  499. };
  500. if ($negative) {
  501. $value = $this->twosComplement($value);
  502. }
  503. $result = $this->toDecimal($value);
  504. return $negative ? $this->neg($result) : $result;
  505. }
  506. /**
  507. * @param string $number A positive, binary number.
  508. *
  509. * @pure
  510. */
  511. private function twosComplement(string $number): string
  512. {
  513. $xor = str_repeat("\xff", strlen($number));
  514. $number ^= $xor;
  515. for ($i = strlen($number) - 1; $i >= 0; $i--) {
  516. $byte = ord($number[$i]);
  517. if (++$byte !== 256) {
  518. $number[$i] = chr($byte);
  519. break;
  520. }
  521. $number[$i] = "\x00";
  522. if ($i === 0) {
  523. $number = "\x01" . $number;
  524. }
  525. }
  526. return $number;
  527. }
  528. /**
  529. * Converts a decimal number to a binary string.
  530. *
  531. * @param string $number The number to convert, positive or zero, only digits.
  532. *
  533. * @pure
  534. */
  535. private function toBinary(string $number): string
  536. {
  537. $result = '';
  538. while ($number !== '0') {
  539. [$number, $remainder] = $this->divQR($number, '256');
  540. $result .= chr((int) $remainder);
  541. }
  542. return strrev($result);
  543. }
  544. /**
  545. * Returns the positive decimal representation of a binary number.
  546. *
  547. * @param string $bytes The bytes representing the number.
  548. *
  549. * @pure
  550. */
  551. private function toDecimal(string $bytes): string
  552. {
  553. $result = '0';
  554. $power = '1';
  555. for ($i = strlen($bytes) - 1; $i >= 0; $i--) {
  556. $index = ord($bytes[$i]);
  557. if ($index !== 0) {
  558. $result = $this->add(
  559. $result,
  560. ($index === 1) ? $power : $this->mul($power, (string) $index),
  561. );
  562. }
  563. if ($i !== 0) {
  564. $power = $this->mul($power, '256');
  565. }
  566. }
  567. return $result;
  568. }
  569. }