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

brick / math / 21606966779

02 Feb 2026 09:09PM UTC coverage: 99.307% (+0.001%) from 99.306%
21606966779

push

github

BenMorel
Harmonize division by zero exception messages

So far, dividing a BigRational by zero did throw a DivisionByZeroException, but with a misleading message "The denominator of a rational number cannot be zero."

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

320 existing lines in 2 files now uncovered.

1290 of 1299 relevant lines covered (99.31%)

1102.37 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\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,778✔
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,769✔
61
            return '0';
406✔
62
        }
63

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

68
        return '-' . $n;
3,992✔
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,823✔
83

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

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

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

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

103
        return $aNeg ? -$result : $result;
4,526✔
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);
349✔
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
    {
UNCOV
198
        if ($m === '1') {
42✔
199
            return '0';
×
200
        }
201

UNCOV
202
        $modVal = $x;
42✔
203

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

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

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

UNCOV
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.
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
    {
UNCOV
240
        if ($a === '0') {
2,384✔
UNCOV
241
            return $this->abs($b);
8✔
242
        }
243

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

UNCOV
248
        return $this->gcd($b, $this->divR($a, $b));
2,372✔
249
    }
250

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

UNCOV
267
        return $this->divQ($this->abs($this->mul($a, $b)), $this->gcd($a, $b));
308✔
268
    }
269

270
    /**
271
     * Returns the square root of the given number, rounded down.
272
     *
273
     * The result is the largest x such that x² ≤ n.
274
     * The input MUST NOT be negative.
275
     *
276
     * @pure
277
     */
278
    abstract public function sqrt(string $n): string;
279

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

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

UNCOV
315
        if ($negative) {
844✔
UNCOV
316
            $number = substr($number, 1);
140✔
317
        }
318

UNCOV
319
        $number = $this->toArbitraryBase($number, self::ALPHABET, $base);
844✔
320

UNCOV
321
        if ($negative) {
844✔
UNCOV
322
            return '-' . $number;
140✔
323
        }
324

UNCOV
325
        return $number;
704✔
326
    }
327

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

345
        if ($number === '') {
1,565✔
346
            return '0';
9✔
347
        }
348

349
        // optimize for "one"
350
        if ($number === $alphabet[1]) {
1,556✔
351
            return '1';
9✔
352
        }
353

354
        $result = '0';
1,547✔
355
        $power = '1';
1,547✔
356

357
        $base = (string) $base;
1,547✔
358

359
        for ($i = strlen($number) - 1; $i >= 0; $i--) {
1,547✔
360
            $index = strpos($alphabet, $number[$i]);
1,547✔
361

362
            if ($index !== 0) {
1,547✔
363
                $result = $this->add(
1,547✔
364
                    $result,
1,547✔
365
                    ($index === 1) ? $power : $this->mul($power, (string) $index),
1,547✔
366
                );
1,547✔
367
            }
368

369
            if ($i !== 0) {
1,547✔
370
                $power = $this->mul($power, $base);
1,529✔
371
            }
372
        }
373

374
        return $result;
1,547✔
375
    }
376

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

394
        $base = (string) $base;
959✔
395
        $result = '';
959✔
396

397
        while ($number !== '0') {
959✔
398
            [$number, $remainder] = $this->divQR($number, $base);
959✔
399
            $remainder = (int) $remainder;
959✔
400

401
            $result .= $alphabet[$remainder];
959✔
402
        }
403

404
        return strrev($result);
959✔
405
    }
406

407
    /**
408
     * Performs a rounded division.
409
     *
410
     * Rounding is performed when the remainder of the division is not zero.
411
     *
412
     * @param string       $a            The dividend.
413
     * @param string       $b            The divisor, must not be zero.
414
     * @param RoundingMode $roundingMode The rounding mode.
415
     *
416
     * @throws RoundingNecessaryException If RoundingMode::Unnecessary is provided but rounding is necessary.
417
     *
418
     * @pure
419
     */
420
    final public function divRound(string $a, string $b, RoundingMode $roundingMode): string
