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

brick / math / 21788758024

07 Feb 2026 11:34PM UTC coverage: 98.544% (-0.8%) from 99.37%
21788758024

push

github

BenMorel
Add fast-path optimizations

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

14 existing lines in 4 files now uncovered.

1354 of 1374 relevant lines covered (98.54%)

2270.88 hits per line

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

99.56
/src/BigDecimal.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\NegativeNumberException;
11
use Brick\Math\Exception\RoundingNecessaryException;
12
use Brick\Math\Internal\CalculatorRegistry;
13
use Brick\Math\Internal\Constants;
14
use Brick\Math\Internal\DecimalHelper;
15
use LogicException;
16
use Override;
17

18
use function in_array;
19
use function intdiv;
20
use function max;
21
use function rtrim;
22
use function str_repeat;
23
use function strlen;
24
use function substr;
25

26
/**
27
 * An arbitrarily large decimal number.
28
 *
29
 * This class is immutable.
30
 *
31
 * The scale of the number is the number of digits after the decimal point. It is always positive or zero.
32
 */
33
final readonly class BigDecimal extends BigNumber
34
{
35
    /**
36
     * The unscaled value of this decimal number.
37
     *
38
     * This is a string of digits with an optional leading minus sign.
39
     * No leading zero must be present.
40
     * No leading minus sign must be present if the value is 0.
41
     */
42
    private string $value;
43

44
    /**
45
     * The scale (number of digits after the decimal point) of this decimal number.
46
     *
47
     * This must be zero or more.
48
     */
49
    private int $scale;
50

51
    /**
52
     * Protected constructor. Use a factory method to obtain an instance.
53
     *
54
     * @param string $value The unscaled value, validated.
55
     * @param int    $scale The scale, validated.
56
     *
57
     * @pure
58
     */
59
    protected function __construct(string $value, int $scale = 0)
60
    {
61
        $this->value = $value;
12,396✔
62
        $this->scale = $scale;
12,396✔
63
    }
64

65
    /**
66
     * Creates a BigDecimal from an unscaled value and a scale.
67
     *
68
     * Example: `(12345, 3)` will result in the BigDecimal `12.345`.
69
     *
70
     * A negative scale is normalized to zero by appending zeros to the unscaled value.
71
     *
72
     * Example: `(12345, -3)` will result in the BigDecimal `12345000`.
73
     *
74
     * @param BigNumber|int|string $value The unscaled value. Must be convertible to a BigInteger.
75
     * @param int                  $scale The scale of the number. If negative, the scale will be set to zero
76
     *                                    and the unscaled value will be adjusted accordingly.
77
     *
78
     * @throws MathException If the value is not valid, or is not convertible to a BigInteger.
79
     *
80
     * @pure
81
     */
82
    public static function ofUnscaledValue(BigNumber|int|string $value, int $scale = 0): BigDecimal
83
    {
84
        $value = BigInteger::of($value)->toString();
279✔
85

86
        if ($scale < 0) {
279✔
87
            if ($value !== '0') {
66✔
88
                $value .= str_repeat('0', -$scale);
54✔
89
            }
90
            $scale = 0;
66✔
91
        }
92

93
        return new BigDecimal($value, $scale);
279✔
94
    }
95

96
    /**
97
     * Returns a BigDecimal representing zero, with a scale of zero.
98
     *
99
     * @pure
100
     */
101
    public static function zero(): BigDecimal
102
    {
103
        /** @var BigDecimal|null $zero */
104
        static $zero;
24✔
105

106
        if ($zero === null) {
24✔
107
            $zero = new BigDecimal('0');
3✔
108
        }
109

110
        return $zero;
24✔
111
    }
112

113
    /**
114
     * Returns a BigDecimal representing one, with a scale of zero.
115
     *
116
     * @pure
117
     */
118
    public static function one(): BigDecimal
119
    {
120
        /** @var BigDecimal|null $one */
121
        static $one;
30✔
122

123
        if ($one === null) {
30✔
124
            $one = new BigDecimal('1');
3✔
125
        }
126

127
        return $one;
30✔
128
    }
129

130
    /**
131
     * Returns a BigDecimal representing ten, with a scale of zero.
132
     *
133
     * @pure
134
     */
135
    public static function ten(): BigDecimal
136
    {
137
        /** @var BigDecimal|null $ten */
138
        static $ten;
3✔
139

140
        if ($ten === null) {
3✔
141
            $ten = new BigDecimal('10');
3✔
142
        }
143

144
        return $ten;
3✔
145
    }
146

147
    /**
148
     * Returns the sum of this number and the given one.
149
     *
150
     * The result has a scale of `max($this->scale, $that->scale)`.
151
     *
152
     * @param BigNumber|int|string $that The number to add. Must be convertible to a BigDecimal.
153
     *
154
     * @throws MathException If the number is not valid, or is not convertible to a BigDecimal.
155
     *
156
     * @pure
157
     */
158
    public function plus(BigNumber|int|string $that): BigDecimal
159
    {
160
        $that = BigDecimal::of($that);
264✔
161

162
        if ($that->value === '0' && $that->scale <= $this->scale) {
264✔
163
            return $this;
24✔
164
        }
165

166
        if ($this->value === '0' && $this->scale <= $that->scale) {
240✔
167
            return $that;
48✔
168
        }
169

170
        [$a, $b] = $this->scaleValues($this, $that);
216✔
171

172
        $value = CalculatorRegistry::get()->add($a, $b);
216✔
173
        $scale = $this->scale > $that->scale ? $this->scale : $that->scale;
216✔
174

175
        return new BigDecimal($value, $scale);
216✔
176
    }
177

178
    /**
179
     * Returns the difference of this number and the given one.
180
     *
181
     * The result has a scale of `max($this->scale, $that->scale)`.
182
     *
183
     * @param BigNumber|int|string $that The number to subtract. Must be convertible to a BigDecimal.
184
     *
185
     * @throws MathException If the number is not valid, or is not convertible to a BigDecimal.
186
     *
187
     * @pure
188
     */
189
    public function minus(BigNumber|int|string $that): BigDecimal
190
    {
191
        $that = BigDecimal::of($that);
168✔
192

193
        if ($that->value === '0' && $that->scale <= $this->scale) {
168✔
194
            return $this;
15✔
195
        }
196

197
        [$a, $b] = $this->scaleValues($this, $that);
153✔
198

199
        $value = CalculatorRegistry::get()->sub($a, $b);
153✔
200
        $scale = $this->scale > $that->scale ? $this->scale : $that->scale;
153✔
201

202
        return new BigDecimal($value, $scale);
153✔
203
    }
204

205
    /**
206
     * Returns the product of this number and the given one.
207
     *
208
     * The result has a scale of `$this->scale + $that->scale`.
209
     *
210
     * @param BigNumber|int|string $that The multiplier. Must be convertible to a BigDecimal.
211
     *
212
     * @throws MathException If the multiplier is not a valid number, or is not convertible to a BigDecimal.
213
     *
214
     * @pure
215
     */
216
    public function multipliedBy(BigNumber|int|string $that): BigDecimal
217
    {
218
        $that = BigDecimal::of($that);
246✔
219

220
        if ($that->value === '1' && $that->scale === 0) {
246✔
221
            return $this;
15✔
222
        }
223

224
        if ($this->value === '1' && $this->scale === 0) {
231✔
225
            return $that;
9✔
226
        }
227

228
        $value = CalculatorRegistry::get()->mul($this->value, $that->value);
222✔
229
        $scale = $this->scale + $that->scale;
222✔
230

231
        return new BigDecimal($value, $scale);
222✔
232
    }
233

234
    /**
235
     * Returns the result of the division of this number by the given one, at the given scale.
236
     *
237
     * @param BigNumber|int|string $that         The divisor. Must be convertible to a BigDecimal.
238
     * @param int                  $scale        The desired scale. Must be non-negative.
239
     * @param RoundingMode         $roundingMode An optional rounding mode, defaults to Unnecessary.
240
     *
241
     * @throws InvalidArgumentException   If the scale is negative.
242
     * @throws MathException              If the divisor is not a valid number or is not convertible to a BigDecimal.
243
     * @throws DivisionByZeroException    If the divisor is zero.
244
     * @throws RoundingNecessaryException If RoundingMode::Unnecessary is used and the result cannot be represented exactly at the given scale.
245
     *
246
     * @pure
247
     */
248
    public function dividedBy(BigNumber|int|string $that, int $scale, RoundingMode $roundingMode = RoundingMode::Unnecessary): BigDecimal
249
    {
250
        if ($scale < 0) {
1,905✔
251
            throw InvalidArgumentException::negativeScale();
3✔
252
        }
253

254
        $that = BigDecimal::of($that);
1,902✔
255

256
        if ($that->isZero()) {
1,902✔
257
            throw DivisionByZeroException::divisionByZero();
12✔
258
        }
259

260
        if ($that->value === '1' && $that->scale === 0 && $scale === $this->scale) {
1,890✔
261
            return $this;
114✔
262
        }
263

264
        $p = $this->valueWithMinScale($that->scale + $scale);
1,776✔
265
        $q = $that->valueWithMinScale($this->scale - $scale);
1,776✔
266

267
        $calculator = CalculatorRegistry::get();
1,776✔
268
        $result = $calculator->divRound($p, $q, $roundingMode);
1,776✔
269

270
        if ($result === null) {
1,776✔
271
            [$a, $b] = $this->scaleValues($this->abs(), $that->abs());
129✔
272

273
            $denominator = $calculator->divQ($b, $calculator->gcd($a, $b));
129✔
274
            $requiredScale = DecimalHelper::computeScaleFromReducedFractionDenominator($denominator);
129✔
275

276
            if ($requiredScale === null) {
129✔
277
                throw RoundingNecessaryException::decimalDivisionNotExact();
9✔
278
            }
279

280
            throw RoundingNecessaryException::decimalDivisionScaleTooSmall();
120✔
281
        }
282

283
        return new BigDecimal($result, $scale);
1,647✔
284
    }
285

286
    /**
287
     * Returns the exact result of the division of this number by the given one.
288
     *
289
     * The scale of the result is automatically calculated to fit all the fraction digits.
290
     *
291
     * @param BigNumber|int|string $that The divisor. Must be convertible to a BigDecimal.
292
     *
293
     * @throws MathException              If the divisor is not a valid number or is not convertible to a BigDecimal.
294
     * @throws DivisionByZeroException    If the divisor is zero.
295
     * @throws RoundingNecessaryException If the result yields an infinite number of digits.
296
     *
297
     * @pure
298
     */
299
    public function dividedByExact(BigNumber|int|string $that): BigDecimal
300
    {
301
        $that = BigDecimal::of($that);
93✔
302

303
        if ($that->value === '0') {
93✔
304
            throw DivisionByZeroException::divisionByZero();
9✔
305
        }
306

307
        [$a, $b] = $this->scaleValues($this->abs(), $that->abs());
84✔
308

309
        $calculator = CalculatorRegistry::get();
84✔
310

311
        $denominator = $calculator->divQ($b, $calculator->gcd($a, $b));
84✔
312
        $scale = DecimalHelper::computeScaleFromReducedFractionDenominator($denominator);
84✔
313

314
        if ($scale === null) {
84✔
315
            throw RoundingNecessaryException::decimalDivisionNotExact();
12✔
316
        }
317

318
        return $this->dividedBy($that, $scale)->strippedOfTrailingZeros();
72✔
319
    }
320

321
    /**
322
     * Returns this number exponentiated to the given value.
323
     *
324
     * The result has a scale of `$this->scale * $exponent`.
325
     *
326
     * @throws InvalidArgumentException If the exponent is not in the range 0 to 1,000,000.
327
     *
328
     * @pure
329
     */
330
    public function power(int $exponent): BigDecimal
331
    {
332
        if ($exponent === 0) {
120✔
333
            return BigDecimal::one();
27✔
334
        }
335

336
        if ($exponent === 1) {
93✔
337
            return $this;
21✔
338
        }
339

340
        if ($exponent < 0 || $exponent > Constants::MAX_POWER) {
72✔
341
            throw InvalidArgumentException::exponentOutOfRange($exponent, 0, Constants::MAX_POWER);
6✔
342
        }
343

344
        return new BigDecimal(CalculatorRegistry::get()->pow($this->value, $exponent), $this->scale * $exponent);
66✔
345
    }
346

347
    /**
348
     * Returns the quotient of the division of this number by the given one.
349
     *
350
     * The quotient has a scale of `0`.
351
     *
352
     * Examples:
353
     *
354
     * - `7.5` quotient `3` returns `2`
355
     * - `7.5` quotient `-3` returns `-2`
356
     * - `-7.5` quotient `3` returns `-2`
357
     * - `-7.5` quotient `-3` returns `2`
358
     *
359
     * @param BigNumber|int|string $that The divisor. Must be convertible to a BigDecimal.
360
     *
361
     * @throws MathException           If the divisor is not valid, or is not convertible to a BigDecimal.
362
     * @throws DivisionByZeroException If the divisor is zero.
363
     *
364
     * @pure
365
     */
366
    public function quotient(BigNumber|int|string $that): BigDecimal
367
    {
368
        $that = BigDecimal::of($that);
159✔
369

370
        if ($that->isZero()) {
159✔
371
            throw DivisionByZeroException::divisionByZero();
3✔
372
        }
373

374
        $p = $this->valueWithMinScale($that->scale);
156✔
375
        $q = $that->valueWithMinScale($this->scale);
156✔
376

377
        $quotient = CalculatorRegistry::get()->divQ($p, $q);
156✔
378

379
        return new BigDecimal($quotient, 0);
156✔
380
    }
381

382
    /**
383
     * Returns the remainder of the division of this number by the given one.
384
     *
385
     * The remainder has a scale of `max($this->scale, $that->scale)`.
386
     * The remainder, when non-zero, has the same sign as the dividend.
387
     *
388
     * Examples:
389
     *
390
     * - `7.5` remainder `3` returns `1.5`
391
     * - `7.5` remainder `-3` returns `1.5`
392
     * - `-7.5` remainder `3` returns `-1.5`
393
     * - `-7.5` remainder `-3` returns `-1.5`
394
     *
395
     * @param BigNumber|int|string $that The divisor. Must be convertible to a BigDecimal.
396
     *
397
     * @throws MathException           If the divisor is not valid, or is not convertible to a BigDecimal.
398
     * @throws DivisionByZeroException If the divisor is zero.
399
     *
400
     * @pure
401
     */
402
    public function remainder(BigNumber|int|string $that): BigDecimal
403
    {
404
        $that = BigDecimal::of($that);
159✔
405

406
        if ($that->isZero()) {
159✔
407
            throw DivisionByZeroException::divisionByZero();
3✔
408
        }
409

410
        $p = $this->valueWithMinScale($that->scale);
156✔
411
        $q = $that->valueWithMinScale($this->scale);
156✔
412

413
        $remainder = CalculatorRegistry::get()->divR($p, $q);
156✔
414

415
        $scale = $this->scale > $that->scale ? $this->scale : $that->scale;
156✔
416

417
        return new BigDecimal($remainder, $scale);
156✔
418
    }
419

420
    /**
421
     * Returns the quotient and remainder of the division of this number by the given one.
422
     *
423
     * The quotient has a scale of `0`, and the remainder has a scale of `max($this->scale, $that->scale)`.
424
     *
425
     * Examples:
426
     *
427
     * - `7.5` quotientAndRemainder `3` returns [`2`, `1.5`]
428
     * - `7.5` quotientAndRemainder `-3` returns [`-2`, `1.5`]
429
     * - `-7.5` quotientAndRemainder `3` returns [`-2`, `-1.5`]
430
     * - `-7.5` quotientAndRemainder `-3` returns [`2`, `-1.5`]
431
     *
432
     * @param BigNumber|int|string $that The divisor. Must be convertible to a BigDecimal.
433
     *
434
     * @return array{BigDecimal, BigDecimal} An array containing the quotient and the remainder.
435
     *
436
     * @throws MathException           If the divisor is not valid, or is not convertible to a BigDecimal.
437
     * @throws DivisionByZeroException If the divisor is zero.
438
     *
439
     * @pure
440
     */
441
    public function quotientAndRemainder(BigNumber|int|string $that): array
442
    {
443
        $that = BigDecimal::of($that);
159✔
444

445
        if ($that->isZero()) {
159✔
446
            throw DivisionByZeroException::divisionByZero();
3✔
447
        }
448

449
        $p = $this->valueWithMinScale($that->scale);
156✔
450
        $q = $that->valueWithMinScale($this->scale);
156✔
451

452
        [$quotient, $remainder] = CalculatorRegistry::get()->divQR($p, $q);
156✔
453

454
        $scale = $this->scale > $that->scale ? $this->scale : $that->scale;
156✔
455

456
        $quotient = new BigDecimal($quotient, 0);
156✔
457
        $remainder = new BigDecimal($remainder, $scale);
156✔
458

459
        return [$quotient, $remainder];
156✔
460
    }
461

462
    /**
463
     * Returns the square root of this number, rounded to the given scale according to the given rounding mode.
464
     *
465
     * @param int          $scale        The target scale. Must be non-negative.
466
     * @param RoundingMode $roundingMode An optional rounding mode, defaults to Unnecessary.
467
     *
468
     * @throws InvalidArgumentException   If the scale is negative.
469
     * @throws NegativeNumberException    If this number is negative.
470
     * @throws RoundingNecessaryException If RoundingMode::Unnecessary is used, but rounding is necessary.
471
     *
472
     * @pure
473
     */
474
    public function sqrt(int $scale, RoundingMode $roundingMode = RoundingMode::Unnecessary): BigDecimal
475
    {
476
        if ($scale < 0) {
6,150✔
477
            throw InvalidArgumentException::negativeScale();
3✔
478
        }
479

480
        if ($this->value === '0') {
6,147✔
481
            return new BigDecimal('0', $scale);
120✔
482
        }
483

484
        if ($this->value[0] === '-') {
6,027✔
485
            throw NegativeNumberException::squareRootOfNegativeNumber();
3✔
486
        }
487

488
        $value = $this->value;
6,024✔
489
        $inputScale = $this->scale;
6,024✔
490

491
        if ($inputScale % 2 !== 0) {
6,024✔
492
            $value .= '0';
2,346✔
493
            $inputScale++;
2,346✔
494
        }
495

496
        $calculator = CalculatorRegistry::get();
6,024✔
497

498
        // Keep one extra digit for rounding.
499
        $intermediateScale = max($scale, intdiv($inputScale, 2)) + 1;
6,024✔
500
        $value .= str_repeat('0', 2 * $intermediateScale - $inputScale);
6,024✔
501

502
        $sqrt = $calculator->sqrt($value);
6,024✔
503
        $isExact = $calculator->mul($sqrt, $sqrt) === $value;
6,024✔
504

505
        if (! $isExact) {
6,024✔
506
            if ($roundingMode === RoundingMode::Unnecessary) {
3,774✔
507
                throw RoundingNecessaryException::decimalSquareRootNotExact();
375✔
508
            }
509

510
            // Non-perfect-square sqrt is irrational, so the true value is strictly above this sqrt floor.
511
            // Add one at the intermediate scale to guarantee Up/Ceiling round up at the target scale.
512
            if (in_array($roundingMode, [RoundingMode::Up, RoundingMode::Ceiling], true)) {
3,399✔
513
                $sqrt = $calculator->add($sqrt, '1');
774✔
514
            }
515

516
            // Irrational sqrt cannot land exactly on a midpoint; treat tie-to-down modes as HalfUp.
517
            elseif (in_array($roundingMode, [RoundingMode::HalfDown, RoundingMode::HalfEven, RoundingMode::HalfFloor], true)) {
2,625✔
518
                $roundingMode = RoundingMode::HalfUp;
1,125✔
519
            }
520
        }
521

522
        $scaled = DecimalHelper::scale($sqrt, $intermediateScale, $scale, $roundingMode);
5,649✔
523

524
        if ($scaled === null) {
5,649✔
525
            throw RoundingNecessaryException::decimalSquareRootScaleTooSmall();
42✔
526
        }
527

528
        return new BigDecimal($scaled, $scale);
5,607✔
529
    }
530

531
    /**
532
     * Returns a copy of this BigDecimal with the decimal point moved to the left by the given number of places.
533
     *
534
     * @pure
535
     */
536
    public function withPointMovedLeft(int $places): BigDecimal
537
    {
538
        if ($places === 0) {
231✔
539
            return $this;
33✔
540
        }
541

542
        if ($places < 0) {
198✔
543
            return $this->withPointMovedRight(-$places);
66✔
544
        }
545

546
        return new BigDecimal($this->value, $this->scale + $places);
132✔
547
    }
548

549
    /**
550
     * Returns a copy of this BigDecimal with the decimal point moved to the right by the given number of places.
551
     *
552
     * @pure
553
     */
554
    public function withPointMovedRight(int $places): BigDecimal
555
    {
556
        if ($places === 0) {
231✔
557
            return $this;
33✔
558
        }
559

560
        if ($places < 0) {
198✔
561
            return $this->withPointMovedLeft(-$places);
66✔
562
        }
563

564
        $value = $this->value;
132✔
565
        $scale = $this->scale - $places;
132✔
566

567
        if ($scale < 0) {
132✔
568
            if ($value !== '0') {
78✔
569
                $value .= str_repeat('0', -$scale);
60✔
570
            }
571
            $scale = 0;
78✔
572
        }
573

574
        return new BigDecimal($value, $scale);
132✔
575
    }
576

577
    /**
578
     * Returns a copy of this BigDecimal with any trailing zeros removed from the fractional part.
579
     *
580
     * @pure
581
     */
582
    public function strippedOfTrailingZeros(): BigDecimal
583
    {
584
        if ($this->scale === 0) {
453✔
585
            return $this;
114✔
586
        }
587

588
        $trimmedValue = rtrim($this->value, '0');
339✔
589

590
        if ($trimmedValue === '') {
339✔
591
            return BigDecimal::zero();
9✔
592
        }
593

594
        $trimmableZeros = strlen($this->value) - strlen($trimmedValue);
330✔
595

596
        if ($trimmableZeros === 0) {
330✔
597
            return $this;
282✔
598
        }
599

600
        if ($trimmableZeros > $this->scale) {
48✔
601
            $trimmableZeros = $this->scale;
12✔
602
        }
603

604
        $value = substr($this->value, 0, -$trimmableZeros);
48✔
605
        $scale = $this->scale - $trimmableZeros;
48✔
606

607
        return new BigDecimal($value, $scale);
48✔
608
    }
609

610
    #[Override]
611
    public function negated(): static
612
    {
613
        return new BigDecimal(CalculatorRegistry::get()->neg($this->value), $this->scale);
1,401✔
614
    }
615

616
    #[Override]
617
    public function compareTo(BigNumber|int|string $that): int
618
    {
619
        $that = BigNumber::of($that);
858✔
620

621
        if ($that instanceof BigInteger) {
858✔
622
            $that = $that->toBigDecimal();
504✔
623
        }
624

625
        if ($that instanceof BigDecimal) {
858✔
626
            [$a, $b] = $this->scaleValues($this, $that);
789✔
627

628
            return CalculatorRegistry::get()->cmp($a, $b);
789✔
629
        }
630

631
        return -$that->compareTo($this);
69✔
632
    }
633

634
    #[Override]
635
    public function getSign(): int
636
    {
637
        return ($this->value === '0') ? 0 : (($this->value[0] === '-') ? -1 : 1);
2,505✔
638
    }
639

640
    /**
641
     * @pure
642
     */
643
    public function getUnscaledValue(): BigInteger
644
    {
645
        return self::newBigInteger($this->value);
2,892✔
646
    }
647

648
    /**
649
     * @pure
650
     */
651
    public function getScale(): int
652
    {
653
        return $this->scale;
2,892✔
654
    }
655

656
    /**
657
     * Returns the number of significant digits in the number.
658
     *
659
     * This is the number of digits in the unscaled value of the number.
660
     * The sign has no impact on the result.
661
     *
662
     * Examples:
663
     *   0 => 1
664
     *   0.0 => 1
665
     *   123 => 3
666
     *   123.456 => 6
667
     *   0.00123 => 3
668
     *   0.0012300 => 5
669
     *
670
     * @pure
671
     */
672
    public function getPrecision(): int
673
    {
674
        $length = strlen($this->value);
81✔
675

676
        return ($this->value[0] === '-') ? $length - 1 : $length;
81✔
677
    }
678

679
    /**
680
     * Returns the integral part of this decimal number.
681
     *
682
     * Examples:
683
     *
684
     * - `123.456` returns `123`
685
     * - `-123.456` returns `-123`
686
     * - `0.123` returns `0`
687
     * - `-0.123` returns `0`
688
     *
689
     * The following identity holds: `$d->isEqualTo($d->getFractionalPart()->plus($d->getIntegralPart()))`.
690
     *
691
     * @pure
692
     */
693
    public function getIntegralPart(): BigInteger
694
    {
695
        if ($this->scale === 0) {
42✔
696
            return self::newBigInteger($this->value);
9✔
697
        }
698

699
        $value = DecimalHelper::padUnscaledValue($this->value, $this->scale);
33✔
700
        $integerPart = substr($value, 0, -$this->scale);
33✔
701

702
        // Handle edge case where integral part is "-0"
703
        if ($integerPart === '-0') {
33✔
704
            $integerPart = '0';
3✔
705
        }
706

707
        return self::newBigInteger($integerPart);
33✔
708
    }
709

710
    /**
711
     * Returns the fractional part of this decimal number.
712
     *
713
     * Examples:
714
     *
715
     * - `123.456` returns `0.456`
716
     * - `-123.456` returns `-0.456`
717
     * - `123` returns `0`
718
     * - `-123` returns `0`
719
     *
720
     * The following identity holds: `$d->isEqualTo($d->getFractionalPart()->plus($d->getIntegralPart()))`.
721
     *
722
     * @pure
723
     */
724
    public function getFractionalPart(): BigDecimal
725
    {
726
        if ($this->scale === 0) {
42✔
727
            return BigDecimal::zero();
9✔
728
        }
729

730
        return $this->minus($this->getIntegralPart());
33✔
731
    }
732

733
    #[Override]
734
    public function toBigInteger(): BigInteger
735
    {
736
        if ($this->scale === 0) {
252✔
737
            return self::newBigInteger($this->value);
135✔
738
        }
739

740
        $rational = $this->toBigRational();
138✔
741
        $integralPart = $rational->getIntegralPart();
138✔
742

743
        if ($rational->isEqualTo($integralPart)) {
138✔
744
            return $integralPart;
87✔
745
        }
746

747
        throw RoundingNecessaryException::decimalNotConvertibleToInteger();
51✔
748
    }
749

750
    #[Override]
751
    public function toBigDecimal(): BigDecimal
752
    {
753
        return $this;
8,775✔
754
    }
755

756
    #[Override]
757
    public function toBigRational(): BigRational
758
    {
759
        $numerator = self::newBigInteger($this->value);
495✔
760
        $denominator = self::newBigInteger('1' . str_repeat('0', $this->scale));
495✔
761

762
        return self::newBigRational($numerator, $denominator, false, true);
495✔
763
    }
764

765
    #[Override]
766
    public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::Unnecessary): BigDecimal
