• 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

100.0
/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;
2,727✔
62

63
        if (is_int($result)) {
2,727✔
UNCOV
64
            return (string) $result;
2,131✔
65
        }
66

67
        if ($a === '0') {
1,534✔
68
            return $b;
500✔
69
        }
70

71
        if ($b === '0') {
1,528✔
72
            return $a;
3✔
73
        }
74

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

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

79
        if ($aNeg) {
1,525✔
80
            $result = $this->neg($result);
121✔
81
        }
82

83
        return $result;
1,525✔
84
    }
85

86
    #[Override]
87
    public function sub(string $a, string $b): string
88
    {
UNCOV
89
        return $this->add($a, $this->neg($b));
828✔
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;
2,221✔
100

101
        if (is_int($result)) {
2,221✔
102
            return (string) $result;
2,105✔
103
        }
104

105
        if ($a === '0' || $b === '0') {
939✔
106
            return '0';
11✔
107
        }
108

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

113
        if ($b === '1') {
927✔
114
            return $a;
3✔
115
        }
116

117
        if ($a === '-1') {
924✔
UNCOV
118
            return $this->neg($b);
6✔
119
        }
120

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

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

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

129
        if ($aNeg !== $bNeg) {
917✔
130
            $result = $this->neg($result);
110✔
131
        }
132

133
        return $result;
917✔
134
    }
135

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

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

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

UNCOV
155
        if ($a === $b) {
3,932✔
UNCOV
156
            return ['1', '0'];
254✔
157
        }
158

UNCOV
159
        if ($b === '1') {
3,904✔
160
            return [$a, '0'];
157✔
161
        }
162

UNCOV
163
        if ($b === '-1') {
3,881✔
UNCOV
164
            return [$this->neg($a), '0'];
130✔
165
        }
166

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

170
        if (is_int($na)) {
3,861✔
171
            /** @var numeric-string $b */
UNCOV
172
            $nb = $b * 1;
3,022✔
173

174
            if (is_int($nb)) {
3,022✔
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.
UNCOV
177
                $q = intdiv($na, $nb);
2,938✔
178
                $r = $na % $nb;
2,938✔
179

UNCOV
180
                return [
2,938✔
UNCOV
181
                    (string) $q,
2,938✔
NEW
182
                    (string) $r,
2,938✔
183
                ];
2,938✔
184
            }
185
        }
186

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

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

UNCOV
191
        if ($aNeg !== $bNeg) {
2,197✔
192
            $q = $this->neg($q);
680✔
193
        }
194

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

UNCOV
199
        return [$q, $r];
2,197✔
200
    }
201

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

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

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

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

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

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

224
        return $result;
568✔
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
        // special case: the algorithm below fails with 0 power 0 mod 1 (returns 1 instead of 0)
UNCOV
234
        if ($base === '0' && $exp === '0' && $mod === '1') {
11✔
UNCOV
235
            return '0';
1✔
236
        }
237

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

UNCOV
243
        $x = $base;
9✔
244

UNCOV
245
        $res = '1';
9✔
246

247
        // numbers are positive, so we can use remainder instead of modulo
UNCOV
248
        $x = $this->divR($x, $mod);
9✔
249

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

255
            $exp = $this->divQ($exp, '2');
9✔
UNCOV
256
            $x = $this->divR($this->mul($x, $x), $mod);
9✔
257
        }
258

UNCOV
259
        return $res;
9✔
260
    }
261

262
    /**
263
     * Adapted from https://cp-algorithms.com/num_methods/roots_newton.html.
264
     */
265
    #[Override]
266
    public function sqrt(string $n): string
267
    {
UNCOV
268
        if ($n === '0') {
486✔
UNCOV
269
            return '0';
1✔
270
        }
271

272
        // initial approximation
NEW
273
        $x = str_repeat('9', intdiv(strlen($n), 2) ?: 1);
485✔
274

UNCOV
275
        $decreased = false;
485✔
276

277
        for (; ;) {
UNCOV
278
            $nx = $this->divQ($this->add($x, $this->divQ($n, $x)), '2');
485✔
279

UNCOV
280
            if ($x === $nx || $this->cmp($nx, $x) > 0 && $decreased) {
485✔
UNCOV
281
                break;
485✔
282
            }
283

284
            $decreased = $this->cmp($nx, $x) < 0;
481✔
UNCOV
285
            $x = $nx;
485✔
286
        }
287

288
        return $x;
485✔
289
    }
290

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

300
        $carry = 0;
1,382✔
301
        $result = '';
1,382✔
302