421
    {
422
        [$quotient, $remainder] = $this->divQR($a, $b);
4,152✔
423

424
        $hasDiscardedFraction = ($remainder !== '0');
4,152✔
425
        $isPositiveOrZero = ($a[0] === '-') === ($b[0] === '-');
4,152✔
426

427
        $discardedFractionSign = function () use ($remainder, $b): int {
4,152✔
428
            $r = $this->abs($this->mul($remainder, '2'));
1,356✔
429
            $b = $this->abs($b);
1,356✔
430

431
            return $this->cmp($r, $b);
1,356✔
432
        };
4,152✔
433

434
        $increment = false;
4,152✔
435

436
        switch ($roundingMode) {
437
            case RoundingMode::Unnecessary:
4,152✔
438
                if ($hasDiscardedFraction) {
1,113✔
439
                    throw RoundingNecessaryException::roundingNecessary();
507✔
440
                }
441

442
                break;
630✔
443

444
            case RoundingMode::Up:
3,039✔
445
                $increment = $hasDiscardedFraction;
864✔
446

447
                break;
864✔
448

449
            case RoundingMode::Down:
2,175✔
450
                break;
279✔
451

452
            case RoundingMode::Ceiling:
1,896✔
453
                $increment = $hasDiscardedFraction && $isPositiveOrZero;
270✔
454

455
                break;
270✔
456

457
            case RoundingMode::Floor:
1,626✔
458
                $increment = $hasDiscardedFraction && ! $isPositiveOrZero;
270✔
459

460
                break;
270✔
461

462
            case RoundingMode::HalfUp:
1,356✔
463
                $increment = $discardedFractionSign() >= 0;
273✔
464

465
                break;
273✔
466

467
            case RoundingMode::HalfDown:
1,083✔
468
                $increment = $discardedFractionSign() > 0;
273✔
469

470
                break;
273✔
471

472
            case RoundingMode::HalfCeiling:
810✔
473
                $increment = $isPositiveOrZero ? $discardedFractionSign() >= 0 : $discardedFractionSign() > 0;
270✔
474

475
                break;
270✔
476

477
            case RoundingMode::HalfFloor:
540✔
478
                $increment = $isPositiveOrZero ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
270✔
479

480
                break;
270✔
481

482
            case RoundingMode::HalfEven:
270✔
483
                $lastDigit = (int) $quotient[-1];
270✔
484
                $lastDigitIsEven = ($lastDigit % 2 === 0);
270✔
485
                $increment = $lastDigitIsEven ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
270✔
486

487
                break;
270✔
488
        }
489

490
        if ($increment) {
3,669✔
491
            return $this->add($quotient, $isPositiveOrZero ? '1' : '-1');
1,782✔
492
        }
493

494
        return $quotient;
2,583✔
495
    }
496

497
    /**
498
     * Calculates bitwise AND of two numbers.
499
     *
500
     * This method can be overridden by the concrete implementation if the underlying library
501
     * has built-in support for bitwise operations.
502
     *
503
     * @pure
504
     */
505
    public function and(string $a, string $b): string
506
    {
UNCOV
507
        return $this->bitwise('and', $a, $b);
102✔
508
    }
509

510
    /**
511
     * Calculates bitwise OR of two numbers.
512
     *
513
     * This method can be overridden by the concrete implementation if the underlying library
514
     * has built-in support for bitwise operations.
515
     *
516
     * @pure
517
     */
518
    public function or(string $a, string $b): string
519
    {
UNCOV
520
        return $this->bitwise('or', $a, $b);
92✔
521
    }
522

523
    /**
524
     * Calculates bitwise XOR of two numbers.
525
     *
526
     * This method can be overridden by the concrete implementation if the underlying library
527
     * has built-in support for bitwise operations.
528
     *
529
     * @pure
530
     */
531
    public function xor(string $a, string $b): string
532
    {
UNCOV
533
        return $this->bitwise('xor', $a, $b);
96✔
534
    }
535

536
    /**
537
     * Extracts the sign & digits of the operands.
538
     *
539
     * @return array{bool, bool, string, string} Whether $a and $b are negative, followed by their digits.
540
     *
541
     * @pure
542
     */
543
    final protected function init(string $a, string $b): array
544
    {
545
        return [
7,043✔
546
            $aNeg = ($a[0] === '-'),
7,043✔
547
            $bNeg = ($b[0] === '-'),
7,043✔
548

549
            $aNeg ? substr($a, 1) : $a,
7,043✔
550
            $bNeg ? substr($b, 1) : $b,
7,043✔
551
        ];
7,043✔
552
    }
553

554
    /**
555
     * @return array{string, string, string} GCD, X, Y
556
     *
557
     * @pure
558
     */
559
    private function gcdExtended(string $a, string $b): array
560
    {
UNCOV
561
        if ($a === '0') {
42✔
UNCOV
562
            return [$b, '0', '1'];
42✔
563
        }
564

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

UNCOV
567
        $x = $this->sub($y1, $this->mul($this->divQ($b, $a), $x1));
38✔
UNCOV
568
        $y = $x1;
38✔
569

UNCOV
570
        return [$gcd, $x, $y];
38✔
571
    }
572

573
    /**
574
     * Performs a bitwise operation on a decimal number.
575
     *
576
     * @param 'and'|'or'|'xor' $operator The operator to use.
577
     * @param string           $a        The left operand.
578
     * @param string           $b        The right operand.
579
     *
580
     * @pure
581
     */
