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

brick / math / 21799125930

08 Feb 2026 01:37PM UTC coverage: 99.188% (+0.7%) from 98.522%
21799125930

push

github

BenMorel
Add fast-path optimizations

3 of 3 new or added lines in 1 file covered. (100.0%)

6 existing lines in 3 files now uncovered.

1343 of 1354 relevant lines covered (99.19%)

2301.93 hits per line

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

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

3
declare(strict_types=1);
4

5
namespace Brick\Math\Internal;
6

7
use Brick\Math\RoundingMode;
8

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

19
/**
20
 * Performs basic operations on arbitrary size integers.
21
 *
22
 * Unless otherwise specified, all parameters must be validated as non-empty strings of digits,
23
 * without leading zero, and with an optional leading minus sign if the number is not zero.
24
 *
25
 * Any other parameter format will lead to undefined behaviour.
26
 * All methods must return strings respecting this format, unless specified otherwise.
27
 *
28
 * @internal
29
 */
30
abstract readonly class Calculator
31
{
32
    /**
33
     * The alphabet for converting from and to base 2 to 36, lowercase.
34
     */
35
    public const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
36

37
    /**
38
     * Returns the absolute value of a number.
39
     *
40
     * @pure
41
     */
42
    final public function abs(string $n): string
43
    {
44
        return ($n[0] === '-') ? substr($n, 1) : $n;
7,187✔
45
    }
46

47
    /**
48
     * Negates a number.
49
     *
50
     * @pure
51
     */
52
    final public function neg(string $n): string
53
    {
54
        if ($n === '0') {
10,855✔
55
            return '0';
480✔
56
        }
57

58
        if ($n[0] === '-') {
10,754✔
59
            return substr($n, 1);
3,122✔
60
        }
61

62
        return '-' . $n;
8,032✔
63
    }
64

65
    /**
66
     * Compares two numbers.
67
     *
68
     * Returns -1 if the first number is less than, 0 if equal to, 1 if greater than the second number.
69
     *
70
     * @return -1|0|1
71
     *
72
     * @pure
73
     */
74
    final public function cmp(string $a, string $b): int
75
    {
76
        [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
12,551✔
77

78
        if ($aNeg && ! $bNeg) {
12,551✔
79
            return -1;
258✔
80
        }
81

82
        if ($bNeg && ! $aNeg) {
12,389✔
83
            return 1;
153✔
84
        }
85

86
        $aLen = strlen($aDig);
12,254✔
87
        $bLen = strlen($bDig);
12,254✔
88

89
        if ($aLen < $bLen) {
12,254✔
90
            $result = -1;
2,414✔
91
        } elseif ($aLen > $bLen) {
10,977✔
92
            $result = 1;
3,278✔
93
        } else {
94
            $result = $aDig <=> $bDig;
9,508✔
95
        }
96

97
        return $aNeg ? -$result : $result;
12,254✔
98
    }
99

100
    /**
101
     * Adds two numbers.
102
     *
103
     * @pure
104
     */
105
    abstract public function add(string $a, string $b): string;
106

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

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

121
    /**
122
     * Returns the quotient of the division of two numbers.
123
     *
124
     * @param string $a The dividend.
125
     * @param string $b The divisor, must not be zero.
126
     *
127
     * @return string The quotient.
128
     *
129
     * @pure
130
     */
131
    abstract public function divQ(string $a, string $b): string;
132

133
    /**
134
     * Returns the remainder of the division of two numbers.
135
     *
136
     * @param string $a The dividend.
137
     * @param string $b The divisor, must not be zero.
138
     *
139
     * @return string The remainder.
140
     *
141
     * @pure
142
     */
143
    abstract public function divR(string $a, string $b): string;
144

145
    /**
146
     * Returns the quotient and remainder of the division of two numbers.
147
     *
148
     * @param string $a The dividend.
149
     * @param string $b The divisor, must not be zero.
150
     *
151
     * @return array{string, string} An array containing the quotient and remainder.
152
     *
153
     * @pure
154
     */
155
    abstract public function divQR(string $a, string $b): array;
156

157
    /**
158
     * Exponentiates a number.
159
     *
160
     * @param string $a The base number.
161
     * @param int    $e The exponent, validated as a non-negative integer.
162
     *
163
     * @return string The power.
164
     *
165
     * @pure
166
     */
167
    abstract public function pow(string $a, int $e): string;
168

169
    /**
170
     * @param string $b The modulus; must not be zero.
171
     *
172
     * @pure
173
     */
174
    public function mod(string $a, string $b): string
175
    {
176
        return $this->divR($this->add($this->divR($a, $b), $b), $b);
256✔
177
    }
178

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

196
        $modVal = $x;
42✔
197

198
        if ($x[0] === '-' || ($this->cmp($this->abs($x), $m) >= 0)) {
42✔
199
            $modVal = $this->mod($x, $m);
16✔
200
        }
201

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

204
        if ($g !== '1') {
42✔
205
            return null;
10✔
206
        }
207

208
        return $this->mod($this->add($this->mod($x, $m), $m), $m);
32✔
209
    }
210

211
    /**
212
     * Raises a number into power with modulo.
213
     *
214
     * @param string $base The base number.
215
     * @param string $exp  The exponent; must be positive or zero.
216
     * @param string $mod  The modulus; must be strictly positive.
217
     *
218
     * @pure
219
     */
220
    abstract public function modPow(string $base, string $exp, string $mod): string;
221

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

238
        if ($b === '0') {
3,852✔
239
            return $this->abs($a);
3,852✔
240
        }
241

242
        return $this->gcd($b, $this->divR($a, $b));
3,848✔
243
    }
