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

brick / math / 21870644451

10 Feb 2026 03:15PM UTC coverage: 98.913% (-0.3%) from 99.188%
21870644451

push

github

BenMorel
Harmonize docs

1365 of 1380 relevant lines covered (98.91%)

2313.75 hits per line

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

97.95
/src/BigRational.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Brick\Math;
6

7
use Brick\Math\Exception\DivisionByZeroException;
8
use Brick\Math\Exception\MathException;
9
use Brick\Math\Exception\RoundingNecessaryException;
10
use Brick\Math\Internal\DecimalHelper;
11
use LogicException;
12
use Override;
13

14
use function is_finite;
15
use function max;
16
use function min;
17
use function strlen;
18
use function substr;
19
use function trigger_error;
20

21
use const E_USER_DEPRECATED;
22

23
/**
24
 * An arbitrarily large rational number.
25
 *
26
 * This class is immutable.
27
 *
28
 * Fractions are automatically simplified to lowest terms. For example, `2/4` becomes `1/2`.
29
 * The denominator is always strictly positive; the sign is carried by the numerator.
30
 */
31
final readonly class BigRational extends BigNumber
32
{
33
    /**
34
     * The numerator.
35
     */
36
    private BigInteger $numerator;
37

38
    /**
39
     * The denominator. Always strictly positive.
40
     */
41
    private BigInteger $denominator;
42

43
    /**
44
     * Protected constructor. Use a factory method to obtain an instance.
45
     *
46
     * @param BigInteger $numerator        The numerator.
47
     * @param BigInteger $denominator      The denominator.
48
     * @param bool       $checkDenominator Whether to check the denominator for negative and zero.
49
     *
50
     * @throws DivisionByZeroException If the denominator is zero.
51
     *
52
     * @pure
53
     */
54
    protected function __construct(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator, bool $simplify)
55
    {
56
        if ($checkDenominator) {
2,460✔
57
            if ($denominator->isZero()) {
162✔
58
                throw DivisionByZeroException::zeroDenominator();
3✔
59
            }
60

61
            if ($denominator->isNegative()) {
159✔
62
                $numerator = $numerator->negated();
54✔
63
                $denominator = $denominator->negated();
54✔
64
            }
65
        }
66

67
        if ($simplify) {
2,457✔
68
            $gcd = $numerator->gcd($denominator);
2,232✔
69

70
            $numerator = $numerator->quotient($gcd);
2,232✔
71
            $denominator = $denominator->quotient($gcd);
2,232✔
72
        }
73

74
        $this->numerator = $numerator;
2,457✔
75
        $this->denominator = $denominator;
2,457✔
76
    }
77

78
    /**
79
     * Creates a BigRational out of a numerator and a denominator.
80
     *
81
     * If the denominator is negative, the signs of both the numerator and the denominator
82
     * will be inverted to ensure that the denominator is always positive.
83
     *
84
     * @param BigNumber|int|string $numerator   The numerator. Must be convertible to a BigInteger.
85
     * @param BigNumber|int|string $denominator The denominator. Must be convertible to a BigInteger.
86
     *
87
     * @throws MathException           If an argument is not valid, or is not convertible to a BigInteger.
88
     * @throws DivisionByZeroException If the denominator is zero.
89
     *
90
     * @pure
91
     */
92
    public static function ofFraction(
93
        BigNumber|int|string $numerator,
94
        BigNumber|int|string $denominator,
95
    ): BigRational {
96
        $numerator = BigInteger::of($numerator);
81✔
97
        $denominator = BigInteger::of($denominator);
81✔
98

99
        return new BigRational($numerator, $denominator, true, true);
81✔
100
    }
101

102
    /**
103
     * Returns a BigRational representing zero.
104
     *
105
     * @pure
106
     */
107
    public static function zero(): BigRational
108
    {
109
        /** @var BigRational|null $zero */
110
        static $zero;
3✔
111

112
        if ($zero === null) {
3✔
113
            $zero = new BigRational(BigInteger::zero(), BigInteger::one(), false, false);
3✔
114
        }
115

116
        return $zero;
3✔
117
    }
118

119
    /**
120
     * Returns a BigRational representing one.
121
     *
122
     * @pure
123
     */
124
    public static function one(): BigRational
125
    {
126
        /** @var BigRational|null $one */
127
        static $one;
24✔
128

129
        if ($one === null) {
24✔
130
            $one = new BigRational(BigInteger::one(), BigInteger::one(), false, false);
3✔
131
        }
132

133
        return $one;
24✔
134
    }
135

136
    /**
137
     * Returns a BigRational representing ten.
138
     *
139
     * @pure
140
     */
141
    public static function ten(): BigRational
142
    {
143
        /** @var BigRational|null $ten */
144
        static $ten;
3✔
145

146
        if ($ten === null) {
3✔
147
            $ten = new BigRational(BigInteger::ten(), BigInteger::one(), false, false);
3✔
148
        }
149

150
        return $ten;
3✔
151
    }
152

153
    /**
154
     * @pure
155
     */
156
    public function getNumerator(): BigInteger
157
    {
158
        return $this->numerator;
189✔
159
    }
160

161
    /**
162
     * @pure
163
     */
164
    public function getDenominator(): BigInteger
165
    {
166
        return $this->denominator;
189✔
167
    }
168

169
    /**
170
     * Returns the integral part of this rational number.
171
     *
172
     * Examples:
173
     *
174
     * - `7/3` returns `2` (since 7/3 = 2 + 1/3)
175
     * - `-7/3` returns `-2` (since -7/3 = -2 + (-1/3))
176
     *
177
     * The following identity holds: `$r->isEqualTo($r->getFractionalPart()->plus($r->getIntegralPart()))`.
178
     *
179
     * @pure
180
     */
181
    public function getIntegralPart(): BigInteger
182
    {
183
        return $this->numerator->quotient($this->denominator);
195✔
184
    }
185

186
    /**
187
     * Returns the fractional part of this rational number.
188
     *
189
     * Examples:
190
     *
191
     * - `7/3` returns `1/3` (since 7/3 = 2 + 1/3)
192
     * - `-7/3` returns `-1/3` (since -7/3 = -2 + (-1/3))
193
     *
194
     * The following identity holds: `$r->isEqualTo($r->getFractionalPart()->plus($r->getIntegralPart()))`.
195
     *
196
     * @pure
197
     */
198
    public function getFractionalPart(): BigRational
199
    {
200
        return new BigRational($this->numerator->remainder($this->denominator), $this->denominator, false, false);
57✔
201
    }
202

203
    /**
204
     * Returns the sum of this number and the given one.
205
     *
206
     * @param BigNumber|int|string $that The number to add.
207
     *
208
     * @throws MathException If the number is not valid.
209
     *
210
     * @pure
211
     */
212
    public function plus(BigNumber|int|string $that): BigRational
213
    {
214
        $that = BigRational::of($that);
123✔
215

216
        if ($that->isZero()) {
123✔
217
            return $this;
9✔
218
        }
219

220
        if ($this->isZero()) {
114✔
221
            return $that;
15✔
222
        }
223

224
        $numerator = $this->numerator->multipliedBy($that->denominator);
105✔
225
        $numerator = $numerator->plus($that->numerator->multipliedBy($this->denominator));
105✔
226
        $denominator = $this->denominator->multipliedBy($that->denominator);
105✔
227

228
        return new BigRational($numerator, $denominator, false, true);
105✔
229
    }
230

231
    /**
232
     * Returns the difference of this number and the given one.
233
     *
234
     * @param BigNumber|int|string $that The number to subtract.
235
     *
236
     * @throws MathException If the number is not valid.
237
     *
238
     * @pure
239
     */
240
    public function minus(BigNumber|int|string $that): BigRational
241
    {
242
        $that = BigRational::of($that);
15✔
243

244
        if ($that->isZero()) {
15✔
245
            return $this;
×
246
        }
247

248
        if ($this->isZero()) {
15✔
249
            return $that->negated();
×
250
        }
251

252
        $numerator = $this->numerator->multipliedBy($that->denominator);
15✔
253
        $numerator = $numerator->minus($that->numerator->multipliedBy($this->denominator));
15✔
254
        $denominator = $this->denominator->multipliedBy($that->denominator);
15✔
255

256
        return new BigRational($numerator, $denominator, false, true);
15✔
257
    }
258

259
    /**
260
     * Returns the product of this number and the given one.
261
     *
262
     * @param BigNumber|int|string $that The multiplier.
263
     *
264
     * @throws MathException If the multiplier is not valid.
265
     *
266
     * @pure
267
     */
268
    public function multipliedBy(BigNumber|int|string $that): BigRational
269
    {
270
        $that = BigRational::of($that);
21✔
271

272
        if ($that->isZero() || $this->isZero()) {
21✔
273
            return BigRational::zero();
×
274
        }
275

276
        $numerator = $this->numerator->multipliedBy($that->numerator);
21✔
277
        $denominator = $this->denominator->multipliedBy($that->denominator);
21✔
278

279
        return new BigRational($numerator, $denominator, false, true);
21✔
280
    }
281

282
    /**
283
     * Returns the result of the division of this number by the given one.
284
     *
285
     * @param BigNumber|int|string $that The divisor.
286
     *
287
     * @throws MathException           If the divisor is not valid.
288
     * @throws DivisionByZeroException If the divisor is zero.
289
     *
290
     * @pure
291
     */
292
    public function dividedBy(BigNumber|int|string $that): BigRational
293
    {
294
        $that = BigRational::of($that);
24✔
295

296
        if ($that->isZero()) {
24✔
297
            throw DivisionByZeroException::divisionByZero();
3✔
298
        }
299

300
        $numerator = $this->numerator->multipliedBy($that->denominator);
21✔
301
        $denominator = $this->denominator->multipliedBy($that->numerator);
21✔
302

303
        return new BigRational($numerator, $denominator, true, true);
21✔
304
    }
305

306
    /**
307
     * Returns this number exponentiated to the given value.
308
     *
309
     * Unlike BigInteger and BigDecimal, BigRational supports negative exponents:
310
     * the result is the reciprocal raised to the absolute value of the exponent.
311
     *
312
     * @throws DivisionByZeroException If the exponent is negative and this number is zero.
313
     *
314
     * @pure
315
     */
316
    public function power(int $exponent): BigRational
317
    {
318
        if ($exponent === 0) {
159✔
319
            return BigRational::one();
21✔
320
        }
321

322
        if ($exponent === 1) {
138✔
323
            return $this;
36✔
324
        }
325

326
        if ($exponent < 0) {
117✔
327
            return $this->reciprocal()->power(-$exponent);
51✔
328
        }
329

330
        return new BigRational(
93✔
331
            $this->numerator->power($exponent),
93✔
332
            $this->denominator->power($exponent),
93✔
333
            false,
93✔
334
            false,
93✔
335
        );
93✔
336
    }
337

338
    /**
339
     * Returns the reciprocal of this BigRational.
340
     *
341
     * The reciprocal has the numerator and denominator swapped.
342
     *
343
     * @throws DivisionByZeroException If the numerator is zero.
344
     *
345
     * @pure
346
     */
347
    public function reciprocal(): BigRational
348
    {
349
        if ($this->isZero()) {
72✔
350
            throw DivisionByZeroException::reciprocalOfZero();
12✔
351
        }
352

353
        return new BigRational($this->denominator, $this->numerator, true, false);
60✔
354
    }
355

356
    #[Override]
357
    public function negated(): static
358
    {
359
        return new BigRational($this->numerator->negated(), $this->denominator, false, false);
30✔
360
    }
361

362
    /**
363
     * Returns the simplified value of this BigRational.
364
     *
365
     * @deprecated Since 0.15, this is a no-op. BigRational numbers are always in their simplest form.
366
     */
367
    public function simplified(): BigRational
368
    {
369
        trigger_error(
39✔
370
            'BigRational::simplified() is a no-op since 0.15, and will be removed in 0.16. BigRational numbers are now always simplified to lowest terms.',
39✔
371
            E_USER_DEPRECATED,
39✔
372
        );
39✔
373

374
        return $this;
39✔
375
    }
376

377
    #[Override]
378
    public function compareTo(BigNumber|int|string $that): int
379
    {
380
        $that = BigRational::of($that);
864✔
381

382
        if ($this->denominator->isEqualTo($that->denominator)) {
864✔
383
            return $this->numerator->compareTo($that->numerator);
369✔
384
        }
385

386
        return $this->numerator
528✔
387
            ->multipliedBy($that->denominator)
528✔
388
            ->compareTo($that->numerator->multipliedBy($this->denominator));
528✔
389
    }
390

391
    #[Override]
392
    public function getSign(): int
393
    {
394
        return $this->numerator->getSign();
489✔
395
    }
396

397
    #[Override]
398
    public function toBigInteger(): BigInteger
399
    {
400
        if ($this->denominator->isEqualTo(1)) {
111✔
401
            return $this->numerator;
78✔
402
        }
403

404
        throw RoundingNecessaryException::rationalNotConvertibleToInteger();
33✔
405
    }
406

407
    #[Override]
408
    public function toBigDecimal(): BigDecimal
409
    {
410
        $scale = DecimalHelper::computeScaleFromReducedFractionDenominator($this->denominator->toString());
483✔
411

412
        if ($scale === null) {
483✔
413
            throw RoundingNecessaryException::rationalNotConvertibleToDecimal();
186✔
414
        }
415

416
        return $this->numerator->toBigDecimal()->dividedBy($this->denominator, $scale)->strippedOfTrailingZeros();
297✔
417
    }
418

419
    #[Override]
420
    public function toBigRational(): BigRational
421
    {
422
        return $this;
1,707✔
423
    }
424

425
    #[Override]
426
    public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::Unnecessary): BigDecimal
