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

brick / math / 21799165025

08 Feb 2026 01:43PM UTC coverage: 99.188% (+0.6%) from 98.544%
21799165025

push

github

BenMorel
Improve documentation

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

4 existing lines in 3 files now uncovered.

1344 of 1355 relevant lines covered (99.19%)

2300.87 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\DecimalHelper;
14
use LogicException;
15
use Override;
16

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

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

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

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

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

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

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

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

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

109
        return $zero;
24✔
110
    }
111

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

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

126
        return $one;
30✔
127
    }
128

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

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

143
        return $ten;
3✔
144
    }
145

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

339
        if ($exponent < 0) {
69✔
340
            throw InvalidArgumentException::negativeExponent();
3✔
341
        }
342

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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