582
    private function bitwise(string $operator, string $a, string $b): string
583
    {
UNCOV
584
        [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
290✔
585

UNCOV
586
        $aBin = $this->toBinary($aDig);
290✔
UNCOV
587
        $bBin = $this->toBinary($bDig);
290✔
588

UNCOV
589
        $aLen = strlen($aBin);
290✔
UNCOV
590
        $bLen = strlen($bBin);
290✔
591

UNCOV
592
        if ($aLen > $bLen) {
290✔
UNCOV
593
            $bBin = str_repeat("\x00", $aLen - $bLen) . $bBin;
102✔
UNCOV
594
        } elseif ($bLen > $aLen) {
188✔
UNCOV
595
            $aBin = str_repeat("\x00", $bLen - $aLen) . $aBin;
132✔
596
        }
597

UNCOV
598
        if ($aNeg) {
290✔
UNCOV
599
            $aBin = $this->twosComplement($aBin);
146✔
600
        }
UNCOV
601
        if ($bNeg) {
290✔
UNCOV
602
            $bBin = $this->twosComplement($bBin);
132✔
603
        }
604

UNCOV
605
        $value = match ($operator) {
290✔
UNCOV
606
            'and' => $aBin & $bBin,
102✔
UNCOV
607
            'or' => $aBin | $bBin,
92✔
UNCOV
608
            'xor' => $aBin ^ $bBin,
96✔
UNCOV
609
        };
290✔
610

UNCOV
611
        $negative = match ($operator) {
290✔
UNCOV
612
            'and' => $aNeg and $bNeg,
102✔
UNCOV
613
            'or' => $aNeg or $bNeg,
92✔
UNCOV
614
            'xor' => $aNeg xor $bNeg,
96✔
UNCOV
615
        };
290✔
616

UNCOV
617
        if ($negative) {
290✔
UNCOV
618
            $value = $this->twosComplement($value);
136✔
619
        }
620

UNCOV
621
        $result = $this->toDecimal($value);
290✔
622

UNCOV
623
        return $negative ? $this->neg($result) : $result;
290✔
624
    }
625

626
    /**
627
     * @param string $number A positive, binary number.
628
     *
629
     * @pure
630
     */
631
    private function twosComplement(string $number): string
632
    {
UNCOV
633
        $xor = str_repeat("\xff", strlen($number));
212✔
634

UNCOV
635
        $number ^= $xor;
212✔
636

UNCOV
637
        for ($i = strlen($number) - 1; $i >= 0; $i--) {
212✔
UNCOV
638
            $byte = ord($number[$i]);
212✔
639

UNCOV
640
            if (++$byte !== 256) {
212✔
UNCOV
641
                $number[$i] = chr($byte);
212✔
642

UNCOV
643
                break;
212✔
644
            }
645

UNCOV
646
            $number[$i] = "\x00";
8✔
647

UNCOV
648
            if ($i === 0) {
8✔
UNCOV
649
                $number = "\x01" . $number;
4✔
650
            }
651
        }
652

UNCOV
653
        return $number;
212✔
654
    }
655

656
    /**
657
     * Converts a decimal number to a binary string.
658
     *
659
     * @param string $number The number to convert, positive or zero, only digits.
660
     *
661
     * @pure
662
     */
663
    private function toBinary(string $number): string
664
    {
UNCOV
665
        $result = '';
290✔
666

UNCOV
667
        while ($number !== '0') {
290✔
UNCOV
668
            [$number, $remainder] = $this->divQR($number, '256');
290✔
UNCOV
669
            $result .= chr((int) $remainder);
290✔
670
        }
671

UNCOV
672
        return strrev($result);
290✔
673
    }
674

675
    /**
676
     * Returns the positive decimal representation of a binary number.
677
     *
678
     * @param string $bytes The bytes representing the number.
679
     *
680
     * @pure
681
     */
682
    private function toDecimal(string $bytes): string
683
    {
UNCOV
684
        $result = '0';
290✔
UNCOV
685
        $power = '1';
290✔
686

UNCOV
687
        for ($i = strlen($bytes) - 1; $i >= 0; $i--) {
290✔
UNCOV
688
            $index = ord($bytes[$i]);
290✔
689

UNCOV
690
            if ($index !== 0) {
290✔
UNCOV
691
                $result = $this->add(
286✔
UNCOV
692
                    $result,
286✔
UNCOV
693
                    ($index === 1) ? $power : $this->mul($power, (string) $index),
286✔
UNCOV
694
                );
286✔
695
            }
696

UNCOV
697
            if ($i !== 0) {
290✔
UNCOV
698
                $power = $this->mul($power, '256');
264✔
699
            }
700
        }
701

UNCOV
702
        return $result;
290✔
703
    }
704
}
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