427
    {
428
        return $this->numerator->toBigDecimal()->dividedBy($this->denominator, $scale, $roundingMode);
21✔
429
    }
430

431
    #[Override]
432
    public function toInt(): int
433
    {
434
        return $this->toBigInteger()->toInt();
42✔
435
    }
436

437
    #[Override]
438
    public function toFloat(): float
439
    {
440
        $numeratorFloat = $this->numerator->toFloat();
162✔
441
        $denominatorFloat = $this->denominator->toFloat();
162✔
442

443
        if (is_finite($numeratorFloat) && is_finite($denominatorFloat)) {
162✔
444
            return $numeratorFloat / $denominatorFloat;
66✔
445
        }
446

447
        // At least one side overflows to INF; use a decimal approximation instead.
448
        // We need ~17 significant digits for double precision (we use 20 for some margin). Since $scale controls
449
        // decimal places (not significant digits), we subtract the estimated order of magnitude so that large results
450
        // use fewer decimal places and small results use more (to look past leading zeros). Clamped to [0, 350] as
451
        // doubles range from e-324 to e308 (350 ≈ 324 + 20 significant digits + margin).
452
        $magnitude = strlen($this->numerator->abs()->toString()) - strlen($this->denominator->toString());
96✔
453
        $scale = min(350, max(0, 20 - $magnitude));
96✔
454

455
        return $this->numerator
96✔
456
            ->toBigDecimal()
96✔
457
            ->dividedBy($this->denominator, $scale, RoundingMode::HalfEven)
96✔
458
            ->toFloat();
96✔
459
    }
