• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

brick / math / 21484945185

29 Jan 2026 03:50PM UTC coverage: 99.428%. Remained the same
21484945185

push

github

BenMorel
Remove deprecated BigRational::simplified()

1216 of 1223 relevant lines covered (99.43%)

2550.52 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.45
/src/Internal/Calculator.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Brick\Math\Internal;
6

7
use Brick\Math\Exception\RoundingNecessaryException;
8
use Brick\Math\RoundingMode;
9

10
use function chr;
11
use function ltrim;
12
use function ord;
13
use function str_repeat;
14
use function strlen;
15
use function strpos;
16
use function strrev;
17
use function strtolower;
18
use function substr;
19

20
/**
21
 * Performs basic operations on arbitrary size integers.
22
 *
23
 * Unless otherwise specified, all parameters must be validated as non-empty strings of digits,
24
 * without leading zero, and with an optional leading minus sign if the number is not zero.
25
 *
26
 * Any other parameter format will lead to undefined behaviour.
27
 * All methods must return strings respecting this format, unless specified otherwise.
28
 *
29
 * @internal
30
 */
31
abstract readonly class Calculator
32
{
33
    /**
34
     * The maximum exponent value allowed for the pow() method.
35
     */
36
    public const MAX_POWER = 1_000_000;
37

38
    /**
39
     * The alphabet for converting from and to base 2 to 36, lowercase.
40
     */
41
    public const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
42

43
    /**
44
     * Returns the absolute value of a number.
45
     *
46
     * @pure
47
     */
48
    final public function abs(string $n): string
49
    {
50
        return ($n[0] === '-') ? substr($n, 1) : $n;
7,588✔
51
    }
52

53
    /**
54
     * Negates a number.
55
     *
56
     * @pure
57
     */
58
    final public function neg(string $n): string
59
    {
60
        if ($n === '0') {
9,982✔
61
            return '0';
492✔
62
        }
63

64
        if ($n[0] === '-') {
9,873✔
65
            return substr($n, 1);
2,098✔
66
        }
67

68
        return '-' . $n;
8,023✔
69
    }
70

71
    /**
72
     * Compares two numbers.
73
     *
74
     * Returns -1 if the first number is less than, 0 if equal to, 1 if greater than the second number.
75
     *
76
     * @return -1|0|1
77
     *
78
     * @pure
79
     */
80
    final public function cmp(string $a, string $b): int
81
    {
82
        [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
13,193✔
83

84
        if ($aNeg && ! $bNeg) {
13,193✔
85
            return -1;
258✔
86
        }
87

88
        if ($bNeg && ! $aNeg) {
13,031✔
89
            return 1;
153✔
90
        }
91

92
        $aLen = strlen($aDig);
12,896✔
93
        $bLen = strlen($bDig);
12,896✔
94

95
        if ($aLen < $bLen) {
12,896✔
96
            $result = -1;
3,491✔
97
        } elseif ($aLen > $bLen) {
10,917✔
98
            $result = 1;
3,263✔
99
        } else {
100
            $result = $aDig <=> $bDig;
9,454✔
101
        }
102

103
        return $aNeg ? -$result : $result;
12,896✔
104
    }
105

106
    /**
107
     * Adds two numbers.
108
     *
109
     * @pure
110
     */
111
    abstract public function add(string $a, string $b): string;
112

113
    /**
114
     * Subtracts two numbers.
115
     *
116
     * @pure
117
     */
118
    abstract public function sub(string $a, string $b): string;
119

120
    /**
121
     * Multiplies two numbers.
122
     *
123
     * @pure
124
     */
125
    abstract public function mul(string $a, string $b): string;
126

127
    /**
128
     * Returns the quotient of the division of two numbers.
129
     *
130
     * @param string $a The dividend.
131
     * @param string $b The divisor, must not be zero.
132
     *
133
     * @return string The quotient.
134
     *
135
     * @pure
136
     */
137
    abstract public function divQ(string $a, string $b): string;
138

139
    /**
140
     * Returns the remainder of the division of two numbers.
141
     *
142
     * @param string $a The dividend.
143
     * @param string $b The divisor, must not be zero.
144
     *
145
     * @return string The remainder.
146
     *
147
     * @pure
148
     */
149
    abstract public function divR(string $a, string $b): string;
150

151
    /**
152
     * Returns the quotient and remainder of the division of two numbers.
153
     *
154
     * @param string $a The dividend.
155
     * @param string $b The divisor, must not be zero.
156
     *
157
     * @return array{string, string} An array containing the quotient and remainder.
158
     *
159
     * @pure
160
     */
161
    abstract public function divQR(string $a, string $b): array;
162

163
    /**
164
     * Exponentiates a number.
165
     *
166
     * @param string $a The base number.
167
     * @param int    $e The exponent, validated as an integer between 0 and MAX_POWER.
168
     *
169
     * @return string The power.
170
     *
171
     * @pure
172
     */
173
    abstract public function pow(string $a, int $e): string;
174

175
    /**
176
     * @param string $b The modulus; must not be zero.
177
     *
178
     * @pure
179
     */
180
    public function mod(string $a, string $b): string
181
    {
182
        return $this->divR($this->add($this->divR($a, $b), $b), $b);
232✔
183
    }
184

185
    /**
186
     * Returns the modular multiplicative inverse of $x modulo $m.
187
     *
188
     * If $x has no multiplicative inverse mod m, this method must return null.
189
     *
190
     * This method can be overridden by the concrete implementation if the underlying library has built-in support.
191
     *
192
     * @param string $m The modulus; must not be negative or zero.
193
     *
194
     * @pure
195
     */
196
    public function modInverse(string $x, string $m): ?string
197
    {
198
        if ($m === '1') {
42✔
199
            return '0';
×
200
        }
201

202
        $modVal = $x;
42✔
203

204
        if ($x[0] === '-' || ($this->cmp($this->abs($x), $m) >= 0)) {
42✔
205
            $modVal = $this->mod($x, $m);
16✔
206
        }
207

208
        [$g, $x] = $this->gcdExtended($modVal, $m);
42✔
209

210
        if ($g !== '1') {
42✔
211
            return null;
10✔
212
        }
213

214
        return $this->mod($this->add($this->mod($x, $m), $m), $m);
32✔
215
    }
216

217
    /**
218
     * Raises a number into power with modulo.
219
     *
220
     * @param string $base The base number; must be positive or zero.
221
     * @param string $exp  The exponent; must be positive or zero.
222
     * @param string $mod  The modulus; must be strictly positive.
223
     *
224
     * @pure
225
     */
226
    abstract public function modPow(string $base, string $exp, string $mod): string;
227

228
    /**
229
     * Returns the greatest common divisor of the two numbers.
230
     *
231
     * This method can be overridden by the concrete implementation if the underlying library
232
     * has built-in support for GCD calculations.
233
     *
234
     * @return string The GCD, always positive, or zero if both arguments are zero.
235
     *
236
     * @pure
237
     */
238
    public function gcd(string $a, string $b): string
239
    {
240
        if ($a === '0') {
3,198✔
241
            return $this->abs($b);
8✔
242
        }
243

244
        if ($b === '0') {
3,190✔
245
            return $this->abs($a);
3,190✔
246
        }
247

248
        return $this->gcd($b, $this->divR($a, $b));
3,182✔
249
    }
250

251
    /**
252
     * Returns the square root of the given number, rounded down.
253
     *
254
     * The result is the largest x such that x² ≤ n.
255
     * The input MUST NOT be negative.
256
     *
257
     * @pure
258
     */
259
    abstract public function sqrt(string $n): string;
260

261
    /**
262
     * Converts a number from an arbitrary base.
263
     *
264
     * This method can be overridden by the concrete implementation if the underlying library
265
     * has built-in support for base conversion.
266
     *
267
     * @param string $number The number, positive or zero, non-empty, case-insensitively validated for the given base.
268
     * @param int    $base   The base of the number, validated from 2 to 36.
269
     *
270
     * @return string The converted number, following the Calculator conventions.
271
     *
272
     * @pure
273
     */
274
    public function fromBase(string $number, int $base): string
275
    {
276
        return $this->fromArbitraryBase(strtolower($number), self::ALPHABET, $base);
1,187✔
277
    }
278

279
    /**
280
     * Converts a number to an arbitrary base.
281
     *
282
     * This method can be overridden by the concrete implementation if the underlying library
283
     * has built-in support for base conversion.
284
     *
285
     * @param string $number The number to convert, following the Calculator conventions.
286
     * @param int    $base   The base to convert to, validated from 2 to 36.
287
     *
288
     * @return string The converted number, lowercase.
289
     *
290
     * @pure
291
     */
292
    public function toBase(string $number, int $base): string
293
    {
294
        $negative = ($number[0] === '-');
844✔
295

296
        if ($negative) {
844✔
297
            $number = substr($number, 1);
140✔
298
        }
299

300
        $number = $this->toArbitraryBase($number, self::ALPHABET, $base);
844✔
301

302
        if ($negative) {
844✔
303
            return '-' . $number;
140✔
304
        }
305

306
        return $number;
704✔
307
    }
308

309
    /**
310
     * Converts a non-negative number in an arbitrary base using a custom alphabet, to base 10.
311
     *
312
     * @param string $number   The number to convert, validated as a non-empty string,
313
     *                         containing only chars in the given alphabet/base.
314
     * @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum.
315
     * @param int    $base     The base of the number, validated from 2 to alphabet length.
316
     *
317
     * @return string The number in base 10, following the Calculator conventions.
318
     *
319
     * @pure
320
     */
321
    final public function fromArbitraryBase(string $number, string $alphabet, int $base): string
322
    {
323
        // remove leading "zeros"
324
        $number = ltrim($number, $alphabet[0]);
1,565✔
325

326
        if ($number === '') {
1,565✔
327
            return '0';
9✔
328
        }
329

330
        // optimize for "one"
331
        if ($number === $alphabet[1]) {
1,556✔
332
            return '1';
9✔
333
        }
334

335
        $result = '0';
1,547✔
336
        $power = '1';
1,547✔
337

338
        $base = (string) $base;
1,547✔
339

340
        for ($i = strlen($number) - 1; $i >= 0; $i--) {
1,547✔
341
            $index = strpos($alphabet, $number[$i]);
1,547✔
342

343
            if ($index !== 0) {
1,547✔
344
                $result = $this->add(
1,547✔
345
                    $result,
1,547✔
346
                    ($index === 1) ? $power : $this->mul($power, (string) $index),
1,547✔
347
                );
1,547✔
348
            }
349

350
            if ($i !== 0) {
1,547✔
351
                $power = $this->mul($power, $base);
1,529✔
352
            }
353
        }
354

355
        return $result;
1,547✔
356
    }
357

358
    /**
359
     * Converts a non-negative number to an arbitrary base using a custom alphabet.
360
     *
361
     * @param string $number   The number to convert, positive or zero, following the Calculator conventions.
362
     * @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum.
363
     * @param int    $base     The base to convert to, validated from 2 to alphabet length.
364
     *
365
     * @return string The converted number in the given alphabet.
366
     *
367
     * @pure
368
     */
369
    final public function toArbitraryBase(string $number, string $alphabet, int $base): string
370
    {
371
        if ($number === '0') {
970✔
372
            return $alphabet[0];
11✔
373
        }
374

375
        $base = (string) $base;
959✔
376
        $result = '';
959✔
377

378
        while ($number !== '0') {
959✔
379
            [$number, $remainder] = $this->divQR($number, $base);
959✔
380
            $remainder = (int) $remainder;
959✔
381

382
            $result .= $alphabet[$remainder];
959✔
383
        }
384

385
        return strrev($result);
959✔
386
    }
387

388
    /**
389
     * Performs a rounded division.
390
     *
391
     * Rounding is performed when the remainder of the division is not zero.
392
     *
393
     * @param string       $a            The dividend.
394
     * @param string       $b            The divisor, must not be zero.
395
     * @param RoundingMode $roundingMode The rounding mode.
396
     *
397
     * @throws RoundingNecessaryException If RoundingMode::Unnecessary is provided but rounding is necessary.
398
     *
399
     * @pure
400
     */
401
    final public function divRound(string $a, string $b, RoundingMode $roundingMode): string
402
    {
403
        [$quotient, $remainder] = $this->divQR($a, $b);
9,645✔
404

405
        $hasDiscardedFraction = ($remainder !== '0');
9,645✔
406
        $isPositiveOrZero = ($a[0] === '-') === ($b[0] === '-');
9,645✔
407

408
        $discardedFractionSign = function () use ($remainder, $b): int {
9,645✔
409
            $r = $this->abs($this->mul($remainder, '2'));
4,356✔
410
            $b = $this->abs($b);
4,356✔
411

412
            return $this->cmp($r, $b);
4,356✔
413
        };
9,645✔
414

415
        $increment = false;
9,645✔
416

417
        switch ($roundingMode) {
418
            case RoundingMode::Unnecessary:
9,645✔
419
                if ($hasDiscardedFraction) {
1,182✔
420
                    throw RoundingNecessaryException::roundingNecessary();
537✔
421
                }
422

423
                break;
669✔
424

425
            case RoundingMode::Up:
8,463✔
426
                $increment = $hasDiscardedFraction;
1,476✔
427

428
                break;
1,476✔
429

430
            case RoundingMode::Down:
6,987✔
431
                break;
879✔
432

433
            case RoundingMode::Ceiling:
6,108✔
434
                $increment = $hasDiscardedFraction && $isPositiveOrZero;
882✔
435

436
                break;
882✔
437

438
            case RoundingMode::Floor:
5,226✔
439
                $increment = $hasDiscardedFraction && ! $isPositiveOrZero;
870✔
440

441
                break;
870✔
442

443
            case RoundingMode::HalfUp:
4,356✔
444
                $increment = $discardedFractionSign() >= 0;
1,998✔
445

446
                break;
1,998✔
447

448
            case RoundingMode::HalfDown:
2,358✔
449
                $increment = $discardedFractionSign() > 0;
498✔
450

451
                break;
498✔
452

453
            case RoundingMode::HalfCeiling:
1,860✔
454
                $increment = $isPositiveOrZero ? $discardedFractionSign() >= 0 : $discardedFractionSign() > 0;
870✔
455

456
                break;
870✔
457

458
            case RoundingMode::HalfFloor:
990✔
459
                $increment = $isPositiveOrZero ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
495✔
460

461
                break;
495✔
462

463
            case RoundingMode::HalfEven:
495✔
464
                $lastDigit = (int) $quotient[-1];
495✔
465
                $lastDigitIsEven = ($lastDigit % 2 === 0);
495✔
466
                $increment = $lastDigitIsEven ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
495✔
467

468
                break;
495✔
469
        }
470

471
        if ($increment) {
9,132✔
472
            return $this->add($quotient, $isPositiveOrZero ? '1' : '-1');
3,447✔
473
        }
474

475
        return $quotient;
6,381✔
476
    }
477

478
    /**
479
     * Calculates bitwise AND of two numbers.
480
     *
481
     * This method can be overridden by the concrete implementation if the underlying library
482
     * has built-in support for bitwise operations.
483
     *
484
     * @pure
485
     */
486
    public function and(string $a, string $b): string
487
    {
488
        return $this->bitwise('and', $a, $b);
102✔
489
    }
490

491
    /**
492
     * Calculates bitwise OR of two numbers.
493
     *
494
     * This method can be overridden by the concrete implementation if the underlying library
495
     * has built-in support for bitwise operations.
496
     *
497
     * @pure
498
     */
499
    public function or(string $a, string $b): string
500
    {
501
        return $this->bitwise('or', $a, $b);
92✔
502
    }
503

504
    /**
505
     * Calculates bitwise XOR of two numbers.
506
     *
507
     * This method can be overridden by the concrete implementation if the underlying library
508
     * has built-in support for bitwise operations.
509
     *
510
     * @pure
511
     */
512
    public function xor(string $a, string $b): string
513
    {
514
        return $this->bitwise('xor', $a, $b);
96✔
515
    }
516

517
    /**
518
     * Extracts the sign & digits of the operands.
519
     *
520
     * @return array{bool, bool, string, string} Whether $a and $b are negative, followed by their digits.
521
     *
522
     * @pure
523
     */
524
    final protected function init(string $a, string $b): array
525
    {
526
        return [
15,411✔
527
            $aNeg = ($a[0] === '-'),
15,411✔
528
            $bNeg = ($b[0] === '-'),
15,411✔
529

530
            $aNeg ? substr($a, 1) : $a,
15,411✔
531
            $bNeg ? substr($b, 1) : $b,
15,411✔
532
        ];
15,411✔
533
    }
534

535
    /**
536
     * @return array{string, string, string} GCD, X, Y
537
     *
538
     * @pure
539
     */
540
    private function gcdExtended(string $a, string $b): array
541
    {
542
        if ($a === '0') {
42✔
543
            return [$b, '0', '1'];
42✔
544
        }
545

546
        [$gcd, $x1, $y1] = $this->gcdExtended($this->mod($b, $a), $a);
38✔
547

548
        $x = $this->sub($y1, $this->mul($this->divQ($b, $a), $x1));
38✔
549
        $y = $x1;
38✔
550

551
        return [$gcd, $x, $y];
38✔
552
    }
553

554
    /**
555
     * Performs a bitwise operation on a decimal number.
556
     *
557
     * @param 'and'|'or'|'xor' $operator The operator to use.
558
     * @param string           $a        The left operand.
559
     * @param string           $b        The right operand.
560
     *
561
     * @pure
562
     */
563
    private function bitwise(string $operator, string $a, string $b): string
564
    {
565
        [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
290✔
566

567
        $aBin = $this->toBinary($aDig);
290✔
568
        $bBin = $this->toBinary($bDig);
290✔
569

570
        $aLen = strlen($aBin);
290✔
571
        $bLen = strlen($bBin);
290✔
572

573
        if ($aLen > $bLen) {
290✔
574
            $bBin = str_repeat("\x00", $aLen - $bLen) . $bBin;
102✔
575
        } elseif ($bLen > $aLen) {
188✔
576
            $aBin = str_repeat("\x00", $bLen - $aLen) . $aBin;
132✔
577
        }
578

579
        if ($aNeg) {
290✔
580
            $aBin = $this->twosComplement($aBin);
146✔
581
        }
582
        if ($bNeg) {
290✔
583
            $bBin = $this->twosComplement($bBin);
132✔
584
        }
585

586
        $value = match ($operator) {
290✔
587
            'and' => $aBin & $bBin,
102✔
588
            'or' => $aBin | $bBin,
92✔
589
            'xor' => $aBin ^ $bBin,
96✔
590
        };
290✔
591

592
        $negative = match ($operator) {
290✔
593
            'and' => $aNeg and $bNeg,
102✔
594
            'or' => $aNeg or $bNeg,
92✔
595
            'xor' => $aNeg xor $bNeg,
96✔
596
        };
290✔
597

598
        if ($negative) {
290✔
599
            $value = $this->twosComplement($value);
136✔
600
        }
601

602
        $result = $this->toDecimal($value);
290✔
603

604
        return $negative ? $this->neg($result) : $result;
290✔
605
    }
606

607
    /**
608
     * @param string $number A positive, binary number.
609
     *
610
     * @pure
611
     */
612
    private function twosComplement(string $number): string
613
    {
614
        $xor = str_repeat("\xff", strlen($number));
212✔
615

616
        $number ^= $xor;
212✔
617

618
        for ($i = strlen($number) - 1; $i >= 0; $i--) {
212✔
619
            $byte = ord($number[$i]);
212✔
620

621
            if (++$byte !== 256) {
212✔
622
                $number[$i] = chr($byte);
212✔
623

624
                break;
212✔
625
            }
626

627
            $number[$i] = "\x00";
8✔
628

629
            if ($i === 0) {
8✔
630
                $number = "\x01" . $number;
4✔
631
            }
632
        }
633

634
        return $number;
212✔
635
    }
636

637
    /**
638
     * Converts a decimal number to a binary string.
639
     *
640
     * @param string $number The number to convert, positive or zero, only digits.
641
     *
642
     * @pure
643
     */
644
    private function toBinary(string $number): string
645
    {
646
        $result = '';
290✔
647

648
        while ($number !== '0') {
290✔
649
            [$number, $remainder] = $this->divQR($number, '256');
290✔
650
            $result .= chr((int) $remainder);
290✔
651
        }
652

653
        return strrev($result);
290✔
654
    }
655

656
    /**
657
     * Returns the positive decimal representation of a binary number.
658
     *
659
     * @param string $bytes The bytes representing the number.
660
     *
661
     * @pure
662
     */
663
    private function toDecimal(string $bytes): string
664
    {
665
        $result = '0';
290✔
666
        $power = '1';
290✔
667

668
        for ($i = strlen($bytes) - 1; $i >= 0; $i--) {
290✔
669
            $index = ord($bytes[$i]);
290✔
670

671
            if ($index !== 0) {
290✔
672
                $result = $this->add(
286✔
673
                    $result,
286✔
674
                    ($index === 1) ? $power : $this->mul($power, (string) $index),
286✔
675
                );
286✔
676
            }
677

678
            if ($i !== 0) {
290✔
679
                $power = $this->mul($power, '256');
264✔
680
            }
681
        }
682

683
        return $result;
290✔
684
    }
685
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc