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

brick / math / 21711699273

05 Feb 2026 12:35PM UTC coverage: 99.361% (+0.08%) from 99.282%
21711699273

push

github

BenMorel
Remove deprecated BigRational::simplified()

1243 of 1251 relevant lines covered (99.36%)

2537.1 hits per line

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

98.72
/src/Internal/Calculator/NativeCalculator.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Brick\Math\Internal\Calculator;
6

7
use Brick\Math\Internal\Calculator;
8
use Override;
9

10
use function assert;
11
use function in_array;
12
use function intdiv;
13
use function is_int;
14
use function ltrim;
15
use function str_pad;
16
use function str_repeat;
17
use function strcmp;
18
use function strlen;
19
use function substr;
20

21
use const PHP_INT_SIZE;
22
use const STR_PAD_LEFT;
23

24
/**
25
 * Calculator implementation using only native PHP code.
26
 *
27
 * @internal
28
 */
29
final readonly class NativeCalculator extends Calculator
30
{
31
    /**
32
     * The max number of digits the platform can natively add, subtract, multiply or divide without overflow.
33
     * For multiplication, this represents the max sum of the lengths of both operands.
34
     *
35
     * In addition, it is assumed that an extra digit can hold a carry (1) without overflowing.
36
     * Example: 32-bit: max number 1,999,999,999 (9 digits + carry)
37
     *          64-bit: max number 1,999,999,999,999,999,999 (18 digits + carry)
38
     */
39
    private int $maxDigits;
40

41
    /**
42
     * @pure
43
     *
44
     * @codeCoverageIgnore
45
     */
46
    public function __construct()
47
    {
48
        $this->maxDigits = match (PHP_INT_SIZE) {
49
            4 => 9,
50
            8 => 18,
51
        };
52
    }
53

54
    #[Override]
55
    public function add(string $a, string $b): string
56
    {
57
        /**
58
         * @var numeric-string $a
59
         * @var numeric-string $b
60
         */
61
        $result = $a + $b;
7,887✔
62

63
        if (is_int($result)) {
7,887✔
64
            return (string) $result;
4,363✔
65
        }
66

67
        if ($a === '0') {
5,189✔
68
            return $b;
4,137✔
69
        }
70

71
        if ($b === '0') {
5,146✔
72
            return $a;
×
73
        }
74

75
        [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
5,146✔
76

77
        $result = $aNeg === $bNeg ? $this->doAdd($aDig, $bDig) : $this->doSub($aDig, $bDig);
5,146✔
78

79
        if ($aNeg) {
5,146✔
80
            $result = $this->neg($result);
127✔
81
        }
82

83
        return $result;
5,146✔
84
    }
85

86
    #[Override]
87
    public function sub(string $a, string $b): string
88
    {
89
        return $this->add($a, $this->neg($b));
4,743✔
90
    }
91

92
    #[Override]
93
    public function mul(string $a, string $b): string
94
    {
95
        /**
96
         * @var numeric-string $a
97
         * @var numeric-string $b
98
         */
99
        $result = $a * $b;
7,290✔
100

101
        if (is_int($result)) {
7,290✔
102
            return (string) $result;
4,229✔
103
        }
104

105
        if ($a === '0' || $b === '0') {
4,298✔
106
            return '0';
7✔
107
        }
108

109
        if ($a === '1') {
4,293✔
110
            return $b;
6✔
111
        }
112

113
        if ($b === '1') {
4,293✔
114
            return $a;
×
115
        }
116

117
        if ($a === '-1') {
4,293✔
118
            return $this->neg($b);
12✔
119
        }
120

121
        if ($b === '-1') {
4,281✔
122
            return $this->neg($a);
1✔
123
        }
124

125
        [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
4,280✔
126

127
        $result = $this->doMul($aDig, $bDig);
4,280✔
128

129
        if ($aNeg !== $bNeg) {
4,280✔
130
            $result = $this->neg($result);
117✔
131
        }
132

133
        return $result;
4,280✔
134
    }
135

136
    #[Override]
137
    public function divQ(string $a, string $b): string
138
    {
139
        return $this->divQR($a, $b)[0];
6,527✔
140
    }
141

142
    #[Override]
143
    public function divR(string $a, string $b): string
144
    {
145
        return $this->divQR($a, $b)[1];
1,989✔
146
    }
147

148
    #[Override]
149
    public function divQR(string $a, string $b): array
150
    {
151
        if ($a === '0') {
9,665✔
152
            return ['0', '0'];
123✔
153
        }
154

155
        if ($a === $b) {
9,628✔
156
            return ['1', '0'];
519✔
157
        }
158

159
        if ($b === '1') {
9,566✔
160
            return [$a, '0'];
631✔
161
        }
162

163
        if ($b === '-1') {
9,520✔
164
            return [$this->neg($a), '0'];
286✔
165
        }
166

167
        /** @var numeric-string $a */
168
        $na = $a * 1; // cast to number
9,493✔
169

170
        if (is_int($na)) {
9,493✔
171
            /** @var numeric-string $b */
172
            $nb = $b * 1;
5,077✔
173

174
            if (is_int($nb)) {
5,077✔
175
                // the only division that may overflow is PHP_INT_MIN / -1,
176
                // which cannot happen here as we've already handled a divisor of -1 above.
177
                $q = intdiv($na, $nb);
4,940✔
178
                $r = $na % $nb;
4,940✔
179

180
                return [
4,940✔
181
                    (string) $q,
4,940✔
182
                    (string) $r,
4,940✔
183
                ];
4,940✔
184
            }
185
        }
186

187
        [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
5,962✔
188

189
        [$q, $r] = $this->doDiv($aDig, $bDig);
5,962✔
190

191
        if ($aNeg !== $bNeg) {
5,962✔
192
            $q = $this->neg($q);
764✔
193
        }
194

195
        if ($aNeg) {
5,962✔
196
            $r = $this->neg($r);
702✔
197
        }
198

199
        return [$q, $r];
5,962✔
200
    }
201

202
    #[Override]
203
    public function pow(string $a, int $e): string
204
    {
205
        if ($e === 0) {
560✔
206
            return '1';
×
207
        }
208

209
        if ($e === 1) {
560✔
210
            return $a;
560✔
211
        }
212

213
        $odd = $e % 2;
560✔
214
        $e -= $odd;
560✔
215

216
        $aa = $this->mul($a, $a);
560✔
217

218
        $result = $this->pow($aa, $e / 2);
560✔
219

220
        if ($odd === 1) {
560✔
221
            $result = $this->mul($result, $a);
449✔
222
        }
223

224
        return $result;
560✔
225
    }
226

227
    /**
228
     * Algorithm from: https://www.geeksforgeeks.org/modular-exponentiation-power-in-modular-arithmetic/.
229
     */
230
    #[Override]
231
    public function modPow(string $base, string $exp, string $mod): string
232
    {
233
        // normalize to Euclidean representative so modPow() stays consistent with mod()
234
        $base = $this->mod($base, $mod);
58✔
235

236
        // special case: the algorithm below fails with power 0 mod 1 (returns 1 instead of 0)
237
        if ($exp === '0' && $mod === '1') {
58✔
238
            return '0';
3✔
239
        }
240

241
        $x = $base;
55✔
242

243
        $res = '1';
55✔
244

245
        // numbers are positive, so we can use remainder instead of modulo
246
        $x = $this->divR($x, $mod);
55✔
247

248
        while ($exp !== '0') {
55✔
249
            if (in_array($exp[-1], ['1', '3', '5', '7', '9'])) { // odd
53✔
250
                $res = $this->divR($this->mul($res, $x), $mod);
53✔
251
            }
252

253
            $exp = $this->divQ($exp, '2');
53✔
254
            $x = $this->divR($this->mul($x, $x), $mod);
53✔
255
        }
256

257
        return $res;
55✔
258
    }
259

260
    /**
261
     * Adapted from https://cp-algorithms.com/num_methods/roots_newton.html.
262
     */
263
    #[Override]
264
    public function sqrt(string $n): string
265
    {
266
        if ($n === '0') {
5,548✔
267
            return '0';
10✔
268
        }
269

270
        // initial approximation
271
        $x = str_repeat('9', intdiv(strlen($n), 2) ?: 1);
5,538✔
272

273
        $decreased = false;
5,538✔
274

275
        for (; ;) {
276
            $nx = $this->divQ($this->add($x, $this->divQ($n, $x)), '2');
5,538✔
277

278
            if ($x === $nx || $this->cmp($nx, $x) > 0 && $decreased) {
5,538✔
279
                break;
5,538✔
280
            }
281

282
            $decreased = $this->cmp($nx, $x) < 0;
5,528✔
283
            $x = $nx;
5,538✔
284
        }
285

286
        return $x;
5,538✔
287
    }
288

289
    /**
290
     * Performs the addition of two non-signed large integers.
291
     *
292
     * @pure
293
     */
294
    private function doAdd(string $a, string $b): string
295
    {
296
        [$a, $b, $length] = $this->pad($a, $b);
4,979✔
297

298
        $carry = 0;
4,979✔
299
        $result = '';
4,979✔
300

301
        for ($i = $length - $this->maxDigits; ; $i -= $this->maxDigits) {
4,979✔
302
            $blockLength = $this->maxDigits;
4,979✔
303

304
            if ($i < 0) {
4,979✔
305
                $blockLength += $i;
4,946✔
306
                $i = 0;
4,946✔
307
            }
308

309
            /** @var numeric-string $blockA */
310
            $blockA = substr($a, $i, $blockLength);
4,979✔
311

312
            /** @var numeric-string $blockB */
313
            $blockB = substr($b, $i, $blockLength);
4,979✔
314

315
            $sum = (string) ($blockA + $blockB + $carry);
4,979✔
316
            $sumLength = strlen($sum);
4,979✔
317

318
            if ($sumLength > $blockLength) {
4,979✔
319
                $sum = substr($sum, 1);
4,611✔
320
                $carry = 1;
4,611✔
321
            } else {
322
                if ($sumLength < $blockLength) {
4,979✔
323
                    $sum = str_repeat('0', $blockLength - $sumLength) . $sum;
4,553✔
324
                }
325
                $carry = 0;
4,979✔
326
            }
327

328
            $result = $sum . $result;
4,979✔
329

330
            if ($i === 0) {
4,979✔
331
                break;
4,979✔
332
            }
333
        }
334

335
        if ($carry === 1) {
4,979✔
336
            $result = '1' . $result;
3,114✔
337
        }
338

339
        return $result;
4,979✔
340
    }
341

342
    /**
343
     * Performs the subtraction of two non-signed large integers.
344
     *
345
     * @pure
346
     */
347
    private function doSub(string $a, string $b): string
348
    {
349
        if ($a === $b) {
4,188✔
350
            return '0';
3,420✔
351
        }
352

353
        // Ensure that we always subtract to a positive result: biggest minus smallest.
354
        $cmp = $this->doCmp($a, $b);
4,173✔
355

356
        $invert = ($cmp === -1);
4,173✔
357

358
        if ($invert) {
4,173✔
359
            $c = $a;
40✔
360
            $a = $b;
40✔
361
            $b = $c;
40✔
362
        }
363

364
        [$a, $b, $length] = $this->pad($a, $b);
4,173✔
365

366
        $carry = 0;
4,173✔
367
        $result = '';
4,173✔
368

369
        $complement = 10 ** $this->maxDigits;
4,173✔
370

371
        for ($i = $length - $this->maxDigits; ; $i -= $this->maxDigits) {
4,173✔
372
            $blockLength = $this->maxDigits;
4,173✔
373

374
            if ($i < 0) {
4,173✔
375
                $blockLength += $i;
4,172✔
376
                $i = 0;
4,172✔
377
            }
378

379
            /** @var numeric-string $blockA */
380
            $blockA = substr($a, $i, $blockLength);
4,173✔
381

382
            /** @var numeric-string $blockB */
383
            $blockB = substr($b, $i, $blockLength);
4,173✔
384

385
            $sum = $blockA - $blockB - $carry;
4,173✔
386

387
            if ($sum < 0) {
4,173✔
388
                $sum += $complement;
4,126✔
389
                $carry = 1;
4,126✔
390
            } else {
391
                $carry = 0;
4,173✔
392
            }
393

394
            $sum = (string) $sum;
4,173✔
395
            $sumLength = strlen($sum);
4,173✔
396

397
            if ($sumLength < $blockLength) {
4,173✔
398
                $sum = str_repeat('0', $blockLength - $sumLength) . $sum;
4,140✔
399
            }
400

401
            $result = $sum . $result;
4,173✔
402

403
            if ($i === 0) {
4,173✔
404
                break;
4,173✔
405
            }
406
        }
407

408
        // Carry cannot be 1 when the loop ends, as a > b
409
        assert($carry === 0);
410

411
        $result = ltrim($result, '0');
4,173✔
412

413
        if ($invert) {
4,173✔
414
            $result = $this->neg($result);
40✔
415
        }
416

417
        return $result;
4,173✔
418
    }
419

420
    /**
421
     * Performs the multiplication of two non-signed large integers.
422
     *
423
     * @pure
424
     */
425
    private function doMul(string $a, string $b): string
426
    {
427
        $x = strlen($a);
4,280✔
428
        $y = strlen($b);
4,280✔
429

430
        $maxDigits = intdiv($this->maxDigits, 2);
4,280✔
431
        $complement = 10 ** $maxDigits;
4,280✔
432

433
        $result = '0';
4,280✔
434

435
        for ($i = $x - $maxDigits; ; $i -= $maxDigits) {
4,280✔
436
            $blockALength = $maxDigits;
4,280✔
437

438
            if ($i < 0) {
4,280✔
439
                $blockALength += $i;
3,994✔
440
                $i = 0;
3,994✔
441
            }
442

443
            $blockA = (int) substr($a, $i, $blockALength);
4,280✔
444

445
            $line = '';
4,280✔
446
            $carry = 0;
4,280✔
447

448
            for ($j = $y - $maxDigits; ; $j -= $maxDigits) {
4,280✔
449
                $blockBLength = $maxDigits;
4,280✔
450

451
                if ($j < 0) {
4,280✔
452
                    $blockBLength += $j;
4,026✔
453
                    $j = 0;
4,026✔
454
                }
455

456
                $blockB = (int) substr($b, $j, $blockBLength);
4,280✔
457

458
                $mul = $blockA * $blockB + $carry;
4,280✔
459
                $value = $mul % $complement;
4,280✔
460
                $carry = ($mul - $value) / $complement;
4,280✔
461

462
                $value = (string) $value;
4,280✔
463
                $value = str_pad($value, $maxDigits, '0', STR_PAD_LEFT);
4,280✔
464

465
                $line = $value . $line;
4,280✔
466

467
                if ($j === 0) {
4,280✔
468
                    break;
4,280✔
469
                }
470
            }
471

472
            if ($carry !== 0) {
4,280✔
473
                $line = $carry . $line;
4,191✔
474
            }
475

476
            $line = ltrim($line, '0');
4,280✔
477

478
            if ($line !== '') {
4,280✔
479
                $line .= str_repeat('0', $x - $blockALength - $i);
4,280✔
480
                $result = $this->add($result, $line);
4,280✔
481
            }
482

483
            if ($i === 0) {
4,280✔
484
                break;
4,280✔
485
            }
486
        }
487

488
        return $result;
4,280✔
489
    }
490

491
    /**
492
     * Performs the division of two non-signed large integers.
493
     *
494
     * @return string[] The quotient and remainder.
495
     *
496
     * @pure
497
     */
498
    private function doDiv(string $a, string $b): array
499
    {
500
        $cmp = $this->doCmp($a, $b);
5,962✔
501

502
        if ($cmp === -1) {
5,962✔
503
            return ['0', $a];
645✔
504
        }
505

506
        $x = strlen($a);
5,909✔
507
        $y = strlen($b);
5,909✔
508

509
        // we now know that a >= b && x >= y
510

511
        $q = '0'; // quotient
5,909✔
512
        $r = $a; // remainder
5,909✔
513
        $z = $y; // focus length, always $y or $y+1
5,909✔
514

515
        /** @var numeric-string $b */
516
        $nb = $b * 1; // cast to number
5,909✔
517
        // performance optimization in cases where the remainder will never cause int overflow
518
        if (is_int(($nb - 1) * 10 + 9)) {
5,909✔
519
            $r = (int) substr($a, 0, $z - 1);
5,748✔
520

521
            for ($i = $z - 1; $i < $x; $i++) {
5,748✔
522
                $n = $r * 10 + (int) $a[$i];
5,748✔
523
                /** @var int $nb */
524
                $q .= intdiv($n, $nb);
5,748✔
525
                $r = $n % $nb;
5,748✔
526
            }
527

528
            return [ltrim($q, '0') ?: '0', (string) $r];
5,748✔
529
        }
530

531
        for (; ;) {
532
            $focus = substr($a, 0, $z);
4,139✔
533

534
            $cmp = $this->doCmp($focus, $b);
4,139✔
535

536
            if ($cmp === -1) {
4,139✔
537
                if ($z === $x) { // remainder < dividend
4,105✔
538
                    break;
3,954✔
539
                }
540

541
                $z++;
4,102✔
542
            }
543

544
            $zeros = str_repeat('0', $x - $z);
4,139✔
545

546
            $q = $this->add($q, '1' . $zeros);
4,139✔
547
            $a = $this->sub($a, $b . $zeros);
4,139✔
548

549
            $r = $a;
4,139✔
550

551
            if ($r === '0') { // remainder == 0
4,139✔
552
                break;
3,406✔
553
            }
554

555
            $x = strlen($a);
4,132✔
556

557
            if ($x < $y) { // remainder < dividend
4,132✔
558
                break;
3,923✔
559
            }
560

561
            $z = $y;
4,139✔
562
        }
563

564
        return [$q, $r];
4,139✔
565
    }
566

567
    /**
568
     * Compares two non-signed large numbers.
569
     *
570
     * @return -1|0|1
571
     *
572
     * @pure
573
     */
574
    private function doCmp(string $a, string $b): int
575
    {
576
        $x = strlen($a);
5,982✔
577
        $y = strlen($b);
5,982✔
578

579
        $cmp = $x <=> $y;
5,982✔
580

581
        if ($cmp !== 0) {
5,982✔
582
            return $cmp;
5,953✔
583
        }
584

585
        return strcmp($a, $b) <=> 0; // enforce -1|0|1
4,163✔
586
    }
587

588
    /**
589
     * Pads the left of one of the given numbers with zeros if necessary to make both numbers the same length.
590
     *
591
     * The numbers must only consist of digits, without leading minus sign.
592
     *
593
     * @return array{string, string, int}
594
     *
595
     * @pure
596
     */
597
    private function pad(string $a, string $b): array
598
    {
599
        $x = strlen($a);
5,135✔
600
        $y = strlen($b);
5,135✔
601

602
        if ($x > $y) {
5,135✔
603
            $b = str_repeat('0', $x - $y) . $b;
4,571✔
604

605
            return [$a, $b, $x];
4,571✔
606
        }
607

608
        if ($x < $y) {
4,953✔
609
            $a = str_repeat('0', $y - $x) . $a;
4,357✔
610

611
            return [$a, $b, $y];
4,357✔
612
        }
613

614
        return [$a, $b, $x];
4,557✔
615
    }
616
}
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