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

brick / math / 21788757013

07 Feb 2026 09:34PM UTC coverage: 98.522% (+0.006%) from 98.516%
21788757013

push

github

BenMorel
Use invalidFormat() instead of emptyNumber() exception for sign-only strings

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

36 existing lines in 4 files now uncovered.

1333 of 1353 relevant lines covered (98.52%)

2299.75 hits per line

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

99.55
/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,342✔
62
        $this->scale = $scale;
12,342✔
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;
15✔
105

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

110
        return $zero;
15✔
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);
222✔
161

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

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

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

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

175
        return new BigDecimal($value, $scale);
192✔
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);
135✔
192

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

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

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

202
        return new BigDecimal($value, $scale);
129✔
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);
783✔
620

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

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

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

631
        return -$that->compareTo($this);
54✔
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 whether this decimal number has a non-zero fractional part.
681
     *
682
     * @pure
683
     */
684
    public function hasNonZeroFractionalPart(): bool
685
    {
686
        if ($this->scale === 0) {
18✔
687
            return false;
6✔
688
        }
689

690
        $value = DecimalHelper::padUnscaledValue($this->value, $this->scale);
12✔
691

692
        return substr($value, -$this->scale) !== str_repeat('0', $this->scale);
12✔
693
    }
694

695
    #[Override]
696
    public function toBigInteger(): BigInteger
697
    {
698
        if ($this->scale === 0) {
252✔
699
            return self::newBigInteger($this->value);
135✔
700
        }
701

702
        $rational = $this->toBigRational();
138✔
703
        $integralPart = $rational->getIntegralPart();
138✔
704

705
        if ($rational->isEqualTo($integralPart)) {
138✔
706
            return $integralPart;
87✔
707
        }
708

709
        throw RoundingNecessaryException::decimalNotConvertibleToInteger();
51✔
710
    }
711

712
    #[Override]
713
    public function toBigDecimal(): BigDecimal
714
    {
715
        return $this;
8,754✔
716
    }
717

718
    #[Override]
719
    public function toBigRational(): BigRational
720
    {
721
        $numerator = self::newBigInteger($this->value);
474✔
722
        $denominator = self::newBigInteger('1' . str_repeat('0', $this->scale));
474✔
723

724
        return self::newBigRational($numerator, $denominator, false, true);
474✔
725
    }
726

727
    #[Override]
728
    public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::Unnecessary): BigDecimal
729
    {
730
        if ($scale < 0) {
75✔
UNCOV
731
            throw InvalidArgumentException::negativeScale();
×
732
        }
733

734
        if ($scale === $this->scale) {
75✔
735
            return $this;
15✔
736
        }
737

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

740
        if ($value === null) {
60✔
741
            throw RoundingNecessaryException::decimalScaleTooSmall();
9✔
742
        }
743

744
        return new BigDecimal($value, $scale);
51✔
745
    }
746

747
    #[Override]
748
    public function toInt(): int
749
    {
750
        return $this->toBigInteger()->toInt();
33✔
751
    }
752

753
    #[Override]
754
    public function toFloat(): float
755
    {
756
        return (float) $this->toString();
117✔
757
    }
758

759
    /**
760
     * @return numeric-string
761
     */
762
    #[Override]
763
    public function toString(): string
764
    {
765
        if ($this->scale === 0) {
7,152✔
766
            /** @var numeric-string */
767
            return $this->value;
1,584✔
768
        }
769

770
        $value = DecimalHelper::padUnscaledValue($this->value, $this->scale);
5,613✔
771

772
        /** @phpstan-ignore return.type */
773
        return substr($value, 0, -$this->scale) . '.' . substr($value, -$this->scale);
5,613✔
774
    }
775

776
    /**
777
     * This method is required for serializing the object and SHOULD NOT be accessed directly.
778
     *
779
     * @internal
780
     *
781
     * @return array{value: string, scale: int}
782
     */
783
    public function __serialize(): array
784
    {
785
        return ['value' => $this->value, 'scale' => $this->scale];
3✔
786
    }
787

788
    /**
789
     * This method is only here to allow unserializing the object and cannot be accessed directly.
790
     *
791
     * @internal
792
     *
793
     * @param array{value: string, scale: int} $data
794
     *
795
     * @throws LogicException
796
     */
797
    public function __unserialize(array $data): void
798
    {
799
        /** @phpstan-ignore isset.initializedProperty */
800
        if (isset($this->value)) {
6✔
801
            throw new LogicException('__unserialize() is an internal function, it must not be called directly.');
3✔
802
        }
803

804
        /** @phpstan-ignore deadCode.unreachable */
805
        $this->value = $data['value'];
3✔
806
        $this->scale = $data['scale'];
3✔
807
    }
808

809
    #[Override]
810
    protected static function from(BigNumber $number): static
811
    {
812
        return $number->toBigDecimal();
11,643✔
813
    }
814

815
    /**
816
     * Puts the internal values of the given decimal numbers on the same scale.
817
     *
818
     * @return array{string, string} The scaled integer values of $x and $y.
819
     *
820
     * @pure
821
     */
822
    private function scaleValues(BigDecimal $x, BigDecimal $y): array
823
    {
824
        $a = $x->value;
1,263✔
825
        $b = $y->value;
1,263✔
826

827
        if ($b !== '0' && $x->scale > $y->scale) {
1,263✔
828
            $b .= str_repeat('0', $x->scale - $y->scale);
459✔
829
        } elseif ($a !== '0' && $x->scale < $y->scale) {
837✔
830
            $a .= str_repeat('0', $y->scale - $x->scale);
198✔
831
        }
832

833
        return [$a, $b];
1,263✔
834
    }
835

836
    /**
837
     * @pure
838
     */
839
    private function valueWithMinScale(int $scale): string
840
    {
841
        $value = $this->value;
1,932✔
842

843
        if ($this->value !== '0' && $scale > $this->scale) {
1,932✔
844
            $value .= str_repeat('0', $scale - $this->scale);
1,770✔
845
        }
846

847
        return $value;
1,932✔
848
    }
849
}
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