460

461
    #[Override]
462
    public function toString(): string
463
    {
464
        $numerator = $this->numerator->toString();
609✔
465
        $denominator = $this->denominator->toString();
609✔
466

467
        if ($denominator === '1') {
609✔
468
            return $numerator;
177✔
469
        }
470

471
        return $numerator . '/' . $denominator;
432✔
472
    }
473

474
    /**
475
     * Returns the decimal representation of this rational number, with repeating decimals in parentheses.
476
     *
477
     * WARNING: This method is unbounded.
478
     *          The length of the repeating decimal period can be as large as `denominator - 1`.
479
     *          For fractions with large denominators, this method can use excessive memory and CPU time.
480
     *          For example, `1/100019` has a repeating period of 100,018 digits.
481
     *
482
     * Examples:
483
     *
484
     * - `10/3` returns `3.(3)`
485
     * - `171/70` returns `2.4(428571)`
486
     * - `1/2` returns `0.5`
487
     *
488
     * @pure
489
     */
490
    public function toRepeatingDecimalString(): string
491
    {
492
        if ($this->isZero()) {
72✔
493
            return '0';
3✔
494
        }
495

496
        $sign = $this->numerator->isNegative() ? '-' : '';
69✔
497
        $numerator = $this->numerator->abs();
69✔
498
        $denominator = $this->denominator;
69✔
499

500
        $integral = $numerator->quotient($denominator);
69✔
501
        $remainder = $numerator->remainder($denominator);
69✔
502

503
        $integralString = $integral->toString();
69✔
504

505
        if ($remainder->isZero()) {
69✔
506
            return $sign . $integralString;
3✔
507
        }
508

509
        $digits = '';
66✔
510
        $remainderPositions = [];
66✔
511
        $index = 0;
66✔
512

513
        while (! $remainder->isZero()) {
66✔
514
            $remainderString = $remainder->toString();
66✔
515

516
            if (isset($remainderPositions[$remainderString])) {
66✔
517
                $repeatIndex = $remainderPositions[$remainderString];
51✔
518
                $nonRepeating = substr($digits, 0, $repeatIndex);
51✔
519
                $repeating = substr($digits, $repeatIndex);
51✔
520

521
                return $sign . $integralString . '.' . $nonRepeating . '(' . $repeating . ')';
51✔
522
            }
523

524
            $remainderPositions[$remainderString] = $index;
66✔
525
            $remainder = $remainder->multipliedBy(10);
66✔
526

527
            $digits .= $remainder->quotient($denominator)->toString();
66✔
528
            $remainder = $remainder->remainder($denominator);
66✔
529
            $index++;
66✔
530
        }
531

532
        return $sign . $integralString . '.' . $digits;
15✔
533
    }