244

245
    /**
246
     * Returns the least common multiple of the two numbers.
247
     *
248
     * This method can be overridden by the concrete implementation if the underlying library
249
     * has built-in support for LCM calculations.
250
     *
251
     * @return string The LCM, always positive, or zero if at least one argument is zero.
252
     *
253
     * @pure
254
     */
255
    public function lcm(string $a, string $b): string
256
    {
257
        if ($a === '0' || $b === '0') {
308✔
UNCOV
258
            return '0';
×
259
        }
260

261
        return $this->divQ($this->abs($this->mul($a, $b)), $this->gcd($a, $b));
308✔
262
    }
263

264
    /**
265
     * Returns the square root of the given number, rounded down.
266
     *
267
     * The result is the largest x such that x² ≤ n.
268
     * The input MUST NOT be negative.
269
     *
270
     * @pure
271
     */
272
    abstract public function sqrt(string $n): string;
273

274
    /**
275
     * Converts a number from an arbitrary base.
276
     *
277
     * This method can be overridden by the concrete implementation if the underlying library
278
     * has built-in support for base conversion.
279
     *
280
     * @param string $number The number, positive or zero, non-empty, case-insensitively validated for the given base.
281
     * @param int    $base   The base of the number, validated from 2 to 36.
282
     *
283
     * @return string The converted number, following the Calculator conventions.
284
     *
285
     * @pure
286
     */
287
    public function fromBase(string $number, int $base): string
288
    {
289
        return $this->fromArbitraryBase(strtolower($number), self::ALPHABET, $base);
1,187✔
290
    }
291

292
    /**
293
     * Converts a number to an arbitrary base.
294
     *
295
     * This method can be overridden by the concrete implementation if the underlying library
296
     * has built-in support for base conversion.
297
     *
298
     * @param string $number The number to convert, following the Calculator conventions.
299
     * @param int    $base   The base to convert to, validated from 2 to 36.
300
     *
301
     * @return string The converted number, lowercase.
302
     *
303
     * @pure
304
     */
305
    public function toBase(string $number, int $base): string
306
    {
307
        $negative = ($number[0] === '-');
848✔
308

309
        if ($negative) {
848✔
310
            $number = substr($number, 1);
140✔
311
        }
312

313
        $number = $this->toArbitraryBase($number, self::ALPHABET, $base);
848✔
314

315
        if ($negative) {
848✔
316
            return '-' . $number;
140✔
317
        }
318

319
        return $number;
708✔
320
    }