303
        for ($i = $length - $this->maxDigits; ; $i -= $this->maxDigits) {
1,382✔
304
            $blockLength = $this->maxDigits;
1,382✔
305

306
            if ($i < 0) {
1,382✔
307
                $blockLength += $i;
1,364✔
308
                $i = 0;
1,364✔
309
            }
310

311
            /** @var numeric-string $blockA */
312
            $blockA = substr($a, $i, $blockLength);
1,382✔
313

314
            /** @var numeric-string $blockB */
315
            $blockB = substr($b, $i, $blockLength);
1,382✔
316

317
            $sum = (string) ($blockA + $blockB + $carry);
1,382✔
318
            $sumLength = strlen($sum);
1,382✔
319

320
            if ($sumLength > $blockLength) {
1,382✔
321
                $sum = substr($sum, 1);
1,078✔
322
                $carry = 1;
1,078✔
323
            } else {
324
                if ($sumLength < $blockLength) {
1,382✔
NEW
325
                    $sum = str_repeat('0', $blockLength - $sumLength) . $sum;
987✔
326
                }
327
                $carry = 0;
1,382✔
328
            }
329

330
            $result = $sum . $result;
1,382✔
331

332
            if ($i === 0) {
1,382✔
333
                break;
1,382✔
334
            }
335
        }
336

337
        if ($carry === 1) {
1,382✔
UNCOV
338
            $result = '1' . $result;
567✔
339
        }
340

341
        return $result;
1,382✔
342
    }
343

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

355
        // Ensure that we always subtract to a positive result: biggest minus smallest.
UNCOV
356
        $cmp = $this->doCmp($a, $b);
576✔
357

UNCOV
358
        $invert = ($cmp === -1);
576✔
359

UNCOV
360
        if ($invert) {
576✔
UNCOV
361
            $c = $a;
40✔
UNCOV
362
            $a = $b;
40✔
UNCOV
363
            $b = $c;
40✔
364
        }
365

UNCOV
366
        [$a, $b, $length] = $this->pad($a, $b);
576✔
367

UNCOV
368
        $carry = 0;
576✔
UNCOV
369
        $result = '';
576✔
370

371
        $complement = 10 ** $this->maxDigits;
576✔
372

NEW
373
        for ($i = $length - $this->maxDigits; ; $i -= $this->maxDigits) {
576✔
UNCOV
374
            $blockLength = $this->maxDigits;
576✔
375

376
            if ($i < 0) {
576✔
377
                $blockLength += $i;
575✔
378
                $i = 0;
575✔
379
            }
380

381
            /** @var numeric-string $blockA */
NEW
382
            $blockA = substr($a, $i, $blockLength);
576✔
383

384
            /** @var numeric-string $blockB */
NEW
385
            $blockB = substr($b, $i, $blockLength);
576✔
386

UNCOV
387
            $sum = $blockA - $blockB - $carry;
576✔
388

389
            if ($sum < 0) {
576✔
UNCOV
390
                $sum += $complement;
509✔
391
                $carry = 1;
509✔
392
            } else {
393
                $carry = 0;
576✔
394
            }
395

UNCOV
396
            $sum = (string) $sum;
576✔
NEW
397
            $sumLength = strlen($sum);
576✔
398

UNCOV
399
            if ($sumLength < $blockLength) {
576✔
NEW
400
                $sum = str_repeat('0', $blockLength - $sumLength) . $sum;
539✔
401
            }
402

UNCOV
403
            $result = $sum . $result;
576✔
404

405
            if ($i === 0) {
576✔
406
                break;
576✔
407
            }
408
        }
409

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

NEW
413
        $result = ltrim($result, '0');
576✔
414

415
        if ($invert) {
576✔
UNCOV
416
            $result = $this->neg($result);
40✔
417
        }
418

UNCOV
419
        return $result;
576✔
420
    }
421

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

432
        $maxDigits = intdiv($this->maxDigits, 2);
917✔
433
        $complement = 10 ** $maxDigits;
917✔
434

435
        $result = '0';
917✔
436

