Completed
Push — master ( 16ba57...edea34 )
by Andreu
13s
created

Decimal::isLessOrEqualTo()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 9.4285
cc 2
eloc 3
nc 2
nop 2
crap 2
1
<?php
2
declare(strict_types=1);
3
4
namespace Litipk\BigNumbers;
5
6
use Litipk\BigNumbers\DecimalConstants as DecimalConstants;
7
8
use Litipk\BigNumbers\Errors\InfiniteInputError;
9
use Litipk\BigNumbers\Errors\NaNInputError;
10
use Litipk\BigNumbers\Errors\NotImplementedError;
11
12
/**
13
 * Immutable object that represents a rational number
14
 *
15
 * @author Andreu Correa Casablanca <[email protected]>
16
 */
17
class Decimal
18
{
19
    const DEFAULT_SCALE = 16;
20
    const CLASSIC_DECIMAL_NUMBER_REGEXP = '/^([+\-]?)0*(([1-9][0-9]*|[0-9])(\.[0-9]+)?)$/';
21
    const EXP_NOTATION_NUMBER_REGEXP = '/^ (?P<sign> [+\-]?) 0*(?P<mantissa> [0-9](?P<decimals> \.[0-9]+)?) [eE] (?P<expSign> [+\-]?)(?P<exp> \d+)$/x';
22
    const EXP_NUM_GROUPS_NUMBER_REGEXP = '/^ (?P<int> \d*) (?: \. (?P<dec> \d+) ) E (?P<sign>[\+\-]) (?P<exp>\d+) $/x';
23
24
    /**
25
     * Internal numeric value
26
     * @var string
27
     */
28
    protected $value;
29
30
    /**
31
     * Number of digits behind the point
32
     * @var integer
33
     */
34
    private $scale;
35
36 169
    private function __construct(string $value, int $scale)
37
    {
38 169
        $this->value = $value;
39 169
        $this->scale = $scale;
40 169
    }
41
42
    private function __clone()
43
    {
44
    }
45
46
    /**
47
     * Decimal "constructor".
48
     *
49
     * @param  mixed $value
50
     * @param  int   $scale
51
     * @return Decimal
52
     */
53 9
    public static function create($value, int $scale = null): Decimal
54
    {
55 9
        if (\is_int($value)) {
56 4
            return self::fromInteger($value);
57 8
        } elseif (\is_float($value)) {
58 4
            return self::fromFloat($value, $scale);
59 4
        } elseif (\is_string($value)) {
60 2
            return self::fromString($value, $scale);
61 2
        } elseif ($value instanceof Decimal) {
62 1
            return self::fromDecimal($value, $scale);
63
        } else {
64 1
            throw new \TypeError(
65
                'Expected (int, float, string, Decimal), but received ' .
0 ignored issues
show
Unused Code introduced by
The call to TypeError::__construct() has too many arguments starting with 'Expected (int, float, s...ue) : \gettype($value)).

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
66 1
                (\is_object($value) ? \get_class($value) : \gettype($value))
67
            );
68
        }
69
    }
70
71 98
    public static function fromInteger(int $intValue): Decimal
72
    {
73 98
        self::paramsValidation($intValue, null);
74
75 98
        return new static((string)$intValue, 0);
76
    }
77
78
    /**
79
     * @param  float $fltValue
80
     * @param  int   $scale
81
     * @return Decimal
82
     */
83 47
    public static function fromFloat(float $fltValue, int $scale = null): Decimal
84
    {
85 47
        self::paramsValidation($fltValue, $scale);
86
87 47
        if (\is_infinite($fltValue)) {
88
            throw new InfiniteInputError('fltValue must be a finite number');
89 47
        } elseif (\is_nan($fltValue)) {
90 1
            throw new NaNInputError("fltValue can't be NaN");
91
        }
92
93 46
        $strValue = (string) $fltValue;
94 46
        $hasPoint = (false !== \strpos($strValue, '.'));
95
96 46
        if (\preg_match(self::EXP_NUM_GROUPS_NUMBER_REGEXP, $strValue, $capture)) {
97 6
            if (null === $scale) {
98 3
                $scale = ('-' === $capture['sign'])
99 2
                    ? $capture['exp'] + \strlen($capture['dec'])
100 3
                    : self::DEFAULT_SCALE;
101
            }
102 6
            $strValue = \number_format($fltValue, $scale, '.', '');
103
        } else {
104
            $naturalScale = (
105 40
                \strlen((string)\fmod($fltValue, 1.0)) - 2 - (($fltValue < 0) ? 1 : 0) + (!$hasPoint ? 1 : 0)
106
            );
107
108 40
            if (null === $scale) {
109 38
                $scale = $naturalScale;
110
            } else {
111 2
                $strValue .= ($hasPoint ? '' : '.') . \str_pad('', $scale - $naturalScale, '0');
112
            }
113
        }
114
115 46
        return new static($strValue, $scale);
116
    }
117
118
    /**
119
     * @param  string  $strValue
120
     * @param  integer $scale
121
     * @return Decimal
122
     */
123 131
    public static function fromString(string $strValue, int $scale = null): Decimal
124
    {
125 131
        self::paramsValidation($strValue, $scale);
126
127 130
        if (\preg_match(self::CLASSIC_DECIMAL_NUMBER_REGEXP, $strValue, $captures) === 1) {
128
129
            // Now it's time to strip leading zeros in order to normalize inner values
130 125
            $value = self::normalizeSign($captures[1]) . $captures[2];
131 125
            $min_scale = isset($captures[4]) ? \max(0, \strlen($captures[4]) - 1) : 0;
132
133 8
        } elseif (\preg_match(self::EXP_NOTATION_NUMBER_REGEXP, $strValue, $captures) === 1) {
134 7
            list($min_scale, $value) = self::fromExpNotationString(
135 7
                $scale,
136 7
                $captures['sign'],
137 7
                $captures['mantissa'],
138 7
                \strlen($captures['mantissa']) - 1,
139 7
                $captures['expSign'],
140 7
                (int)$captures['exp']
141
            );
142
        } else {
143 1
            throw new NaNInputError('strValue must be a number');
144
        }
145
146 129
        $scale = $scale ?? $min_scale;
147 129
        if ($scale < $min_scale) {
148 68
            $value = self::innerRound($value, $scale);
149 126
        } elseif ($min_scale < $scale) {
150 17
            $hasPoint = (false !== \strpos($value, '.'));
151 17
            $value .= ($hasPoint ? '' : '.') . \str_pad('', $scale - $min_scale, '0');
152
        }
153
154 129
        return new static($value, $scale);
155
    }
156
157
    /**
158
     * Constructs a new Decimal object based on a previous one,
159
     * but changing it's $scale property.
160
     *
161
     * @param  Decimal  $decValue
162
     * @param  null|int $scale
163
     * @return Decimal
164
     */
165 3
    public static function fromDecimal(Decimal $decValue, int $scale = null): Decimal
166
    {
167 3
        self::paramsValidation($decValue, $scale);
168
169
        // This block protect us from unnecessary additional instances
170 3
        if ($scale === null || $scale >= $decValue->scale) {
171 3
            return $decValue;
172
        }
173
174 2
        return new static(
175 2
            self::innerRound($decValue->value, $scale),
176 2
            $scale
177
        );
178
    }
179
180
    /**
181
     * Adds two Decimal objects
182
     * @param  Decimal  $b
183
     * @param  null|int $scale
184
     * @return Decimal
185
     */
186 40
    public function add(Decimal $b, int $scale = null): Decimal
187
    {
188 40
        self::paramsValidation($b, $scale);
189
190 40
        return self::fromString(
191 40
            \bcadd($this->value, $b->value, \max($this->scale, $b->scale)),
192 40
            $scale
193
        );
194
    }
195
196
    /**
197
     * Subtracts two BigNumber objects
198
     * @param  Decimal $b
199
     * @param  integer $scale
200
     * @return Decimal
201
     */
202 33
    public function sub(Decimal $b, int $scale = null): Decimal
203
    {
204 33
        self::paramsValidation($b, $scale);
205
206 33
        return self::fromString(
207 33
            \bcsub($this->value, $b->value, \max($this->scale, $b->scale)),
208 33
            $scale
209
        );
210
    }
211
212
    /**
213
     * Multiplies two BigNumber objects
214
     * @param  Decimal $b
215
     * @param  integer $scale
216
     * @return Decimal
217
     */
218 48
    public function mul(Decimal $b, int $scale = null): Decimal
219
    {
220 48
        self::paramsValidation($b, $scale);
221
222 47
        if ($b->isZero()) {
223 1
            return DecimalConstants::Zero();
224
        }
225
226 47
        return self::fromString(
227 47
            \bcmul($this->value, $b->value, $this->scale + $b->scale),
228 47
            $scale
229
        );
230
    }
231
232
    /**
233
     * Divides the object by $b .
234
     * Warning: div with $scale == 0 is not the same as
235
     *          integer division because it rounds the
236
     *          last digit in order to minimize the error.
237
     *
238
     * @param  Decimal $b
239
     * @param  integer $scale
240
     * @return Decimal
241
     */
242 63
    public function div(Decimal $b, int $scale = null): Decimal
243
    {
244 63
        self::paramsValidation($b, $scale);
245
246 63
        if ($b->isZero()) {
247 1
            throw new \DomainException("Division by zero is not allowed.");
248 63
        } elseif ($this->isZero()) {
249 1
            return DecimalConstants::Zero();
250
        } else {
251 62
            if (null !== $scale) {
252 56
                $divscale = $scale;
253
            } else {
254
                // $divscale is calculated in order to maintain a reasonable precision
255 20
                $this_abs = $this->abs();
256 20
                $b_abs    = $b->abs();
257
258
                $log10_result =
259 20
                    self::innerLog10($this_abs->value, $this_abs->scale, 1) -
260 20
                    self::innerLog10($b_abs->value, $b_abs->scale, 1);
261
262 20
                $divscale = (int)\max(
263 20
                    $this->scale + $b->scale,
264 20
                    \max(
265 20
                        self::countSignificativeDigits($this, $this_abs),
266 20
                        self::countSignificativeDigits($b, $b_abs)
267 20
                    ) - \max(\ceil($log10_result), 0),
268 20
                    \ceil(-$log10_result) + 1
269
                );
270
            }
271
272 62
            return self::fromString(
273 62
                \bcdiv($this->value, $b->value, $divscale+1), $divscale
274
            );
275
        }
276
    }
277
278
    /**
279
     * Returns the square root of this object
280
     * @param  integer $scale
281
     * @return Decimal
282
     */
283 4
    public function sqrt(int $scale = null): Decimal
284
    {
285 4
        if ($this->isNegative()) {
286 1
            throw new \DomainException(
287 1
                "Decimal can't handle square roots of negative numbers (it's only for real numbers)."
288
            );
289 3
        } elseif ($this->isZero()) {
290 1
            return DecimalConstants::Zero();
291
        }
292
293 3
        $sqrt_scale = ($scale !== null ? $scale : $this->scale);
294
295 3
        return self::fromString(
296 3
            \bcsqrt($this->value, $sqrt_scale+1),
297 3
            $sqrt_scale
298
        );
299
    }
300
301
    /**
302
     * Powers this value to $b
303
     *
304
     * @param  Decimal  $b      exponent
305
     * @param  integer  $scale
306
     * @return Decimal
307
     */
308 10
    public function pow(Decimal $b, int $scale = null): Decimal
309
    {
310 10
        if ($this->isZero()) {
311 2
            if ($b->isPositive()) {
312 1
                return Decimal::fromDecimal($this, $scale);
313
            } else {
314 1
                throw new \DomainException("zero can't be powered to zero or negative numbers.");
315
            }
316 8
        } elseif ($b->isZero()) {
317 1
            return DecimalConstants::One();
318 7
        } else if ($b->isNegative()) {
319 2
            return DecimalConstants::One()->div(
320 2
                $this->pow($b->additiveInverse(), max($scale, self::DEFAULT_SCALE)),
321 2
                max($scale, self::DEFAULT_SCALE)
322
            );
323 7
        } elseif (0 === $b->scale) {
324 4
            $pow_scale = \max($this->scale, $b->scale, $scale ?? 0);
325
326 4
            return self::fromString(
327 4
                \bcpow($this->value, $b->value, $pow_scale+1),
328 4
                $pow_scale
329
            );
330
        } else {
331 4
            if ($this->isPositive()) {
332 3
                $pow_scale = \max($this->scale, $b->scale, $scale ?? 0);
333
334 3
                $truncated_b = \bcadd($b->value, '0', 0);
335 3
                $remaining_b = \bcsub($b->value, $truncated_b, $b->scale);
336
337 3
                $first_pow_approx = \bcpow($this->value, $truncated_b, $pow_scale+1);
338 3
                $intermediate_root = self::innerPowWithLittleExponent(
339 3
                    $this->value,
340 3
                    $remaining_b,
341 3
                    $b->scale,
342 3
                    $pow_scale+1
343
                );
344
345 3
                return Decimal::fromString(
346 3
                    \bcmul($first_pow_approx, $intermediate_root, $pow_scale+1),
347 3
                    $pow_scale
348
                );
349
            } else { // elseif ($this->isNegative())
350 1
                if (!$b->isInteger()) {
351 1
                    throw new NotImplementedError(
352
                        "Usually negative numbers can't be powered to non integer numbers. " .
353 1
                        "The cases where is possible are not implemented."
354
                    );
355
                }
356
357
                return (\preg_match('/^[+\-]?[0-9]*[02468](\.0+)?$/', $b->value, $captures) === 1)
358
                    ? $this->additiveInverse()->pow($b, $scale)                      // $b is an even number
359
                    : $this->additiveInverse()->pow($b, $scale)->additiveInverse();  // $b is an odd number
360
            }
361
        }
362
    }
363
364
    /**
365
     * Returns the object's logarithm in base 10
366
     * @param  integer $scale
367
     * @return Decimal
368
     */
369 5
    public function log10(int $scale = null): Decimal
370
    {
371 5
        if ($this->isNegative()) {
372 1
            throw new \DomainException(
373 1
                "Decimal can't handle logarithms of negative numbers (it's only for real numbers)."
374
            );
375 4
        } elseif ($this->isZero()) {
376 1
            throw new \DomainException(
377 1
                "Decimal can't represent infinite numbers."
378
            );
379
        }
380
381 3
        return self::fromString(
382 3
            self::innerLog10($this->value, $this->scale, $scale !== null ? $scale+1 : $this->scale+1),
383 3
            $scale
384
        );
385
    }
386
387 94
    public function isZero(int $scale = null): bool
388
    {
389 94
        $cmp_scale = $scale !== null ? $scale : $this->scale;
390
391 94
        return (\bccomp(self::innerRound($this->value, $cmp_scale), '0', $cmp_scale) === 0);
392
    }
393
394 29
    public function isPositive(): bool
395
    {
396 29
        return ($this->value[0] !== '-' && !$this->isZero());
397
    }
398
399 69
    public function isNegative(): bool
400
    {
401 69
        return ($this->value[0] === '-');
402
    }
403
404 3
    public function isInteger(): bool
405
    {
406 3
        return (\preg_match('/^[+\-]?[0-9]+(\.0+)?$/', $this->value, $captures) === 1);
407
    }
408
409
    /**
410
     * Equality comparison between this object and $b
411
     * @param  Decimal $b
412
     * @param integer $scale
413
     * @return boolean
414
     */
415 115
    public function equals(Decimal $b, int $scale = null): bool
416
    {
417 115
        self::paramsValidation($b, $scale);
418
419 115
        if ($this === $b) {
420 2
            return true;
421
        } else {
422 114
            $cmp_scale = $scale !== null ? $scale : \max($this->scale, $b->scale);
423
424
            return (
425 114
                \bccomp(
426 114
                    self::innerRound($this->value, $cmp_scale),
427 114
                    self::innerRound($b->value, $cmp_scale),
428 114
                    $cmp_scale
429 114
                ) === 0
430
            );
431
        }
432
    }
433
434
    /**
435
     * $this > $b : returns 1 , $this < $b : returns -1 , $this == $b : returns 0
436
     *
437
     * @param  Decimal $b
438
     * @param  integer $scale
439
     * @return integer
440
     */
441 54
    public function comp(Decimal $b, int $scale = null): int
442
    {
443 54
        self::paramsValidation($b, $scale);
444
445 54
        if ($this === $b) {
446 7
            return 0;
447
        }
448
449 53
        $cmp_scale = $scale !== null ? $scale : \max($this->scale, $b->scale);
450
451 53
        return \bccomp(
452 53
            self::innerRound($this->value, $cmp_scale),
453 53
            self::innerRound($b->value, $cmp_scale),
454 53
            $cmp_scale
455
        );
456
    }
457
458
459
    /**
460
     * Returns true if $this > $b, otherwise false
461
     *
462
     * @param  Decimal $b
463
     * @param  integer $scale
464
     * @return bool
465
     */
466 3
    public function isGreaterThan(Decimal $b, int $scale = null): bool
467
    {
468 3
        return $this->comp($b, $scale) === 1;
469
    }
470
471
    /**
472
     * Returns true if $this >= $b
473
     *
474
     * @param  Decimal $b
475
     * @param  integer $scale
476
     * @return bool
477
     */
478 3
    public function isGreaterOrEqualTo(Decimal $b, int $scale = null): bool
479
    {
480 3
        $comparisonResult = $this->comp($b, $scale);
481
482 3
        return $comparisonResult === 1 || $comparisonResult === 0;
483
    }
484
485
    /**
486
     * Returns true if $this < $b, otherwise false
487
     *
488
     * @param  Decimal $b
489
     * @param  integer $scale
490
     * @return bool
491
     */
492 3
    public function isLessThan(Decimal $b, int $scale = null): bool
493
    {
494 3
        return $this->comp($b, $scale) === -1;
495
    }
496
497
    /**
498
     * Returns true if $this <= $b, otherwise false
499
     *
500
     * @param  Decimal $b
501
     * @param  integer $scale
502
     * @return bool
503
     */
504 3
    public function isLessOrEqualTo(Decimal $b, int $scale = null): bool
505
    {
506 3
        $comparisonResult = $this->comp($b, $scale);
507
508 3
        return $comparisonResult === -1 || $comparisonResult === 0;
509
    }
510
511
    /**
512
     * Returns the element's additive inverse.
513
     * @return Decimal
514
     */
515 15
    public function additiveInverse(): Decimal
516
    {
517 15
        if ($this->isZero()) {
518 1
            return $this;
519 14
        } elseif ($this->isNegative()) {
520 12
            $value = \substr($this->value, 1);
521
        } else { // if ($this->isPositive()) {
522 2
            $value = '-' . $this->value;
523
        }
524
525 14
        return new static($value, $this->scale);
526
    }
527
528
529
    /**
530
     * "Rounds" the Decimal to have at most $scale digits after the point
531
     * @param  integer $scale
532
     * @return Decimal
533
     */
534 52
    public function round(int $scale = 0): Decimal
535
    {
536 52
        if ($scale >= $this->scale) {
537 21
            return $this;
538
        }
539
540 51
        return self::fromString(self::innerRound($this->value, $scale));
541
    }
542
543
    /**
544
     * "Ceils" the Decimal to have at most $scale digits after the point
545
     * @param  integer $scale
546
     * @return Decimal
547
     */
548 4
    public function ceil($scale = 0): Decimal
549
    {
550 4
        if ($scale >= $this->scale) {
551 2
            return $this;
552
        }
553
554 3
        if ($this->isNegative()) {
555 1
            return self::fromString(\bcadd($this->value, '0', $scale));
556
        }
557
558 2
        return $this->innerTruncate($scale);
559
    }
560
561 28
    private function innerTruncate(int $scale = 0, bool $ceil = true): Decimal
562
    {
563 28
        $rounded = \bcadd($this->value, '0', $scale);
564
565 28
        $rlen = \strlen($rounded);
566 28
        $tlen = \strlen($this->value);
567
568 28
        $mustTruncate = false;
569 28
        for ($i=$tlen-1; $i >= $rlen; $i--) {
570 28
            if ((int)$this->value[$i] > 0) {
571 28
                $mustTruncate = true;
572 28
                break;
573
            }
574
        }
575
576 28
        if ($mustTruncate) {
577 28
            $rounded = $ceil
578 2
                ? \bcadd($rounded, \bcpow('10', (string)-$scale, $scale), $scale)
579 28
                : \bcsub($rounded, \bcpow('10', (string)-$scale, $scale), $scale);
580
        }
581
582 28
        return self::fromString($rounded, $scale);
583
    }
584
585
    /**
586
     * "Floors" the Decimal to have at most $scale digits after the point
587
     * @param  integer $scale
588
     * @return Decimal
589
     */
590 47
    public function floor(int $scale = 0): Decimal
591
    {
592 47
        if ($scale >= $this->scale) {
593 38
            return $this;
594
        }
595
596 38
        if ($this->isNegative()) {
597 26
            return $this->innerTruncate($scale, false);
598
        }
599
600 35
        return self::fromString(\bcadd($this->value, '0', $scale));
601
    }
602
603
    /**
604
     * Returns the absolute value (always a positive number)
605
     * @return Decimal
606
     */
607 21
    public function abs(): Decimal
608
    {
609 21
        return ($this->isZero() || $this->isPositive())
610 19
            ? $this
611 21
            : $this->additiveInverse();
612
    }
613
614
    /**
615
     * Calculate modulo with a decimal
616
     * @param Decimal $d
617
     * @param integer $scale
618
     * @return $this % $d
619
     */
620 27
    public function mod(Decimal $d, int $scale = null): Decimal
621
    {
622 27
        $div = $this->div($d, 1)->floor();
623 27
        return $this->sub($div->mul($d), $scale);
624
    }
625
626
    /**
627
     * Calculates the sine of this method with the highest possible accuracy
628
     * Note that accuracy is limited by the accuracy of predefined PI;
629
     *
630
     * @param integer $scale
631
     * @return Decimal sin($this)
632
     */
633 13
    public function sin(int $scale = null): Decimal
634
    {
635
        // First normalise the number in the [0, 2PI] domain
636 13
        $x = $this->mod(DecimalConstants::PI()->mul(Decimal::fromString("2")));
637
638
        // PI has only 32 significant numbers
639 13
        $scale = (null === $scale) ? 32 : $scale;
640
641 13
        return self::factorialSerie(
642 13
            $x,
643 13
            DecimalConstants::zero(),
644
            function ($i) {
645 13
                return ($i % 2 === 1) ? (
646 13
                ($i % 4 === 1) ? DecimalConstants::one() : DecimalConstants::negativeOne()
647 13
                ) : DecimalConstants::zero();
648 13
            },
649 13
            $scale
650
        );
651
    }
652
653
    /**
654
     * Calculates the cosecant of this with the highest possible accuracy
655
     * Note that accuracy is limited by the accuracy of predefined PI;
656
     *
657
     * @param integer $scale
658
     * @return Decimal
659
     */
660 3
    public function cosec(int $scale = null): Decimal
661
    {
662 3
        $sin = $this->sin($scale + 2);
663 3
        if ($sin->isZero()) {
664
            throw new \DomainException(
665
                "The cosecant of this 'angle' is undefined."
666
            );
667
        }
668
669 3
        return DecimalConstants::one()->div($sin)->round($scale);
670
    }
671
672
    /**
673
     * Calculates the cosine of this method with the highest possible accuracy
674
     * Note that accuracy is limited by the accuracy of predefined PI;
675
     *
676
     * @param integer $scale
677
     * @return Decimal cos($this)
678
     */
679 13
    public function cos(int $scale = null): Decimal
680
    {
681
        // First normalise the number in the [0, 2PI] domain
682 13
        $x = $this->mod(DecimalConstants::PI()->mul(Decimal::fromString("2")));
683
684
        // PI has only 32 significant numbers
685 13
        $scale = ($scale === null) ? 32 : $scale;
686
687 13
        return self::factorialSerie(
688 13
            $x,
689 13
            DecimalConstants::one(),
690
            function ($i) {
691 13
                return ($i % 2 === 0) ? (
692 13
                    ($i % 4 === 0) ? DecimalConstants::one() : DecimalConstants::negativeOne()
693 13
                ) : DecimalConstants::zero();
694 13
            },
695 13
            $scale
696
        );
697
    }
698
699
    /**
700
     * Calculates the secant of this with the highest possible accuracy
701
     * Note that accuracy is limited by the accuracy of predefined PI;
702
     *
703
     * @param integer $scale
704
     * @return Decimal
705
     */
706 3
    public function sec(int $scale = null): Decimal
707
    {
708 3
        $cos = $this->cos($scale + 2);
709 3
        if ($cos->isZero()) {
710
            throw new \DomainException(
711
                "The secant of this 'angle' is undefined."
712
            );
713
        }
714
715 3
        return DecimalConstants::one()->div($cos)->round($scale);
716
    }
717
718
    /**
719
     *	Calculates the arcsine of this with the highest possible accuracy
720
     *
721
     * @param integer $scale
722
     * @return Decimal
723
     */
724 5
    public function arcsin(int $scale = null): Decimal
725
    {
726 5
        if($this->comp(DecimalConstants::one(), $scale + 2) === 1 || $this->comp(DecimalConstants::negativeOne(), $scale + 2) === -1) {
727 2
            throw new \DomainException(
728 2
                "The arcsin of this number is undefined."
729
            );
730
        }
731
732 3
        if ($this->round($scale)->isZero()) {
733
            return DecimalConstants::zero();
734
        }
735 3
        if ($this->round($scale)->equals(DecimalConstants::one())) {
736 1
            return DecimalConstants::pi()->div(Decimal::fromInteger(2))->round($scale);
737
        }
738 2
        if ($this->round($scale)->equals(DecimalConstants::negativeOne())) {
739 1
            return DecimalConstants::pi()->div(Decimal::fromInteger(-2))->round($scale);
740
        }
741
742 1
        $scale = ($scale === null) ? 32 : $scale;
743
744 1
        return self::powerSerie(
745 1
            $this,
746 1
            DecimalConstants::zero(),
747 1
            $scale
748
        );
749
    }
750
751
    /**
752
     *	Calculates the arccosine of this with the highest possible accuracy
753
     *
754
     * @param integer $scale
755
     * @return Decimal
756
     */
757 5
    public function arccos(int $scale = null): Decimal
758
    {
759 5
        if($this->comp(DecimalConstants::one(), $scale + 2) === 1 || $this->comp(DecimalConstants::negativeOne(), $scale + 2) === -1) {
760 2
            throw new \DomainException(
761 2
                "The arccos of this number is undefined."
762
            );
763
        }
764
765 3
        $piOverTwo = DecimalConstants::pi()->div(Decimal::fromInteger(2), $scale + 2)->round($scale);
766
767 3
        if ($this->round($scale)->isZero()) {
768
            return $piOverTwo;
769
        }
770 3
        if ($this->round($scale)->equals(DecimalConstants::one())) {
771 1
            return DecimalConstants::zero();
772
        }
773 2
        if ($this->round($scale)->equals(DecimalConstants::negativeOne())) {
774 1
            return DecimalConstants::pi()->round($scale);
775
        }
776
777 1
        $scale = ($scale === null) ? 32 : $scale;
778
779 1
        return $piOverTwo->sub(
780 1
            self::powerSerie(
781 1
                $this,
782 1
                DecimalConstants::zero(),
783 1
                $scale
784
            )
785 1
        )->round($scale);
786
    }
787
788
    /**
789
     *	Calculates the arctangente of this with the highest possible accuracy
790
     *
791
     * @param integer $scale
792
     * @return Decimal
793
     */
794 3
    public function arctan(int $scale = null): Decimal
795
    {
796 3
        $piOverFour = DecimalConstants::pi()->div(Decimal::fromInteger(4), $scale + 2)->round($scale);
797
798 3
        if ($this->round($scale)->isZero()) {
799 1
            return DecimalConstants::zero();
800
        }
801 2
        if ($this->round($scale)->equals(DecimalConstants::one())) {
802
            return $piOverFour;
803
        }
804 2
        if ($this->round($scale)->equals(DecimalConstants::negativeOne())) {
805 1
            return DecimalConstants::negativeOne()->mul($piOverFour);
806
        }
807
808 1
        $scale = ($scale === null) ? 32 : $scale;
809
810 1
        return self::simplePowerSerie(
811 1
            $this,
812 1
            DecimalConstants::zero(),
813 1
            $scale + 2
814 1
        )->round($scale);
815
    }
816
817
    /**
818
     * Calculates the arccotangente of this with the highest possible accuracy
819
     *
820
     * @param integer $scale
821
     * @return Decimal
822
     */
823 3
    public function arccot(int $scale = null): Decimal
824
    {
825 3
        $scale = ($scale === null) ? 32 : $scale;
826
827 3
        $piOverTwo = DecimalConstants::pi()->div(Decimal::fromInteger(2), $scale + 2);
828 3
        if ($this->round($scale)->isZero()) {
829 1
            return $piOverTwo->round($scale);
830
        }
831
832 2
        $piOverFour = DecimalConstants::pi()->div(Decimal::fromInteger(4), $scale + 2);
833 2
        if ($this->round($scale)->equals(DecimalConstants::one())) {
834
            return $piOverFour->round($scale);
835
        }
836 2
        if ($this->round($scale)->equals(DecimalConstants::negativeOne())) {
837 1
            return DecimalConstants::negativeOne()->mul($piOverFour, $scale + 2)->round($scale);
838
        }
839
840 1
        return $piOverTwo->sub(
841 1
            self::simplePowerSerie(
842 1
                $this,
843 1
                DecimalConstants::zero(),
844 1
                $scale + 2
845
            )
846 1
        )->round($scale);
847
    }
848
849
    /**
850
     * Calculates the arcsecant of this with the highest possible accuracy
851
     *
852
     * @param integer $scale
853
     * @return Decimal
854
     */
855 5
    public function arcsec(int $scale = null): Decimal
856
    {
857 5
        if($this->comp(DecimalConstants::one(), $scale + 2) === -1 && $this->comp(DecimalConstants::negativeOne(), $scale + 2) === 1) {
858 1
            throw new \DomainException(
859 1
                "The arcsecant of this number is undefined."
860
            );
861
        }
862
863 4
        $piOverTwo = DecimalConstants::pi()->div(Decimal::fromInteger(2), $scale + 2)->round($scale);
864
865 4
        if ($this->round($scale)->equals(DecimalConstants::one())) {
866 1
            return DecimalConstants::zero();
867
        }
868 3
        if ($this->round($scale)->equals(DecimalConstants::negativeOne())) {
869 1
            return DecimalConstants::pi()->round($scale);
870
        }
871
872 2
        $scale = ($scale === null) ? 32 : $scale;
873
874 2
        return $piOverTwo->sub(
875 2
            self::powerSerie(
876 2
                DecimalConstants::one()->div($this, $scale + 2),
877 2
                DecimalConstants::zero(),
878 2
                $scale + 2
879
            )
880 2
        )->round($scale);
881
    }
882
883
    /**
884
     * Calculates the arccosecant of this with the highest possible accuracy
885
     *
886
     * @param integer $scale
887
     * @return Decimal
888
     */
889 5
    public function arccsc(int $scale = null): Decimal
890
    {
891 5
        if($this->comp(DecimalConstants::one(), $scale + 2) === -1 && $this->comp(DecimalConstants::negativeOne(), $scale + 2) === 1) {
892 1
            throw new \DomainException(
893 1
                "The arccosecant of this number is undefined."
894
            );
895
        }
896
897 4
        $scale = ($scale === null) ? 32 : $scale;
898
899 4
        if ($this->round($scale)->equals(DecimalConstants::one())) {
900 1
            return DecimalConstants::pi()->div(Decimal::fromInteger(2), $scale + 2)->round($scale);
901
        }
902 3
        if ($this->round($scale)->equals(DecimalConstants::negativeOne())) {
903 1
            return DecimalConstants::pi()->div(Decimal::fromInteger(-2), $scale + 2)->round($scale);
904
        }
905
906 2
        return self::powerSerie(
907 2
            DecimalConstants::one()->div($this, $scale + 2),
908 2
            DecimalConstants::zero(),
909 2
            $scale + 2
910 2
        )->round($scale);
911
    }
912
913
    /**
914
     * Returns exp($this), said in other words: e^$this .
915
     *
916
     * @param integer $scale
917
     * @return Decimal
918
     */
919 11
    public function exp(int $scale = null): Decimal
920
    {
921 11
        if ($this->isZero()) {
922 3
            return DecimalConstants::one();
923
        }
924
925 8
        $scale = $scale ?? \max(
926
            $this->scale,
927 8
            (int)($this->isNegative() ? self::innerLog10($this->value, $this->scale, 0) : self::DEFAULT_SCALE)
928
        );
929
930 8
        return self::factorialSerie(
931
            $this, DecimalConstants::one(), function ($i) { return DecimalConstants::one(); }, $scale
0 ignored issues
show
Unused Code introduced by
The parameter $i is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
932
        );
933
    }
934
935
    /**
936
     * Internal method used to compute sin, cos and exp
937
     *
938
     * @param Decimal $x
939
     * @param Decimal $firstTerm
940
     * @param callable $generalTerm
941
     * @param $scale
942
     * @return Decimal
943
     */
944 28
    private static function factorialSerie (Decimal $x, Decimal $firstTerm, callable $generalTerm, int $scale): Decimal
945
    {
946 28
        $approx = $firstTerm;
947 28
        $change = DecimalConstants::One();
948
949 28
        $faculty = DecimalConstants::One();    // Calculates the faculty under the sign
950 28
        $xPowerN = DecimalConstants::One();    // Calculates x^n
951
952 28
        for ($i = 1; !$change->floor($scale+1)->isZero(); $i++) {
953
            // update x^n and n! for this walkthrough
954 28
            $xPowerN = $xPowerN->mul($x);
955 28
            $faculty = $faculty->mul(Decimal::fromInteger($i));
956
957
            /** @var Decimal $multiplier */
958 28
            $multiplier = $generalTerm($i);
959
960 28
            if (!$multiplier->isZero()) {
961 28
                $change = $multiplier->mul($xPowerN, $scale + 2)->div($faculty, $scale + 2);
962 28
                $approx = $approx->add($change, $scale + 2);
963
            }
964
        }
965
966 28
        return $approx->round($scale);
967
    }
968
969
970
    /**
971
     * Internal method used to compute arcsine and arcosine
972
     *
973
     * @param Decimal $x
974
     * @param Decimal $firstTerm
975
     * @param $scale
976
     * @return Decimal
977
     */
978 6
    private static function powerSerie (Decimal $x, Decimal $firstTerm, int $scale): Decimal
979
    {
980 6
        $approx = $firstTerm;
981 6
        $change = DecimalConstants::One();
982
983 6
        $xPowerN = DecimalConstants::One();     // Calculates x^n
984 6
        $factorN = DecimalConstants::One();      // Calculates a_n
0 ignored issues
show
Unused Code introduced by
$factorN is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
985
986 6
        $numerator = DecimalConstants::one();
987 6
        $denominator = DecimalConstants::one();
988
989 6
        for ($i = 1; !$change->floor($scale + 2)->isZero(); $i++) {
990 6
            $xPowerN = $xPowerN->mul($x);
991
992 6
            if ($i % 2 === 0) {
993 6
                $factorN = DecimalConstants::zero();
994 6
            } elseif ($i === 1) {
995 6
                $factorN = DecimalConstants::one();
996
            } else {
997 6
                $incrementNum = Decimal::fromInteger($i - 2);
998 6
                $numerator = $numerator->mul($incrementNum, $scale +2);
999
1000 6
                $incrementDen = Decimal::fromInteger($i - 1);
1001 6
                $increment = Decimal::fromInteger($i);
1002
                $denominator = $denominator
1003 6
                    ->div($incrementNum, $scale +2)
1004 6
                    ->mul($incrementDen, $scale +2)
1005 6
                    ->mul($increment, $scale +2);
1006
1007 6
                $factorN = $numerator->div($denominator, $scale + 2);
1008
            }
1009
1010 6
            if (!$factorN->isZero()) {
1011 6
                $change = $factorN->mul($xPowerN, $scale + 2);
1012 6
                $approx = $approx->add($change, $scale + 2);
1013
            }
1014
        }
1015
1016 6
        return $approx->round($scale);
1017
    }
1018
1019
    /**
1020
     * Internal method used to compute arctan and arccotan
1021
     *
1022
     * @param Decimal $x
1023
     * @param Decimal $firstTerm
1024
     * @param $scale
1025
     * @return Decimal
1026
     */
1027 2
    private static function simplePowerSerie (Decimal $x, Decimal $firstTerm, int $scale): Decimal
1028
    {
1029 2
        $approx = $firstTerm;
1030 2
        $change = DecimalConstants::One();
1031
1032 2
        $xPowerN = DecimalConstants::One();     // Calculates x^n
1033 2
        $sign = DecimalConstants::One();      // Calculates a_n
0 ignored issues
show
Unused Code introduced by
$sign is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1034
1035 2
        for ($i = 1; !$change->floor($scale + 2)->isZero(); $i++) {
1036 2
            $xPowerN = $xPowerN->mul($x);
1037
1038 2
            if ($i % 2 === 0) {
1039 2
                $factorN = DecimalConstants::zero();
1040
            } else {
1041 2
                 if ($i % 4 === 1) {
1042 2
                     $factorN = DecimalConstants::one()->div(Decimal::fromInteger($i), $scale + 2);
1043
                 } else {
1044 2
                     $factorN = DecimalConstants::negativeOne()->div(Decimal::fromInteger($i), $scale + 2);
1045
                 }
1046
            }
1047
1048 2
            if (!$factorN->isZero()) {
1049 2
                $change = $factorN->mul($xPowerN, $scale + 2);
1050 2
                $approx = $approx->add($change, $scale + 2);
1051
            }
1052
        }
1053
1054 2
        return $approx->round($scale);
1055
    }
1056
1057
    /**
1058
     * Calculates the tangent of this method with the highest possible accuracy
1059
     * Note that accuracy is limited by the accuracy of predefined PI;
1060
     *
1061
     * @param integer $scale
1062
     * @return Decimal tan($this)
1063
     */
1064 4
    public function tan(int $scale = null): Decimal
1065
    {
1066 4
	    $cos = $this->cos($scale + 2);
1067 4
	    if ($cos->isZero()) {
1068 1
	        throw new \DomainException(
1069 1
	            "The tangent of this 'angle' is undefined."
1070
	        );
1071
	    }
1072
1073 3
	    return $this->sin($scale + 2)->div($cos)->round($scale);
1074
    }
1075
1076
    /**
1077
     * Calculates the cotangent of this method with the highest possible accuracy
1078
     * Note that accuracy is limited by the accuracy of predefined PI;
1079
     *
1080
     * @param integer $scale
1081
     * @return Decimal cotan($this)
1082
     */
1083 4
    public function cotan(int $scale = null): Decimal
1084
    {
1085 4
	    $sin = $this->sin($scale + 2);
1086 4
	    if ($sin->isZero()) {
1087 1
	        throw new \DomainException(
1088 1
	            "The cotangent of this 'angle' is undefined."
1089
	        );
1090
	    }
1091
1092 3
	    return $this->cos($scale + 2)->div($sin)->round($scale);
1093
    }
1094
1095
    /**
1096
     * Indicates if the passed parameter has the same sign as the method's bound object.
1097
     *
1098
     * @param Decimal $b
1099
     * @return bool
1100
     */
1101
    public function hasSameSign(Decimal $b): bool
1102
    {
1103
        return $this->isPositive() && $b->isPositive() || $this->isNegative() && $b->isNegative();
1104
    }
1105
1106 3
    public function asFloat(): float
1107
    {
1108 3
        return \floatval($this->value);
1109
    }
1110
1111 1
    public function asInteger(): int
1112
    {
1113 1
        return \intval($this->value);
1114
    }
1115
1116
    /**
1117
     * WARNING: use with caution! Return the inner representation of the class.
1118
     */
1119 11
    public function innerValue(): string
1120
    {
1121 11
        return $this->value;
1122
    }
1123
1124
    /**
1125
     * @return string
1126
     */
1127 58
    public function __toString(): string
1128
    {
1129 58
        return $this->value;
1130
    }
1131
1132
    /*
1133
     *
1134
     */
1135 7
    private static function fromExpNotationString(
1136
        int $scale = null,
1137
        string $sign,
1138
        string $mantissa,
1139
        int $nDecimals,
1140
        string $expSign,
1141
        int $expVal
1142
    ): array
1143
    {
1144 7
        $mantissaScale = \max($nDecimals, 0);
1145
1146 7
        if (self::normalizeSign($expSign) === '') {
1147 5
            $minScale = \max($mantissaScale - $expVal, 0);
1148 5
            $tmp_multiplier = \bcpow('10', (string)$expVal);
1149
        } else {
1150 2
            $minScale = $mantissaScale + $expVal;
1151 2
            $tmp_multiplier = \bcpow('10', (string)-$expVal, $expVal);
1152
        }
1153
1154
        $value = (
1155 7
            self::normalizeSign($sign) .
1156 7
            \bcmul(
1157 7
                $mantissa,
1158 7
                $tmp_multiplier,
1159 7
                \max($minScale, $scale ?? 0)
1160
            )
1161
        );
1162
1163 7
        return [$minScale, $value];
1164
    }
1165
1166
    /**
1167
     * "Rounds" the decimal string to have at most $scale digits after the point
1168
     *
1169
     * @param  string $value
1170
     * @param  int    $scale
1171
     * @return string
1172
     */
1173 148
    private static function innerRound(string $value, int $scale = 0): string
1174
    {
1175 148
        $rounded = \bcadd($value, '0', $scale);
1176
1177 148
        $diffDigit = \bcsub($value, $rounded, $scale+1);
1178 148
        $diffDigit = (int)$diffDigit[\strlen($diffDigit)-1];
1179
1180 148
        if ($diffDigit >= 5) {
1181 67
            $rounded = ($diffDigit >= 5 && $value[0] !== '-')
1182 63
                ? \bcadd($rounded, \bcpow('10', (string)-$scale, $scale), $scale)
1183 67
                : \bcsub($rounded, \bcpow('10', (string)-$scale, $scale), $scale);
1184
        }
1185
1186 148
        return $rounded;
1187
    }
1188
1189
    /**
1190
     * Calculates the logarithm (in base 10) of $value
1191
     *
1192
     * @param  string $value     The number we want to calculate its logarithm (only positive numbers)
1193
     * @param  int    $in_scale  Expected scale used by $value (only positive numbers)
1194
     * @param  int    $out_scale Scale used by the return value (only positive numbers)
1195
     * @return string
1196
     */
1197 23
    private static function innerLog10(string $value, int $in_scale, int $out_scale): string
1198
    {
1199 23
        $value_len = \strlen($value);
1200
1201 23
        $cmp = \bccomp($value, '1', $in_scale);
1202
1203
        switch ($cmp) {
1204 23
            case 1:
1205 10
                $value_log10_approx = $value_len - ($in_scale > 0 ? ($in_scale+2) : 1);
1206
1207 10
                return \bcadd(
1208 10
                    (string)$value_log10_approx,
1209 10
                    (string)\log10((float)\bcdiv(
1210 10
                        $value,
1211 10
                        \bcpow('10', (string)$value_log10_approx),
1212 10
                        \min($value_len, $out_scale)
1213
                    )),
1214 10
                    $out_scale
1215
                );
1216 14
            case -1:
1217 13
                \preg_match('/^0*\.(0*)[1-9][0-9]*$/', $value, $captures);
1218 13
                $value_log10_approx = -\strlen($captures[1])-1;
1219
1220 13
                return \bcadd(
1221 13
                    (string)$value_log10_approx,
1222 13
                    (string)\log10((float)\bcmul(
1223 13
                        $value,
1224 13
                        \bcpow('10', (string)-$value_log10_approx),
1225 13
                        $in_scale + $value_log10_approx
1226
                    )),
1227 13
                    $out_scale
1228
                );
1229
            default: // case 0:
1230 7
                return '0';
1231
        }
1232
    }
1233
1234
    /**
1235
     * Returns $base^$exponent
1236
     *
1237
     * @param  string $base
1238
     * @param  string $exponent   0 < $exponent < 1
1239
     * @param  int    $exp_scale Number of $exponent's significative digits
1240
     * @param  int    $out_scale Number of significative digits that we want to compute
1241
     * @return string
1242
     */
1243 3
    private static function innerPowWithLittleExponent(
1244
        string $base,
1245
        string $exponent,
1246
        int $exp_scale,
1247
        int $out_scale
1248
    ): string
1249
    {
1250 3
        $inner_scale = (int)\ceil($exp_scale * \log(10) / \log(2)) + 1;
1251
1252 3
        $result_a = '1';
1253 3
        $result_b = '0';
1254
1255 3
        $actual_index = 0;
1256 3
        $exponent_remaining = $exponent;
1257
1258 3
        while (\bccomp($result_a, $result_b, $out_scale) !== 0 && \bccomp($exponent_remaining, '0', $inner_scale) !== 0) {
1259 3
            $result_b = $result_a;
1260 3
            $index_info = self::computeSquareIndex($exponent_remaining, $actual_index, $exp_scale, $inner_scale);
1261 3
            $exponent_remaining = $index_info[1];
1262 3
            $result_a = \bcmul(
1263 3
                $result_a,
1264 3
                self::compute2NRoot($base, $index_info[0], 2*($out_scale+1)),
1265 3
                2*($out_scale+1)
1266
            );
1267
        }
1268
1269 3
        return self::innerRound($result_a, $out_scale);
1270
    }
1271
1272
    /**
1273
     * Auxiliar method. It helps us to decompose the exponent into many summands.
1274
     *
1275
     * @param  string $exponent_remaining
1276
     * @param  int    $actual_index
1277
     * @param  int    $exp_scale           Number of $exponent's significative digits
1278
     * @param  int    $inner_scale         ceil($exp_scale*log(10)/log(2))+1;
1279
     * @return array
1280
     */
1281 3
    private static function computeSquareIndex(
1282
        string $exponent_remaining,
1283
        int $actual_index,
1284
        int $exp_scale,
1285
        int $inner_scale
1286
    ): array
1287
    {
1288 3
        $actual_rt = \bcpow('0.5', (string)$actual_index, $exp_scale);
1289 3
        $r = \bcsub($exponent_remaining, $actual_rt, $inner_scale);
1290
1291 3
        while (\bccomp($r, '0', $exp_scale) === -1) {
1292 3
            ++$actual_index;
1293 3
            $actual_rt = \bcmul('0.5', $actual_rt, $inner_scale);
1294 3
            $r = \bcsub($exponent_remaining, $actual_rt, $inner_scale);
1295
        }
1296
1297 3
        return [$actual_index, $r];
1298
    }
1299
1300
    /**
1301
     * Auxiliar method. Computes $base^((1/2)^$index)
1302
     *
1303
     * @param  string  $base
1304
     * @param  integer $index
1305
     * @param  integer $out_scale
1306
     * @return string
1307
     */
1308 3
    private static function compute2NRoot(string $base, int $index, int $out_scale): string
1309
    {
1310 3
        $result = $base;
1311
1312 3
        for ($i = 0; $i < $index; $i++) {
1313 3
            $result = \bcsqrt($result, ($out_scale + 1) * ($index - $i) + 1);
1314
        }
1315
1316 3
        return self::innerRound($result, $out_scale);
1317
    }
1318
1319
    /**
1320
     * Validates basic constructor's arguments
1321
     * @param  mixed    $value
1322
     * @param  null|int  $scale
1323
     */
1324 172
    protected static function paramsValidation($value, int $scale = null)
1325
    {
1326 172
        if (null === $value) {
1327
            throw new \InvalidArgumentException('$value must be a non null number');
1328
        }
1329
1330 172
        if (null !== $scale && $scale < 0) {
1331 2
            throw new \InvalidArgumentException('$scale must be a positive integer');
1332
        }
1333 171
    }
1334
1335
    /**
1336
     * @return string
1337
     */
1338 129
    private static function normalizeSign(string $sign): string
1339
    {
1340 129
        if ('+' === $sign) {
1341 4
            return '';
1342
        }
1343
1344 129
        return $sign;
1345
    }
1346
1347
    /**
1348
     * Counts the number of significant digits of $val.
1349
     * Assumes a consistent internal state (without zeros at the end or the start).
1350
     *
1351
     * @param  Decimal $val
1352
     * @param  Decimal $abs $val->abs()
1353
     * @return int
1354
     */
1355 20
    private static function countSignificativeDigits(Decimal $val, Decimal $abs): int
1356
    {
1357 20
        return \strlen($val->value) - (
1358 20
            ($abs->comp(DecimalConstants::One()) === -1) ? 2 : \max($val->scale, 1)
1359 20
        ) - ($val->isNegative() ? 1 : 0);
1360
    }
1361
}
1362