321

322
    /**
323
     * Converts a non-negative number in an arbitrary base using a custom alphabet, to base 10.
324
     *
325
     * @param string $number   The number to convert, validated as a non-empty string,
326
     *                         containing only chars in the given alphabet/base.
327
     * @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum.
328
     * @param int    $base     The base of the number, validated from 2 to alphabet length.
329
     *
330
     * @return string The number in base 10, following the Calculator conventions.
331
     *
332
     * @pure
333
     */
334
    final public function fromArbitraryBase(string $number, string $alphabet, int $base): string
335
    {
336
        // remove leading "zeros"
337
        $number = ltrim($number, $alphabet[0]);
1,565✔
338

339
        if ($number === '') {
1,565✔
340
            return '0';
9✔
341
        }
342

343
        // optimize for "one"
344
        if ($number === $alphabet[1]) {
1,556✔
345
            return '1';
9✔
346
        }
347

348
        $result = '0';
1,547✔
349
        $power = '1';
1,547✔
350

351
        $base = (string) $base;
1,547✔
352

353
        for ($i = strlen($number) - 1; $i >= 0; $i--) {
1,547✔
354
            $index = strpos($alphabet, $number[$i]);
1,547✔
355

356
            if ($index !== 0) {
1,547✔
357
                $result = $this->add(
1,547✔
358
                    $result,
1,547✔
359
                    ($index === 1) ? $power : $this->mul($power, (string) $index),
1,547✔
360
                );
1,547✔
361
            }
362

363
            if ($i !== 0) {
1,547✔
364
                $power = $this->mul($power, $base);
1,529✔
365
            }
366
        }
367

368
        return $result;
1,547✔
369
    }
370

371
    /**
372
     * Converts a non-negative number to an arbitrary base using a custom alphabet.
373
     *
374
     * @param string $number   The number to convert, positive or zero, following the Calculator conventions.
375
     * @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum.
376
     * @param int    $base     The base to convert to, validated from 2 to alphabet length.
377
     *
378
     * @return string The converted number in the given alphabet.
379
     *
380
     * @pure
381
     */
382
    final public function toArbitraryBase(string $number, string $alphabet, int $base): string
383
    {
384
        if ($number === '0') {
974✔
385
            return $alphabet[0];
11✔
386
        }
387

388
        $base = (string) $base;
963✔
389
        $result = '';
963✔
390

391
        while ($number !== '0') {
963✔
392
            [$number, $remainder] = $this->divQR($number, $base);
963✔
393
            $remainder = (int) $remainder;
963✔
394

395
            $result .= $alphabet[$remainder];
963✔
396
        }
397

398
        return strrev($result);
963✔
399
    }
400

401
    /**
402
     * Performs a rounded division.
403
     *
404
     * When the remainder of the division is not zero, rounding is performed according to the rounding mode provided,
405
     * unless RoundingMode::Unnecessary is used, in which case the method returns null.
406
     *
407
     * @param string       $a            The dividend.
408
     * @param string       $b            The divisor, must not be zero.
409
     * @param RoundingMode $roundingMode The rounding mode.
410
     *
411
     * @pure
412
     */
413
    final public function divRound(string $a, string $b, RoundingMode $roundingMode): ?string