767
    {
768
        if ($scale < 0) {
75✔
UNCOV
769
            throw InvalidArgumentException::negativeScale();
×
770
        }
771

772
        if ($scale === $this->scale) {
75✔
773
            return $this;
15✔
774
        }
775

776
        $value = DecimalHelper::scale($this->value, $this->scale, $scale, $roundingMode);
60✔
777

778
        if ($value === null) {
60✔
779
            throw RoundingNecessaryException::decimalScaleTooSmall();
9✔
780
        }
781

782
        return new BigDecimal($value, $scale);
51✔
783
    }
784

785
    #[Override]
786
    public function toInt(): int
787
    {
788
        return $this->toBigInteger()->toInt();
33✔
789
    }
790

791
    #[Override]
792
    public function toFloat(): float
793
    {
794
        return (float) $this->toString();
117✔
795
    }
796

797
    /**
798
     * @return numeric-string
799
     */
800
    #[Override]
801
    public function toString(): string
802
    {
803
        if ($this->scale === 0) {
7,203✔
804
            /** @var numeric-string */
805
            return $this->value;
1,593✔
806
        }
807

808
        $value = DecimalHelper::padUnscaledValue($this->value, $this->scale);
5,655✔
809

810
        /** @phpstan-ignore return.type */
811
        return substr($value, 0, -$this->scale) . '.' . substr($value, -$this->scale);
5,655✔
812
    }