437
        for ($i = $x - $maxDigits; ; $i -= $maxDigits) {
917✔
438
            $blockALength = $maxDigits;
917✔
439

440
            if ($i < 0) {
917✔
441
                $blockALength += $i;
910✔
442
                $i = 0;
910✔
443
            }
444

445
            $blockA = (int) substr($a, $i, $blockALength);
917✔
446

447
            $line = '';
917✔
448
            $carry = 0;
917✔
449

450
            for ($j = $y - $maxDigits; ; $j -= $maxDigits) {
917✔
451
                $blockBLength = $maxDigits;
917✔
452

453
                if ($j < 0) {
917✔
454
                    $blockBLength += $j;
903✔
455
                    $j = 0;
903✔
456
                }
457

458
                $blockB = (int) substr($b, $j, $blockBLength);
917✔
459

460
                $mul = $blockA * $blockB + $carry;
917✔
461
                $value = $mul % $complement;
917✔
462
                $carry = ($mul - $value) / $complement;
917✔
463

464
                $value = (string) $value;
917✔
465
                $value = str_pad($value, $maxDigits, '0', STR_PAD_LEFT);
917✔
466

467
                $line = $value . $line;
917✔
468

469
                if ($j === 0) {
917✔
470
                    break;
917✔
471
                }
472
            }
473

474
            if ($carry !== 0) {
917✔
475
                $line = $carry . $line;
912✔
476
            }
477

478
            $line = ltrim($line, '0');
917✔
479

480
            if ($line !== '') {
917✔
481
                $line .= str_repeat('0', $x - $blockALength - $i);
917✔
482
                $result = $this->add($result, $line);
917✔
483
            }
484

485
            if ($i === 0) {
917✔
486
                break;
917✔
487
            }
488
        }
489

490
        return $result;
917✔
491
    }
492

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

UNCOV
504
        if ($cmp === -1) {
2,197✔
UNCOV
505
            return ['0', $a];
529✔
506
        }
507

NEW
508
        $x = strlen($a);
2,157✔
NEW
509
        $y = strlen($b);
2,157✔
510

511
        // we now know that a >= b && x >= y
512

UNCOV
513
        $q = '0'; // quotient
2,157✔
UNCOV
514
        $r = $a; // remainder
2,157✔
UNCOV
515
        $z = $y; // focus length, always $y or $y+1
2,157✔
516

517
        /** @var numeric-string $b */
UNCOV
518
        $nb = $b * 1; // cast to number
2,157✔
519
        // performance optimization in cases where the remainder will never cause int overflow
520
        if (is_int(($nb - 1) * 10 + 9)) {
2,157✔
NEW
521
            $r = (int) substr($a, 0, $z - 1);
2,066✔
522

523
            for ($i = $z - 1; $i < $x; $i++) {
2,066✔
524
                $n = $r * 10 + (int) $a[$i];
2,066✔
525
                /** @var int $nb */
NEW
526
                $q .= intdiv($n, $nb);
2,066✔
UNCOV
527
                $r = $n % $nb;
2,066✔
528
            }
529

NEW
530
            return [ltrim($q, '0') ?: '0', (string) $r];
2,066✔
531
        }
532

533
        for (; ;) {
NEW
534
            $focus = substr($a, 0, $z);
482✔
535

536
            $cmp = $this->doCmp($focus, $b);
482✔
537

538
            if ($cmp === -1) {
482✔
539
                if ($z === $x) { // remainder < dividend
470✔
UNCOV
540
                    break;
432✔
541
                }
542

UNCOV
543
                $z++;
466✔
544
            }
545

NEW
546
            $zeros = str_repeat('0', $x - $z);
482✔
547

UNCOV
548
            $q = $this->add($q, '1' . $zeros);
482✔
549
            $a = $this->sub($a, $b . $zeros);
482✔
550

551
            $r = $a;
482✔
552

553
            if ($r === '0') { // remainder == 0
482✔
554
                break;
338✔
555
            }
556

NEW
557
            $x = strlen($a);
479✔
558

UNCOV
559
            if ($x < $y) { // remainder < dividend
479✔
UNCOV
560
                break;
395✔
561
            }
562

563
            $z = $y;
482✔
564
        }
565

566
        return [$q, $r];
482✔
567
    }
568

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

581
        $cmp = $x <=> $y;
2,276✔
582

UNCOV
583
        if ($cmp !== 0) {
2,276✔
UNCOV
584
            return $cmp;
2,219✔
585
        }
586

NEW
587
        return strcmp($a, $b) <=> 0; // enforce -1|0|1
536✔
588
    }
589

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

604
        if ($x > $y) {
1,514✔
NEW
605
            $b = str_repeat('0', $x - $y) . $b;
958✔
606

UNCOV
607
            return [$a, $b, $x];
958✔
608
        }
609

610
        if ($x < $y) {
1,343✔
611
            $a = str_repeat('0', $y - $x) . $a;
1,023✔
612

613
            return [$a, $b, $y];
1,023✔
614
        }
615

616
        return [$a, $b, $x];
972✔
617
    }
618
}
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