414
    {
415
        [$quotient, $remainder] = $this->divQR($a, $b);
7,164✔
416

417
        $hasDiscardedFraction = ($remainder !== '0');
7,164✔
418
        $isPositiveOrZero = ($a[0] === '-') === ($b[0] === '-');
7,164✔
419

420
        $discardedFractionSign = function () use ($remainder, $b): int {
7,164✔
421
            $r = $this->abs($this->mul($remainder, '2'));
3,309✔
422
            $b = $this->abs($b);
3,309✔
423

424
            return $this->cmp($r, $b);
3,309✔
425
        };
7,164✔
426

427
        $increment = false;
7,164✔
428

429
        switch ($roundingMode) {
430
            case RoundingMode::Unnecessary:
7,164✔
431
                if ($hasDiscardedFraction) {
624✔
432
                    return null;
246✔
433
                }
434

435
                break;
402✔
436

437
            case RoundingMode::Up:
6,540✔
438
                $increment = $hasDiscardedFraction;
1,263✔
439

440
                break;
1,263✔
441

442
            case RoundingMode::Down:
5,277✔
443
                break;
663✔
444

445
            case RoundingMode::Ceiling:
4,614✔
446
                $increment = $hasDiscardedFraction && $isPositiveOrZero;
660✔
447

448
                break;
660✔
449

450
            case RoundingMode::Floor:
3,954✔
451
                $increment = $hasDiscardedFraction && ! $isPositiveOrZero;
645✔
452

453
                break;
645✔
454

455
            case RoundingMode::HalfUp:
3,309✔
456
                $increment = $discardedFractionSign() >= 0;
1,647✔
457

458
                break;
1,647✔
459

460
            case RoundingMode::HalfDown:
1,662✔
461
                $increment = $discardedFractionSign() > 0;
315✔
462

463
                break;
315✔
464

465
            case RoundingMode::HalfCeiling:
1,347✔
466
                $increment = $isPositiveOrZero ? $discardedFractionSign() >= 0 : $discardedFractionSign() > 0;
645✔
467

468
                break;
645✔
469

470
            case RoundingMode::HalfFloor:
702✔
471
                $increment = $isPositiveOrZero ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
312✔
472

473
                break;
312✔
474

475
            case RoundingMode::HalfEven:
390✔
476
                $lastDigit = (int) $quotient[-1];
390✔
477
                $lastDigitIsEven = ($lastDigit % 2 === 0);
390✔
478
                $increment = $lastDigitIsEven ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
390✔
479

480
                break;
390✔
481
        }
482

483
        if ($increment) {
6,942✔
484
            return $this->add($quotient, $isPositiveOrZero ? '1' : '-1');
3,480✔
485
        }
486

487
        return $quotient;
4,158✔
488
    }
489

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

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

516
    /**
517
     * Calculates bitwise XOR of two numbers.
518
     *
519
     * This method can be overridden by the concrete implementation if the underlying library
520
     * has built-in support for bitwise operations.
521
     *
522
     * @pure
523
     */
524
    public function xor(string $a, string $b): string
525
    {
526
        return $this->bitwise('xor', $a, $b);
96✔
527
    }
528

529
    /**
530
     * Extracts the sign & digits of the operands.
531
     *
532
     * @return array{bool, bool, string, string} Whether $a and $b are negative, followed by their digits.
533
     *
534
     * @pure
535
     */
536
    final protected function init(string $a, string $b): array
537
    {
538
        return [
14,795✔
539
            $aNeg = ($a[0] === '-'),
14,795✔
540
            $bNeg = ($b[0] === '-'),
14,795✔
541

542
            $aNeg ? substr($a, 1) : $a,
14,795✔
543
            $bNeg ? substr($b, 1) : $b,
14,795✔
544
        ];
14,795✔
545
    }
546

547
    /**
548
     * @return array{string, string, string} GCD, X, Y
549
     *
550
     * @pure
551
     */
552
    private function gcdExtended(string $a, string $b): array
553
    {
554
        if ($a === '0') {
42✔
555
            return [$b, '0', '1'];
42✔
556
        }
557

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

560
        $x = $this->sub($y1, $this->mul($this->divQ($b, $a), $x1));
38✔
561
        $y = $x1;
38✔
562

563
        return [$gcd, $x, $y];
38✔
564
    }
565

566
    /**
567
     * Performs a bitwise operation on a decimal number.
568
     *
569
     * @param 'and'|'or'|'xor' $operator The operator to use.
570
     * @param string           $a        The left operand.
571
     * @param string           $b        The right operand.
572
     *
573
     * @pure
574
     */
