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

brick / math / 17335524386

29 Aug 2025 10:28PM UTC coverage: 99.743%. Remained the same
17335524386

push

github

BenMorel
Apply ECS

243 of 264 new or added lines in 10 files covered. (92.05%)

75 existing lines in 3 files now uncovered.

1165 of 1168 relevant lines covered (99.74%)

1135.14 hits per line

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

99.47
/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;
3,460✔
51
    }
52

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

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

68
        return '-' . $n;
4,040✔
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);
4,799✔
83

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

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

92
        $aLen = strlen($aDig);
4,496✔
93
        $bLen = strlen($bDig);
4,496✔
94

95
        if ($aLen < $bLen) {
4,496✔
96
            $result = -1;
1,262✔
97
        } elseif ($aLen > $bLen) {
3,977✔
98
            $result = 1;
1,247✔
99
        } else {
100
            $result = $aDig <=> $bDig;
2,923✔
101
        }
102

103
        return $aNeg ? -$result : $result;
4,496✔
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);
226✔
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') {
36✔
UNCOV
199
            return '0';
×
200
        }
201

202
        $modVal = $x;
36✔
203

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

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

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

214
        return $this->mod($this->add($this->mod($x, $m), $m), $m);
26✔
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') {
2,072✔
241
            return $this->abs($b);
8✔
242
        }
243

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

248
        return $this->gcd($b, $this->divR($a, $b));
2,056✔
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)
1,547✔
347
                    ? $power
1,057✔
348
                    : $this->mul($power, (string) $index),
1,547✔
349
                );
1,547✔
350
            }
351

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

357
        return $result;
1,547✔
358
    }
359

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

377
        $base = (string) $base;
959✔
378
        $result = '';
959✔
379

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

384
            $result .= $alphabet[$remainder];
959✔
385
        }
386

387
        return strrev($result);
959✔
388
    }
389

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

407
        $hasDiscardedFraction = ($remainder !== '0');
4,062✔
408
        $isPositiveOrZero = ($a[0] === '-') === ($b[0] === '-');
4,062✔
409

410
        $discardedFractionSign = function () use ($remainder, $b): int {
4,062✔
411
            $r = $this->abs($this->mul($remainder, '2'));
1,356✔
412
            $b = $this->abs($b);
1,356✔
413

414
            return $this->cmp($r, $b);
1,356✔
415
        };
4,062✔
416

417
        $increment = false;
4,062✔
418

419
        switch ($roundingMode) {
420
            case RoundingMode::UNNECESSARY:
4,062✔
421
                if ($hasDiscardedFraction) {
1,023✔
422
                    throw RoundingNecessaryException::roundingNecessary();
495✔
423
                }
424

425
                break;
552✔
426

427
            case RoundingMode::UP:
3,039✔
428
                $increment = $hasDiscardedFraction;
864✔
429

430
                break;
864✔
431

432
            case RoundingMode::DOWN:
2,175✔
433
                break;
279✔
434

435
            case RoundingMode::CEILING:
1,896✔
436
                $increment = $hasDiscardedFraction && $isPositiveOrZero;
270✔
437

438
                break;
270✔
439

440
            case RoundingMode::FLOOR:
1,626✔
441
                $increment = $hasDiscardedFraction && ! $isPositiveOrZero;
270✔
442

443
                break;
270✔
444

445
            case RoundingMode::HALF_UP:
1,356✔
446
                $increment = $discardedFractionSign() >= 0;
273✔
447

448
                break;
273✔
449

450
            case RoundingMode::HALF_DOWN:
1,083✔
451
                $increment = $discardedFractionSign() > 0;
273✔
452

453
                break;
273✔
454

455
            case RoundingMode::HALF_CEILING:
810✔
456
                $increment = $isPositiveOrZero ? $discardedFractionSign() >= 0 : $discardedFractionSign() > 0;
270✔
457

458
                break;
270✔
459

460
            case RoundingMode::HALF_FLOOR:
540✔
461
                $increment = $isPositiveOrZero ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
270✔
462

463
                break;
270✔
464

465
            case RoundingMode::HALF_EVEN:
270✔
466
                $lastDigit = (int) $quotient[-1];
270✔
467
                $lastDigitIsEven = ($lastDigit % 2 === 0);
270✔
468
                $increment = $lastDigitIsEven ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
270✔
469

470
                break;
270✔
471
        }
472

473
        if ($increment) {
3,591✔
474
            return $this->add($quotient, $isPositiveOrZero ? '1' : '-1');
1,782✔
475
        }
476

477
        return $quotient;
2,505✔
478
    }
479

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

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

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

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

532
            $aNeg ? substr($a, 1) : $a,
7,004✔
533
            $bNeg ? substr($b, 1) : $b,
7,004✔
534
        ];
7,004✔
535
    }
536

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

548
        [$gcd, $x1, $y1] = $this->gcdExtended($this->mod($b, $a), $a);
32✔
549

550
        $x = $this->sub($y1, $this->mul($this->divQ($b, $a), $x1));
32✔
551
        $y = $x1;
32✔
552

553
        return [$gcd, $x, $y];
32✔
554
    }
555

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

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

572
        $aLen = strlen($aBin);
290✔
573
        $bLen = strlen($bBin);
290✔
574

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

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

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

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

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

604
        $result = $this->toDecimal($value);
290✔
605

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

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

618
        $number ^= $xor;
212✔
619

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

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

626
                break;
212✔
627
            }
628

629
            $number[$i] = "\x00";
8✔
630

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

636
        return $number;
212✔
637
    }
638

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

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

655
        return strrev($result);
290✔
656
    }
657

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

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

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

682
            if ($i !== 0) {
290✔
683
                $power = $this->mul($power, '256');
264✔
684
            }
685
        }
686

687
        return $result;
290✔
688
    }
689
}
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