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

brick / math / 21924796355

11 Feb 2026 10:02PM UTC coverage: 98.92% (+0.002%) from 98.918%
21924796355

push

github

BenMorel
Remove deprecated BigRational::simplified()

1374 of 1389 relevant lines covered (98.92%)

2298.24 hits per line

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

97.96
/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

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

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

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

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

64
        if ($simplify) {
2,298✔
65
            $gcd = $numerator->gcd($denominator);
2,088✔
66

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

71
        $this->numerator = $numerator;
2,298✔
72
        $this->denominator = $denominator;
2,298✔
73
    }
74

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

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

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

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

113
        return $zero;
12✔
114
    }
115

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

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

130
        return $one;
24✔
131
    }
132

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

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

147
        return $ten;
3✔
148
    }
149

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

407
    #[Override]
408
    public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::Unnecessary): BigDecimal
409
    {
410
        if ($roundingMode === RoundingMode::Unnecessary) {
21✔
411
            $requiredScale = DecimalHelper::computeScaleFromReducedFractionDenominator($this->denominator->toString());
9✔
412

413
            if ($requiredScale === null) {
9✔
414
                throw RoundingNecessaryException::rationalNotConvertibleToDecimal();
3✔
415
            }
416

417
            if ($requiredScale > $scale) {
6✔
418
                throw RoundingNecessaryException::rationalScaleTooSmall();
3✔
419
            }
420
        }
421

422
        return $this->numerator->toBigDecimal()->dividedBy($this->denominator, $scale, $roundingMode);
15✔
423
    }
424

425
    #[Override]
426
    public function toInt(): int
427
    {
428
        return $this->toBigInteger()->toInt();
48✔
429
    }
430

431
    #[Override]
432
    public function toFloat(): float
433
    {
434
        $numeratorFloat = $this->numerator->toFloat();
162✔
435
        $denominatorFloat = $this->denominator->toFloat();
162✔
436

437
        if (is_finite($numeratorFloat) && is_finite($denominatorFloat)) {
162✔
438
            return $numeratorFloat / $denominatorFloat;
66✔
439
        }
440

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

449
        return $this->numerator
96✔
450
            ->toBigDecimal()
96✔
451
            ->dividedBy($this->denominator, $scale, RoundingMode::HalfEven)
96✔
452
            ->toFloat();
96✔
453
    }
454

455
    #[Override]
456
    public function toString(): string
457
    {
458
        $numerator = $this->numerator->toString();
582✔
459
        $denominator = $this->denominator->toString();
582✔
460

461
        if ($denominator === '1') {
582✔
462
            return $numerator;
159✔
463
        }
464

465
        return $numerator . '/' . $denominator;
423✔
466
    }
467

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

490
        $sign = $this->numerator->isNegative() ? '-' : '';
69✔
491
        $numerator = $this->numerator->abs();
69✔
492
        $denominator = $this->denominator;
69✔
493

494
        $integral = $numerator->quotient($denominator);
69✔
495
        $remainder = $numerator->remainder($denominator);
69✔
496

497
        $integralString = $integral->toString();
69✔
498

499
        if ($remainder->isZero()) {
69✔
500
            return $sign . $integralString;
3✔
501
        }
502

503
        $digits = '';
66✔
504
        $remainderPositions = [];
66✔
505
        $index = 0;
66✔
506

507
        while (! $remainder->isZero()) {
66✔
508
            $remainderString = $remainder->toString();
66✔
509

510
            if (isset($remainderPositions[$remainderString])) {
66✔
511
                $repeatIndex = $remainderPositions[$remainderString];
51✔
512
                $nonRepeating = substr($digits, 0, $repeatIndex);
51✔
513
                $repeating = substr($digits, $repeatIndex);
51✔
514

515
                return $sign . $integralString . '.' . $nonRepeating . '(' . $repeating . ')';
51✔
516
            }
517

518
            $remainderPositions[$remainderString] = $index;
66✔
519
            $remainder = $remainder->multipliedBy(10);
66✔
520

521
            $digits .= $remainder->quotient($denominator)->toString();
66✔
522
            $remainder = $remainder->remainder($denominator);
66✔
523
            $index++;
66✔
524
        }
525

526
        return $sign . $integralString . '.' . $digits;
15✔
527
    }
528

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

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

557
        /** @phpstan-ignore deadCode.unreachable */
558
        $this->numerator = $data['numerator'];
3✔
559
        $this->denominator = $data['denominator'];
3✔
560
    }
561

562
    #[Override]
563
    protected static function from(BigNumber $number): static
564
    {
565
        return $number->toBigRational();
2,139✔
566
    }
567
}
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