575
    private function bitwise(string $operator, string $a, string $b): string
576
    {
577
        [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
290✔
578

579
        $aBin = $this->toBinary($aDig);
290✔
580
        $bBin = $this->toBinary($bDig);
290✔
581

582
        $aLen = strlen($aBin);
290✔
583
        $bLen = strlen($bBin);
290✔
584

585
        if ($aLen > $bLen) {
290✔
586
            $bBin = str_repeat("\x00", $aLen - $bLen) . $bBin;
102✔
587
        } elseif ($bLen > $aLen) {
188✔
588
            $aBin = str_repeat("\x00", $bLen - $aLen) . $aBin;
132✔
589
        }
590

591
        if ($aNeg) {
290✔
592
            $aBin = $this->twosComplement($aBin);
146✔
593
        }
594
        if ($bNeg) {
290✔
595
            $bBin = $this->twosComplement($bBin);
132✔
596
        }
597

598
        $value = match ($operator) {
290✔
599
            'and' => $aBin & $bBin,
102✔
600
            'or' => $aBin | $bBin,
92✔
601
            'xor' => $aBin ^ $bBin,
96✔
602
        };
290✔
603

604
        $negative = match ($operator) {
290✔
605
            'and' => $aNeg and $bNeg,
102✔
606
            'or' => $aNeg or $bNeg,
92✔
607
            'xor' => $aNeg xor $bNeg,
96✔
608
        };
290✔
609

610
        if ($negative) {
290✔
611
            $value = $this->twosComplement($value);
136✔
612
        }
613

614
        $result = $this->toDecimal($value);
290✔
615

616
        return $negative ? $this->neg($result) : $result;
290✔
617
    }
618

619
    /**
620
     * @param string $number A positive, binary number.
621
     *
622
     * @pure
623
     */
624
    private function twosComplement(string $number): string
625
    {
626
        $xor = str_repeat("\xff", strlen($number));
212✔
627

628
        $number ^= $xor;
212✔
629

630
        for ($i = strlen($number) - 1; $i >= 0; $i--) {
212✔
631
            $byte = ord($number[$i]);
212✔
632

633
            if (++$byte !== 256) {
212✔
634
                $number[$i] = chr($byte);
212✔
635

636
                break;
212✔
637
            }
638

639
            $number[$i] = "\x00";
8✔
640

641
            if ($i === 0) {
8✔
642
                $number = "\x01" . $number;
4✔
643
            }
644
        }
645

646
        return $number;
212✔
647
    }
648

649
    /**
650
     * Converts a decimal number to a binary string.
651
     *
652
     * @param string $number The number to convert, positive or zero, only digits.
653
     *
654
     * @pure
655
     */
656
    private function toBinary(string $number): string
657
    {
658
        $result = '';
290✔
659

660
        while ($number !== '0') {
290✔
661
            [$number, $remainder] = $this->divQR($number, '256');
290✔
662
            $result .= chr((int) $remainder);
290✔
663
        }
664

665
        return strrev($result);
290✔
666
    }
667

668
    /**
669
     * Returns the positive decimal representation of a binary number.
670
     *
671
     * @param string $bytes The bytes representing the number.
672
     *
673
     * @pure
674
     */
675
    private function toDecimal(string $bytes): string
676
    {
677
        $result = '0';
290✔
678
        $power = '1';
290✔
679

680
        for ($i = strlen($bytes) - 1; $i >= 0; $i--) {
290✔
681
            $index = ord($bytes[$i]);
290✔
682

683
            if ($index !== 0) {
290✔
684
                $result = $this->add(
286✔
685
                    $result,
286✔
686
                    ($index === 1) ? $power : $this->mul($power, (string) $index),
286✔
687
                );
286✔
688
            }
689

690
            if ($i !== 0) {
290✔
691
                $power = $this->mul($power, '256');
264✔
692
            }
693
        }
694

695
        return $result;
290✔
696
    }
697
}
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