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

brick / math / 21938548292

12 Feb 2026 08:09AM UTC coverage: 98.994% (+0.07%) from 98.92%
21938548292

push

github

BenMorel
Remove deprecated BigRational::simplified()

1377 of 1391 relevant lines covered (98.99%)

2295.18 hits per line

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

97.99
/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\InvalidArgumentException;
9
use Brick\Math\Exception\MathException;
10
use Brick\Math\Exception\RoundingNecessaryException;
11
use Brick\Math\Internal\DecimalHelper;
12
use LogicException;
13
use Override;
14

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

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

36
    /**
37
     * The denominator. Always strictly positive.
38
     */
39
    private BigInteger $denominator;
40

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

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

65
        if ($simplify) {
2,301✔
66
            $gcd = $numerator->gcd($denominator);
2,091✔
67

68
            $numerator = $numerator->quotient($gcd);
2,091✔
69
            $denominator = $denominator->quotient($gcd);
2,091✔
70
        }
71

72
        $this->numerator = $numerator;
2,301✔
73
        $this->denominator = $denominator;
2,301✔
74
    }
75

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

97
        return new BigRational($numerator, $denominator, true, true);
81✔
98
    }
99

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

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

114
        return $zero;
12✔
115
    }
116

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

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

131
        return $one;
24✔
132
    }
133

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

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

148
        return $ten;
3✔
149
    }
150

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

274
        $numerator = $this->numerator->multipliedBy($that->numerator);
21✔
275
        $denominator = $this->denominator->multipliedBy($that->denominator);
21✔
276

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

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

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

298
        $numerator = $this->numerator->multipliedBy($that->denominator);
21✔
299
        $denominator = $this->denominator->multipliedBy($that->numerator);
21✔
300

301
        return new BigRational($numerator, $denominator, true, true);
21✔
302
    }
303

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

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

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

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

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

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

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

360
    #[Override]
361
    public function compareTo(BigNumber|int|string $that): int
362
    {
363
        $that = BigRational::of($that);
726✔
364

365
        if ($this->denominator->isEqualTo($that->denominator)) {
726✔
366
            return $this->numerator->compareTo($that->numerator);
276✔
367
        }
368

369
        return $this->numerator
477✔
370
            ->multipliedBy($that->denominator)
477✔
371
            ->compareTo($that->numerator->multipliedBy($this->denominator));
477✔
372
    }
373

374
    #[Override]
375
    public function getSign(): int
376
    {
377
        return $this->numerator->getSign();
489✔
378
    }
379

380
    #[Override]
381
    public function toBigInteger(): BigInteger
382
    {
383
        if ($this->denominator->isEqualTo(1)) {
123✔
384
            return $this->numerator;
81✔
385
        }
386

387
        throw RoundingNecessaryException::rationalNotConvertibleToInteger();
42✔
388
    }
389

390
    #[Override]
391
    public function toBigDecimal(): BigDecimal
392
    {
393
        $scale = DecimalHelper::computeScaleFromReducedFractionDenominator($this->denominator->toString());
483✔
394

395
        if ($scale === null) {
483✔
396
            throw RoundingNecessaryException::rationalNotConvertibleToDecimal();
186✔
397
        }
398

399
        return $this->numerator->toBigDecimal()->dividedBy($this->denominator, $scale)->strippedOfTrailingZeros();
297✔
400
    }
401

402
    #[Override]
403
    public function toBigRational(): BigRational
404
    {
405
        return $this;
1,686✔
406
    }
407

408
    #[Override]
409
    public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::Unnecessary): BigDecimal
410
    {
411
        if ($scale < 0) {
24✔
412
            throw InvalidArgumentException::negativeScale();
3✔
413
        }
414

415
        if ($roundingMode === RoundingMode::Unnecessary) {
21✔
416
            $requiredScale = DecimalHelper::computeScaleFromReducedFractionDenominator($this->denominator->toString());
9✔
417

418
            if ($requiredScale === null) {
9✔
419
                throw RoundingNecessaryException::rationalNotConvertibleToDecimal();
3✔
420
            }
421

422
            if ($requiredScale > $scale) {
6✔
423
                throw RoundingNecessaryException::rationalScaleTooSmall();
3✔
424
            }
425
        }
426

427
        return $this->numerator->toBigDecimal()->dividedBy($this->denominator, $scale, $roundingMode);
15✔
428
    }
429

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

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

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

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

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

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

466
        if ($denominator === '1') {
582✔
467
            return $numerator;
159✔
468
        }
469

470
        return $numerator . '/' . $denominator;
423✔
471
    }
472

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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