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

brick / math / 21806924673

08 Feb 2026 10:42PM UTC coverage: 98.914% (-0.3%) from 99.188%
21806924673

push

github

BenMorel
Optimize BigRational::compareTo()

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

104 existing lines in 5 files now uncovered.

1366 of 1381 relevant lines covered (98.91%)

2312.69 hits per line

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

97.87
/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\NumberFormatException;
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,421✔
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,418✔
66
            $gcd = $numerator->gcd($denominator);
2,202✔
67

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

72
        $this->numerator = $numerator;
2,418✔
73
        $this->denominator = $denominator;
2,418✔
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 NumberFormatException      If an argument does not represent a valid number.
86
     * @throws RoundingNecessaryException If an argument represents a non-integer number.
87
     * @throws DivisionByZeroException    If the denominator is zero.
88
     *
89
     * @pure
90
     */
91
    public static function ofFraction(
92
        BigNumber|int|string $numerator,
93
        BigNumber|int|string $denominator,
94
    ): BigRational {
95
        $numerator = BigInteger::of($numerator);
81✔
96
        $denominator = BigInteger::of($denominator);
81✔
97

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

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

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

115
        return $zero;
3✔
116
    }
117

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

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

132
        return $one;
24✔
133
    }
134

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

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

149
        return $ten;
3✔
150
    }
151

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

388
        throw RoundingNecessaryException::rationalNotConvertibleToInteger();
33✔
389
    }
390

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

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

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

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

409
    #[Override]
410
    public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::Unnecessary): BigDecimal
411
    {
412
        return $this->numerator->toBigDecimal()->dividedBy($this->denominator, $scale, $roundingMode);
21✔
413
    }
414

415
    #[Override]
416
    public function toInt(): int
417
    {
418
        return $this->toBigInteger()->toInt();
42✔
419
    }
420

421
    #[Override]
422
    public function toFloat(): float
423
    {
424
        $numeratorFloat = $this->numerator->toFloat();
162✔
425
        $denominatorFloat = $this->denominator->toFloat();
162✔
426

427
        if (is_finite($numeratorFloat) && is_finite($denominatorFloat)) {
162✔
428
            return $numeratorFloat / $denominatorFloat;
66✔
429
        }
430

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

439
        return $this->numerator
96✔
440
            ->toBigDecimal()
96✔
441
            ->dividedBy($this->denominator, $scale, RoundingMode::HalfEven)
96✔
442
            ->toFloat();
96✔
443
    }
444

445
    #[Override]
446
    public function toString(): string
447
    {
448
        $numerator = $this->numerator->toString();
570✔
449
        $denominator = $this->denominator->toString();
570✔
450

451
        if ($denominator === '1') {
570✔
452
            return $numerator;
162✔
453
        }
454

455
        return $numerator . '/' . $denominator;
408✔
456
    }
457

458
    /**
459
     * Returns the decimal representation of this rational number, with repeating decimals in parentheses.
460
     *
461
     * Examples:
462
     *
463
     * - `10/3` returns `3.(3)`
464
     * - `171/70` returns `2.4(428571)`
465
     * - `1/2` returns `0.5`
466
     *
467
     * Warning: the length of the repeating decimal period can be as large as `denominator - 1`.
468
     * For fractions with large denominators, this method may use excessive memory and time.
469
     * For example, `1/100019` has a repeating period of 100,018 digits.
470
     *
471
     * @pure
472
     */
473
    public function toRepeatingDecimalString(): string
474
    {
475
        if ($this->isZero()) {
72✔
476
            return '0';
3✔
477
        }
478

479
        $sign = $this->numerator->isNegative() ? '-' : '';
69✔
480
        $numerator = $this->numerator->abs();
69✔
481
        $denominator = $this->denominator;
69✔
482

483
        $integral = $numerator->quotient($denominator);
69✔
484
        $remainder = $numerator->remainder($denominator);
69✔
485

486
        $integralString = $integral->toString();
69✔
487

488
        if ($remainder->isZero()) {
69✔
489
            return $sign . $integralString;
3✔
490
        }
491

492
        $digits = '';
66✔
493
        $remainderPositions = [];
66✔
494
        $index = 0;
66✔
495

496
        while (! $remainder->isZero()) {
66✔
497
            $remainderString = $remainder->toString();
66✔
498

499
            if (isset($remainderPositions[$remainderString])) {
66✔
500
                $repeatIndex = $remainderPositions[$remainderString];
51✔
501
                $nonRepeating = substr($digits, 0, $repeatIndex);
51✔
502
                $repeating = substr($digits, $repeatIndex);
51✔
503

504
                return $sign . $integralString . '.' . $nonRepeating . '(' . $repeating . ')';
51✔
505
            }
506

507
            $remainderPositions[$remainderString] = $index;
66✔
508
            $remainder = $remainder->multipliedBy(10);
66✔
509

510
            $digits .= $remainder->quotient($denominator)->toString();
66✔
511
            $remainder = $remainder->remainder($denominator);
66✔
512
            $index++;
66✔
513
        }
514

515
        return $sign . $integralString . '.' . $digits;
15✔
516
    }
517

518
    /**
519
     * This method is required for serializing the object and SHOULD NOT be accessed directly.
520
     *
521
     * @internal
522
     *
523
     * @return array{numerator: BigInteger, denominator: BigInteger}
524
     */
525
    public function __serialize(): array
526
    {
527
        return ['numerator' => $this->numerator, 'denominator' => $this->denominator];
3✔
528
    }
529

530
    /**
531
     * This method is only here to allow unserializing the object and cannot be accessed directly.
532
     *
533
     * @internal
534
     *
535
     * @param array{numerator: BigInteger, denominator: BigInteger} $data
536
     *
537
     * @throws LogicException
538
     */
539
    public function __unserialize(array $data): void
540
    {
541
        /** @phpstan-ignore isset.initializedProperty */
542
        if (isset($this->numerator)) {
6✔
543
            throw new LogicException('__unserialize() is an internal function, it must not be called directly.');
3✔
544
        }
545

546
        /** @phpstan-ignore deadCode.unreachable */
547
        $this->numerator = $data['numerator'];
3✔
548
        $this->denominator = $data['denominator'];
3✔
549
    }
550

551
    #[Override]
552
    protected static function from(BigNumber $number): static
553
    {
554
        return $number->toBigRational();
2,280✔
555
    }
556
}
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