534

535
    /**
536
     * This method is required for serializing the object and SHOULD NOT be accessed directly.
537
     *
538
     * @internal
539
     *
540
     * @return array{numerator: BigInteger, denominator: BigInteger}
541
     */
542
    public function __serialize(): array
543
    {
544
        return ['numerator' => $this->numerator, 'denominator' => $this->denominator];
3✔
545
    }
546

547
    /**
548
     * This method is only here to allow unserializing the object and cannot be accessed directly.
549
     *
550
     * @internal
551
     *
552
     * @param array{numerator: BigInteger, denominator: BigInteger} $data
553
     *
554
     * @throws LogicException
555
     */
556
    public function __unserialize(array $data): void
557
    {
558
        /** @phpstan-ignore isset.initializedProperty */
559
        if (isset($this->numerator)) {
6✔
560
            throw new LogicException('__unserialize() is an internal function, it must not be called directly.');
3✔
561
        }
562

563
        /** @phpstan-ignore deadCode.unreachable */
564
        $this->numerator = $data['numerator'];
3✔
565
        $this->denominator = $data['denominator'];
3✔
566
    }
567

568
    #[Override]
569
    protected static function from(BigNumber $number): static
570
    {
571
        return $number->toBigRational();
2,319✔
572
    }
573
}
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