813

814
    /**
815
     * This method is required for serializing the object and SHOULD NOT be accessed directly.
816
     *
817
     * @internal
818
     *
819
     * @return array{value: string, scale: int}
820
     */
821
    public function __serialize(): array
822
    {
823
        return ['value' => $this->value, 'scale' => $this->scale];
3✔
824
    }
825

826
    /**
827
     * This method is only here to allow unserializing the object and cannot be accessed directly.
828
     *
829
     * @internal
830
     *
831
     * @param array{value: string, scale: int} $data
832
     *
833
     * @throws LogicException
834
     */
835
    public function __unserialize(array $data): void
836
    {
837
        /** @phpstan-ignore isset.initializedProperty */
838
        if (isset($this->value)) {
6✔
839
            throw new LogicException('__unserialize() is an internal function, it must not be called directly.');
3✔
840
        }
841

842
        /** @phpstan-ignore deadCode.unreachable */
843
        $this->value = $data['value'];
3✔
844
        $this->scale = $data['scale'];
3✔
845
    }
846

847
    #[Override]
848
    protected static function from(BigNumber $number): static
849
    {
850
        return $number->toBigDecimal();
11,667✔
851
    }
852

853
    /**
854
     * Puts the internal values of the given decimal numbers on the same scale.
855
     *
856
     * @return array{string, string} The scaled integer values of $x and $y.
857
     *
858
     * @pure
859
     */
860
    private function scaleValues(BigDecimal $x, BigDecimal $y): array
861
    {
862
        $a = $x->value;
1,323✔
863
        $b = $y->value;
1,323✔
864

865
        if ($b !== '0' && $x->scale > $y->scale) {
1,323✔
866
            $b .= str_repeat('0', $x->scale - $y->scale);
501✔
867
        } elseif ($a !== '0' && $x->scale < $y->scale) {
879✔
868
            $a .= str_repeat('0', $y->scale - $x->scale);
198✔
869
        }
870

871
        return [$a, $b];
1,323✔
872
    }
873

874
    /**
875
     * @pure
876
     */
877
    private function valueWithMinScale(int $scale): string
878
    {
879
        $value = $this->value;
1,932✔
880

881
        if ($this->value !== '0' && $scale > $this->scale) {
1,932✔
882
            $value .= str_repeat('0', $scale - $this->scale);
1,770✔
883
        }
884

885
        return $value;
1,932✔
886
    }
887
}
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