Completed
Pull Request — master (#75)
by
unknown
01:40
created

Decimal::jsonSerialize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 2
1
<?php
2
declare(strict_types=1);
3
4
namespace Litipk\BigNumbers;
5
6
use JsonSerializable;
7
use Litipk\BigNumbers\DecimalConstants as DecimalConstants;
8
9
use Litipk\BigNumbers\Errors\InfiniteInputError;
10
use Litipk\BigNumbers\Errors\NaNInputError;
11
use Litipk\BigNumbers\Errors\NotImplementedError;
12
13
/**
14
 * Immutable object that represents a rational number
15
 *
16
 * @author Andreu Correa Casablanca <[email protected]>
17
 */
18
class Decimal implements JsonSerializable
19
{
20
    const DEFAULT_SCALE = 16;
21
    const CLASSIC_DECIMAL_NUMBER_REGEXP = '/^([+\-]?)0*(([1-9][0-9]*|[0-9])(\.[0-9]+)?)$/';
22
    const EXP_NOTATION_NUMBER_REGEXP = '/^ (?P<sign> [+\-]?) 0*(?P<mantissa> [0-9](?P<decimals> \.[0-9]+)?) [eE] (?P<expSign> [+\-]?)(?P<exp> \d+)$/x';
23
    const EXP_NUM_GROUPS_NUMBER_REGEXP = '/^ (?P<int> \d*) (?: \. (?P<dec> \d+) ) E (?P<sign>[\+\-]) (?P<exp>\d+) $/x';
24
25
    /**
26
     * Internal numeric value
27
     * @var string
28
     */
29
    protected $value;
30
31
    /**
32
     * Number of digits behind the point
33
     * @var integer
34
     */
35
    private $scale;
36
37 171
    private function __construct(string $value, int $scale)
38
    {
39 171
        $this->value = $value;
40 171
        $this->scale = $scale;
41 171
    }
42
43
    private function __clone()
44
    {
45
    }
46
47
    /**
48
     * Decimal "constructor".
49
     *
50
     * @param  mixed $value
51
     * @param  int   $scale
52
     * @return Decimal
53
     */
54 9
    public static function create($value, int $scale = null): Decimal
55
    {
56 9
        if (\is_int($value)) {
57 4
            return self::fromInteger($value);
58 8
        } elseif (\is_float($value)) {
59 4
            return self::fromFloat($value, $scale);
60 4
        } elseif (\is_string($value)) {
61 2
            return self::fromString($value, $scale);
62 2
        } elseif ($value instanceof Decimal) {
63 1
            return self::fromDecimal($value, $scale);
64
        } else {
65 1
            throw new \TypeError(
66
                '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...
67 1
                (\is_object($value) ? \get_class($value) : \gettype($value))
68
            );
69
        }
70
    }
71
72 98
    public static function fromInteger(int $intValue): Decimal
73
    {
74 98
        self::paramsValidation($intValue, null);
75
76 98
        return new static((string)$intValue, 0);
77
    }
78
79
    /**
80
     * @param  float $fltValue
81
     * @param  int   $scale
82
     * @return Decimal
83
     */
84 49
    public static function fromFloat(float $fltValue, int $scale = null): Decimal
85
    {
86 49
        self::paramsValidation($fltValue, $scale);
87
88 49
        if (\is_infinite($fltValue)) {
89
            throw new InfiniteInputError('fltValue must be a finite number');
90 49
        } elseif (\is_nan($fltValue)) {
91 1
            throw new NaNInputError("fltValue can't be NaN");
92
        }
93
94 48
        $strValue = (string) $fltValue;
95 48
        $hasPoint = (false !== \strpos($strValue, '.'));
96
97 48
        if (\preg_match(self::EXP_NUM_GROUPS_NUMBER_REGEXP, $strValue, $capture)) {
98 6
            if (null === $scale) {
99 3
                $scale = ('-' === $capture['sign'])
100 2
                    ? $capture['exp'] + \strlen($capture['dec'])
101 3
                    : self::DEFAULT_SCALE;
102
            }
103 6
            $strValue = \number_format($fltValue, $scale, '.', '');
104
        } else {
105
            $naturalScale = (
106 42
                \strlen((string)\fmod($fltValue, 1.0)) - 2 - (($fltValue < 0) ? 1 : 0) + (!$hasPoint ? 1 : 0)
107
            );
108
109 42
            if (null === $scale) {
110 40
                $scale = $naturalScale;
111
            } else {
112 2
                $strValue .= ($hasPoint ? '' : '.') . \str_pad('', $scale - $naturalScale, '0');
113
            }
114
        }
115
116 48
        return new static($strValue, $scale);
117
    }
118
119
    /**
120
     * @param  string  $strValue
121
     * @param  integer $scale
122
     * @return Decimal
123
     */
124 133
    public static function fromString(string $strValue, int $scale = null): Decimal
125
    {
126 133
        self::paramsValidation($strValue, $scale);
127
128 132
        if (\preg_match(self::CLASSIC_DECIMAL_NUMBER_REGEXP, $strValue, $captures) === 1) {
129
130
            // Now it's time to strip leading zeros in order to normalize inner values
131 127
            $value = self::normalizeSign($captures[1]) . $captures[2];
132 127
            $min_scale = isset($captures[4]) ? \max(0, \strlen($captures[4]) - 1) : 0;
133
134 8
        } elseif (\preg_match(self::EXP_NOTATION_NUMBER_REGEXP, $strValue, $captures) === 1) {
135 7
            list($min_scale, $value) = self::fromExpNotationString(
136 7
                $scale,
137 7
                $captures['sign'],
138 7
                $captures['mantissa'],
139 7
                \strlen($captures['mantissa']) - 1,
140 7
                $captures['expSign'],
141 7
                (int)$captures['exp']
142
            );
143
        } else {
144 1
            throw new NaNInputError('strValue must be a number');
145
        }
146
147 131
        $scale = $scale ?? $min_scale;
148 131
        if ($scale < $min_scale) {
149 70
            $value = self::innerRound($value, $scale);
150 128
        } elseif ($min_scale < $scale) {
151 17
            $hasPoint = (false !== \strpos($value, '.'));
152 17
            $value .= ($hasPoint ? '' : '.') . \str_pad('', $scale - $min_scale, '0');
153
        }
154
155 131
        return new static($value, $scale);
156
    }
157
158
    /**
159
     * Constructs a new Decimal object based on a previous one,
160
     * but changing it's $scale property.
161
     *
162
     * @param  Decimal  $decValue
163
     * @param  null|int $scale
164
     * @return Decimal
165
     */
166 3
    public static function fromDecimal(Decimal $decValue, int $scale = null): Decimal
167
    {
168 3
        self::paramsValidation($decValue, $scale);
169
170
        // This block protect us from unnecessary additional instances
171 3
        if ($scale === null || $scale >= $decValue->scale) {
172 3
            return $decValue;
173
        }
174
175 2
        return new static(
176 2
            self::innerRound($decValue->value, $scale),
177 2
            $scale
178
        );
179
    }
180
181
    /**
182
     * Adds two Decimal objects
183
     * @param  Decimal  $b
184
     * @param  null|int $scale
185
     * @return Decimal
186
     */
187 40
    public function add(Decimal $b, int $scale = null): Decimal
188
    {
189 40
        self::paramsValidation($b, $scale);
190
191 40
        return self::fromString(
192 40
            \bcadd($this->value, $b->value, \max($this->scale, $b->scale)),
193 40
            $scale
194
        );
195
    }
196
197
    /**
198
     * Subtracts two BigNumber objects
199
     * @param  Decimal $b
200
     * @param  integer $scale
201
     * @return Decimal
202
     */
203 33
    public function sub(Decimal $b, int $scale = null): Decimal
204
    {
205 33
        self::paramsValidation($b, $scale);
206
207 33
        return self::fromString(
208 33
            \bcsub($this->value, $b->value, \max($this->scale, $b->scale)),
209 33
            $scale
210
        );
211
    }
212
213
    /**
214
     * Multiplies two BigNumber objects
215
     * @param  Decimal $b
216
     * @param  integer $scale
217
     * @return Decimal
218
     */
219 48
    public function mul(Decimal $b, int $scale = null): Decimal
220
    {
221 48
        self::paramsValidation($b, $scale);
222
223 47
        if ($b->isZero()) {
224 1
            return DecimalConstants::Zero();
225
        }
226
227 47
        return self::fromString(
228 47
            \bcmul($this->value, $b->value, $this->scale + $b->scale),
229 47
            $scale
230
        );
231
    }
232
233
    /**
234
     * Divides the object by $b .
235
     * Warning: div with $scale == 0 is not the same as
236
     *          integer division because it rounds the
237
     *          last digit in order to minimize the error.
238
     *
239
     * @param  Decimal $b
240
     * @param  integer $scale
241
     * @return Decimal
242
     */
243 65
    public function div(Decimal $b, int $scale = null): Decimal
244
    {
245 65
        self::paramsValidation($b, $scale);
246
247 65
        if ($b->isZero()) {
248 1
            throw new \DomainException("Division by zero is not allowed.");
249 65
        } elseif ($this->isZero()) {
250 1
            return DecimalConstants::Zero();
251
        } else {
252 64
            if (null !== $scale) {
253 56
                $divscale = $scale;
254
            } else {
255
                // $divscale is calculated in order to maintain a reasonable precision
256 22
                $this_abs = $this->abs();
257 22
                $b_abs    = $b->abs();
258
259
                $log10_result =
260 22
                    self::innerLog10($this_abs->value, $this_abs->scale, 1) -
261 22
                    self::innerLog10($b_abs->value, $b_abs->scale, 1);
262
263 22
                $divscale = (int)\max(
264 22
                    $this->scale + $b->scale,
265 22
                    \max(
266 22
                        self::countSignificativeDigits($this, $this_abs),
267 22
                        self::countSignificativeDigits($b, $b_abs)
268 22
                    ) - \max(\ceil($log10_result), 0),
269 22
                    \ceil(-$log10_result) + 1
270
                );
271
            }
272
273 64
            return self::fromString(
274 64
                \bcdiv($this->value, $b->value, $divscale+1), $divscale
275
            );
276
        }
277
    }
278
279
    /**
280
     * Returns the square root of this object
281
     * @param  integer $scale
282
     * @return Decimal
283
     */
284 4
    public function sqrt(int $scale = null): Decimal
285
    {
286 4
        if ($this->isNegative()) {
287 1
            throw new \DomainException(
288 1
                "Decimal can't handle square roots of negative numbers (it's only for real numbers)."
289
            );
290 3
        } elseif ($this->isZero()) {
291 1
            return DecimalConstants::Zero();
292
        }
293
294 3
        $sqrt_scale = ($scale !== null ? $scale : $this->scale);
295
296 3
        return self::fromString(
297 3
            \bcsqrt($this->value, $sqrt_scale+1),
298 3
            $sqrt_scale
299
        );
300
    }
301
302
    /**
303
     * Powers this value to $b
304
     *
305
     * @param  Decimal  $b      exponent
306
     * @param  integer  $scale
307
     * @return Decimal
308
     */
309 10
    public function pow(Decimal $b, int $scale = null): Decimal
310
    {
311 10
        if ($this->isZero()) {
312 2
            if ($b->isPositive()) {
313 1
                return Decimal::fromDecimal($this, $scale);
314
            } else {
315 1
                throw new \DomainException("zero can't be powered to zero or negative numbers.");
316
            }
317 8
        } elseif ($b->isZero()) {
318 1
            return DecimalConstants::One();
319 7
        } else if ($b->isNegative()) {
320 2
            return DecimalConstants::One()->div(
321 2
                $this->pow($b->additiveInverse(), max($scale, self::DEFAULT_SCALE)),
322 2
                max($scale, self::DEFAULT_SCALE)
323
            );
324 7
        } elseif (0 === $b->scale) {
325 4
            $pow_scale = \max($this->scale, $b->scale, $scale ?? 0);
326
327 4
            return self::fromString(
328 4
                \bcpow($this->value, $b->value, $pow_scale+1),
329 4
                $pow_scale
330
            );
331
        } else {
332 4
            if ($this->isPositive()) {
333 3
                $pow_scale = \max($this->scale, $b->scale, $scale ?? 0);
334
335 3
                $truncated_b = \bcadd($b->value, '0', 0);
336 3
                $remaining_b = \bcsub($b->value, $truncated_b, $b->scale);
337
338 3
                $first_pow_approx = \bcpow($this->value, $truncated_b, $pow_scale+1);
339 3
                $intermediate_root = self::innerPowWithLittleExponent(
340 3
                    $this->value,
341 3
                    $remaining_b,
342 3
                    $b->scale,
343 3
                    $pow_scale+1
344
                );
345
346 3
                return Decimal::fromString(
347 3
                    \bcmul($first_pow_approx, $intermediate_root, $pow_scale+1),
348 3
                    $pow_scale
349
                );
350
            } else { // elseif ($this->isNegative())
351 1
                if (!$b->isInteger()) {
352 1
                    throw new NotImplementedError(
353
                        "Usually negative numbers can't be powered to non integer numbers. " .
354 1
                        "The cases where is possible are not implemented."
355
                    );
356
                }
357
358
                return (\preg_match('/^[+\-]?[0-9]*[02468](\.0+)?$/', $b->value, $captures) === 1)
359
                    ? $this->additiveInverse()->pow($b, $scale)                      // $b is an even number
360
                    : $this->additiveInverse()->pow($b, $scale)->additiveInverse();  // $b is an odd number
361
            }
362
        }
363
    }
364
365
    /**
366
     * Returns the object's logarithm in base 10
367
     * @param  integer $scale
368
     * @return Decimal
369
     */
370 7
    public function log10(int $scale = null): Decimal
371
    {
372 7
        if ($this->isNegative()) {
373 1
            throw new \DomainException(
374 1
                "Decimal can't handle logarithms of negative numbers (it's only for real numbers)."
375
            );
376 6
        } elseif ($this->isZero()) {
377 1
            throw new \DomainException(
378 1
                "Decimal can't represent infinite numbers."
379
            );
380
        }
381
382 5
        return self::fromString(
383 5
            self::innerLog10($this->value, $this->scale, $scale !== null ? $scale+1 : $this->scale+1),
384 5
            $scale
385
        );
386
    }
387
388 96
    public function isZero(int $scale = null): bool
389
    {
390 96
        $cmp_scale = $scale !== null ? $scale : $this->scale;
391
392 96
        return (\bccomp(self::innerRound($this->value, $cmp_scale), '0', $cmp_scale) === 0);
393
    }
394
395 31
    public function isPositive(): bool
396
    {
397 31
        return ($this->value[0] !== '-' && !$this->isZero());
398
    }
399
400 71
    public function isNegative(): bool
401
    {
402 71
        return ($this->value[0] === '-');
403
    }
404
405 3
    public function isInteger(): bool
406
    {
407 3
        return (\preg_match('/^[+\-]?[0-9]+(\.0+)?$/', $this->value, $captures) === 1);
408
    }
409
410
    /**
411
     * Equality comparison between this object and $b
412
     * @param  Decimal $b
413
     * @param integer $scale
414
     * @return boolean
415
     */
416 115
    public function equals(Decimal $b, int $scale = null): bool
417
    {
418 115
        self::paramsValidation($b, $scale);
419
420 115
        if ($this === $b) {
421 2
            return true;
422
        } else {
423 114
            $cmp_scale = $scale !== null ? $scale : \max($this->scale, $b->scale);
424
425
            return (
426 114
                \bccomp(
427 114
                    self::innerRound($this->value, $cmp_scale),
428 114
                    self::innerRound($b->value, $cmp_scale),
429 114
                    $cmp_scale
430 114
                ) === 0
431
            );
432
        }
433
    }
434
435
    /**
436
     * $this > $b : returns 1 , $this < $b : returns -1 , $this == $b : returns 0
437
     *
438
     * @param  Decimal $b
439
     * @param  integer $scale
440
     * @return integer
441
     */
442 56
    public function comp(Decimal $b, int $scale = null): int
443
    {
444 56
        self::paramsValidation($b, $scale);
445
446 56
        if ($this === $b) {
447 7
            return 0;
448
        }
449
450 55
        $cmp_scale = $scale !== null ? $scale : \max($this->scale, $b->scale);
451
452 55
        return \bccomp(
453 55
            self::innerRound($this->value, $cmp_scale),
454 55
            self::innerRound($b->value, $cmp_scale),
455 55
            $cmp_scale
456
        );
457
    }
458
459
460
    /**
461
     * Returns true if $this > $b, otherwise false
462
     *
463
     * @param  Decimal $b
464
     * @param  integer $scale
465
     * @return bool
466
     */
467 3
    public function isGreaterThan(Decimal $b, int $scale = null): bool
468
    {
469 3
        return $this->comp($b, $scale) === 1;
470
    }
471
472
    /**
473
     * Returns true if $this >= $b
474
     *
475
     * @param  Decimal $b
476
     * @param  integer $scale
477
     * @return bool
478
     */
479 3
    public function isGreaterOrEqualTo(Decimal $b, int $scale = null): bool
480
    {
481 3
        $comparisonResult = $this->comp($b, $scale);
482
483 3
        return $comparisonResult === 1 || $comparisonResult === 0;
484
    }
485
486
    /**
487
     * Returns true if $this < $b, otherwise false
488
     *
489
     * @param  Decimal $b
490
     * @param  integer $scale
491
     * @return bool
492
     */
493 3
    public function isLessThan(Decimal $b, int $scale = null): bool
494
    {
495 3
        return $this->comp($b, $scale) === -1;
496
    }
497
498
    /**
499
     * Returns true if $this <= $b, otherwise false
500
     *
501
     * @param  Decimal $b
502
     * @param  integer $scale
503
     * @return bool
504
     */
505 3
    public function isLessOrEqualTo(Decimal $b, int $scale = null): bool
506
    {
507 3
        $comparisonResult = $this->comp($b, $scale);
508
509 3
        return $comparisonResult === -1 || $comparisonResult === 0;
510
    }
511
512
    /**
513
     * Returns the element's additive inverse.
514
     * @return Decimal
515
     */
516 15
    public function additiveInverse(): Decimal
517
    {
518 15
        if ($this->isZero()) {
519 1
            return $this;
520 14
        } elseif ($this->isNegative()) {
521 12
            $value = \substr($this->value, 1);
522
        } else { // if ($this->isPositive()) {
523 2
            $value = '-' . $this->value;
524
        }
525
526 14
        return new static($value, $this->scale);
527
    }
528
529
530
    /**
531
     * "Rounds" the Decimal to have at most $scale digits after the point
532
     * @param  integer $scale
533
     * @return Decimal
534
     */
535 52
    public function round(int $scale = 0): Decimal
536
    {
537 52
        if ($scale >= $this->scale) {
538 21
            return $this;
539
        }
540
541 51
        return self::fromString(self::innerRound($this->value, $scale));
542
    }
543
544
    /**
545
     * "Ceils" the Decimal to have at most $scale digits after the point
546
     * @param  integer $scale
547
     * @return Decimal
548
     */
549 4
    public function ceil($scale = 0): Decimal
550
    {
551 4
        if ($scale >= $this->scale) {
552 2
            return $this;
553
        }
554
555 3
        if ($this->isNegative()) {
556 1
            return self::fromString(\bcadd($this->value, '0', $scale));
557
        }
558
559 2
        return $this->innerTruncate($scale);
560
    }
561
562 28
    private function innerTruncate(int $scale = 0, bool $ceil = true): Decimal
563
    {
564 28
        $rounded = \bcadd($this->value, '0', $scale);
565
566 28
        $rlen = \strlen($rounded);
567 28
        $tlen = \strlen($this->value);
568
569 28
        $mustTruncate = false;
570 28
        for ($i=$tlen-1; $i >= $rlen; $i--) {
571 28
            if ((int)$this->value[$i] > 0) {
572 28
                $mustTruncate = true;
573 28
                break;
574
            }
575
        }
576
577 28
        if ($mustTruncate) {
578 28
            $rounded = $ceil
579 2
                ? \bcadd($rounded, \bcpow('10', (string)-$scale, $scale), $scale)
580 28
                : \bcsub($rounded, \bcpow('10', (string)-$scale, $scale), $scale);
581
        }
582
583 28
        return self::fromString($rounded, $scale);
584
    }
585
586
    /**
587
     * "Floors" the Decimal to have at most $scale digits after the point
588
     * @param  integer $scale
589
     * @return Decimal
590
     */
591 47
    public function floor(int $scale = 0): Decimal
592
    {
593 47
        if ($scale >= $this->scale) {
594 38
            return $this;
595
        }
596
597 38
        if ($this->isNegative()) {
598 26
            return $this->innerTruncate($scale, false);
599
        }
600
601 35
        return self::fromString(\bcadd($this->value, '0', $scale));
602
    }
603
604
    /**
605
     * Returns the absolute value (always a positive number)
606
     * @return Decimal
607
     */
608 23
    public function abs(): Decimal
609
    {
610 23
        return ($this->isZero() || $this->isPositive())
611 21
            ? $this
612 23
            : $this->additiveInverse();
613
    }
614
615
    /**
616
     * Calculate modulo with a decimal
617
     * @param Decimal $d
618
     * @param integer $scale
619
     * @return $this % $d
620
     */
621 27
    public function mod(Decimal $d, int $scale = null): Decimal
622
    {
623 27
        $div = $this->div($d, 1)->floor();
624 27
        return $this->sub($div->mul($d), $scale);
625
    }
626
627
    /**
628
     * Calculates the sine of this method with the highest possible accuracy
629
     * Note that accuracy is limited by the accuracy of predefined PI;
630
     *
631
     * @param integer $scale
632
     * @return Decimal sin($this)
633
     */
634 13
    public function sin(int $scale = null): Decimal
635
    {
636
        // First normalise the number in the [0, 2PI] domain
637 13
        $x = $this->mod(DecimalConstants::PI()->mul(Decimal::fromString("2")));
638
639
        // PI has only 32 significant numbers
640 13
        $scale = (null === $scale) ? 32 : $scale;
641
642 13
        return self::factorialSerie(
643 13
            $x,
644 13
            DecimalConstants::zero(),
645 13
            function ($i) {
646 13
                return ($i % 2 === 1) ? (
647 13
                ($i % 4 === 1) ? DecimalConstants::one() : DecimalConstants::negativeOne()
648 13
                ) : DecimalConstants::zero();
649 13
            },
650 13
            $scale
651
        );
652
    }
653
654
    /**
655
     * Calculates the cosecant of this with the highest possible accuracy
656
     * Note that accuracy is limited by the accuracy of predefined PI;
657
     *
658
     * @param integer $scale
659
     * @return Decimal
660
     */
661 3
    public function cosec(int $scale = null): Decimal
662
    {
663 3
        $sin = $this->sin($scale + 2);
664 3
        if ($sin->isZero()) {
665
            throw new \DomainException(
666
                "The cosecant of this 'angle' is undefined."
667
            );
668
        }
669
670 3
        return DecimalConstants::one()->div($sin)->round($scale);
671
    }
672
673
    /**
674
     * Calculates the cosine of this method with the highest possible accuracy
675
     * Note that accuracy is limited by the accuracy of predefined PI;
676
     *
677
     * @param integer $scale
678
     * @return Decimal cos($this)
679
     */
680 13
    public function cos(int $scale = null): Decimal
681
    {
682
        // First normalise the number in the [0, 2PI] domain
683 13
        $x = $this->mod(DecimalConstants::PI()->mul(Decimal::fromString("2")));
684
685
        // PI has only 32 significant numbers
686 13
        $scale = ($scale === null) ? 32 : $scale;
687
688 13
        return self::factorialSerie(
689 13
            $x,
690 13
            DecimalConstants::one(),
691 13
            function ($i) {
692 13
                return ($i % 2 === 0) ? (
693 13
                    ($i % 4 === 0) ? DecimalConstants::one() : DecimalConstants::negativeOne()
694 13
                ) : DecimalConstants::zero();
695 13
            },
696 13
            $scale
697
        );
698
    }
699
700
    /**
701
     * Calculates the secant of this with the highest possible accuracy
702
     * Note that accuracy is limited by the accuracy of predefined PI;
703
     *
704
     * @param integer $scale
705
     * @return Decimal
706
     */
707 3
    public function sec(int $scale = null): Decimal
708
    {
709 3
        $cos = $this->cos($scale + 2);
710 3
        if ($cos->isZero()) {
711
            throw new \DomainException(
712
                "The secant of this 'angle' is undefined."
713
            );
714
        }
715
716 3
        return DecimalConstants::one()->div($cos)->round($scale);
717
    }
718
719
    /**
720
     *	Calculates the arcsine of this with the highest possible accuracy
721
     *
722
     * @param integer $scale
723
     * @return Decimal
724
     */
725 5
    public function arcsin(int $scale = null): Decimal
726
    {
727 5
        if($this->comp(DecimalConstants::one(), $scale + 2) === 1 || $this->comp(DecimalConstants::negativeOne(), $scale + 2) === -1) {
728 2
            throw new \DomainException(
729 2
                "The arcsin of this number is undefined."
730
            );
731
        }
732
733 3
        if ($this->round($scale)->isZero()) {
734
            return DecimalConstants::zero();
735
        }
736 3
        if ($this->round($scale)->equals(DecimalConstants::one())) {
737 1
            return DecimalConstants::pi()->div(Decimal::fromInteger(2))->round($scale);
738
        }
739 2
        if ($this->round($scale)->equals(DecimalConstants::negativeOne())) {
740 1
            return DecimalConstants::pi()->div(Decimal::fromInteger(-2))->round($scale);
741
        }
742
743 1
        $scale = ($scale === null) ? 32 : $scale;
744
745 1
        return self::powerSerie(
746 1
            $this,
747 1
            DecimalConstants::zero(),
748 1
            $scale
749
        );
750
    }
751
752
    /**
753
     *	Calculates the arccosine of this with the highest possible accuracy
754
     *
755
     * @param integer $scale
756
     * @return Decimal
757
     */
758 5
    public function arccos(int $scale = null): Decimal
759
    {
760 5
        if($this->comp(DecimalConstants::one(), $scale + 2) === 1 || $this->comp(DecimalConstants::negativeOne(), $scale + 2) === -1) {
761 2
            throw new \DomainException(
762 2
                "The arccos of this number is undefined."
763
            );
764
        }
765
766 3
        $piOverTwo = DecimalConstants::pi()->div(Decimal::fromInteger(2), $scale + 2)->round($scale);
767
768 3
        if ($this->round($scale)->isZero()) {
769
            return $piOverTwo;
770
        }
771 3
        if ($this->round($scale)->equals(DecimalConstants::one())) {
772 1
            return DecimalConstants::zero();
773
        }
774 2
        if ($this->round($scale)->equals(DecimalConstants::negativeOne())) {
775 1
            return DecimalConstants::pi()->round($scale);
776
        }
777
778 1
        $scale = ($scale === null) ? 32 : $scale;
779
780 1
        return $piOverTwo->sub(
781 1
            self::powerSerie(
782 1
                $this,
783 1
                DecimalConstants::zero(),
784 1
                $scale
785
            )
786 1
        )->round($scale);
787
    }
788
789
    /**
790
     *	Calculates the arctangente of this with the highest possible accuracy
791
     *
792
     * @param integer $scale
793
     * @return Decimal
794
     */
795 3
    public function arctan(int $scale = null): Decimal
796
    {
797 3
        $piOverFour = DecimalConstants::pi()->div(Decimal::fromInteger(4), $scale + 2)->round($scale);
798
799 3
        if ($this->round($scale)->isZero()) {
800 1
            return DecimalConstants::zero();
801
        }
802 2
        if ($this->round($scale)->equals(DecimalConstants::one())) {
803
            return $piOverFour;
804
        }
805 2
        if ($this->round($scale)->equals(DecimalConstants::negativeOne())) {
806 1
            return DecimalConstants::negativeOne()->mul($piOverFour);
807
        }
808
809 1
        $scale = ($scale === null) ? 32 : $scale;
810
811 1
        return self::simplePowerSerie(
812 1
            $this,
813 1
            DecimalConstants::zero(),
814 1
            $scale + 2
815 1
        )->round($scale);
816
    }
817
818
    /**
819
     * Calculates the arccotangente of this with the highest possible accuracy
820
     *
821
     * @param integer $scale
822
     * @return Decimal
823
     */
824 3
    public function arccot(int $scale = null): Decimal
825
    {
826 3
        $scale = ($scale === null) ? 32 : $scale;
827
828 3
        $piOverTwo = DecimalConstants::pi()->div(Decimal::fromInteger(2), $scale + 2);
829 3
        if ($this->round($scale)->isZero()) {
830 1
            return $piOverTwo->round($scale);
831
        }
832
833 2
        $piOverFour = DecimalConstants::pi()->div(Decimal::fromInteger(4), $scale + 2);
834 2
        if ($this->round($scale)->equals(DecimalConstants::one())) {
835
            return $piOverFour->round($scale);
836
        }
837 2
        if ($this->round($scale)->equals(DecimalConstants::negativeOne())) {
838 1
            return DecimalConstants::negativeOne()->mul($piOverFour, $scale + 2)->round($scale);
839
        }
840
841 1
        return $piOverTwo->sub(
842 1
            self::simplePowerSerie(
843 1
                $this,
844 1
                DecimalConstants::zero(),
845 1
                $scale + 2
846
            )
847 1
        )->round($scale);
848
    }
849
850
    /**
851
     * Calculates the arcsecant of this with the highest possible accuracy
852
     *
853
     * @param integer $scale
854
     * @return Decimal
855
     */
856 5
    public function arcsec(int $scale = null): Decimal
857
    {
858 5
        if($this->comp(DecimalConstants::one(), $scale + 2) === -1 && $this->comp(DecimalConstants::negativeOne(), $scale + 2) === 1) {
859 1
            throw new \DomainException(
860 1
                "The arcsecant of this number is undefined."
861
            );
862
        }
863
864 4
        $piOverTwo = DecimalConstants::pi()->div(Decimal::fromInteger(2), $scale + 2)->round($scale);
865
866 4
        if ($this->round($scale)->equals(DecimalConstants::one())) {
867 1
            return DecimalConstants::zero();
868
        }
869 3
        if ($this->round($scale)->equals(DecimalConstants::negativeOne())) {
870 1
            return DecimalConstants::pi()->round($scale);
871
        }
872
873 2
        $scale = ($scale === null) ? 32 : $scale;
874
875 2
        return $piOverTwo->sub(
876 2
            self::powerSerie(
877 2
                DecimalConstants::one()->div($this, $scale + 2),
878 2
                DecimalConstants::zero(),
879 2
                $scale + 2
880
            )
881 2
        )->round($scale);
882
    }
883
884
    /**
885
     * Calculates the arccosecant of this with the highest possible accuracy
886
     *
887
     * @param integer $scale
888
     * @return Decimal
889
     */
890 5
    public function arccsc(int $scale = null): Decimal
891
    {
892 5
        if($this->comp(DecimalConstants::one(), $scale + 2) === -1 && $this->comp(DecimalConstants::negativeOne(), $scale + 2) === 1) {
893 1
            throw new \DomainException(
894 1
                "The arccosecant of this number is undefined."
895
            );
896
        }
897
898 4
        $scale = ($scale === null) ? 32 : $scale;
899
900 4
        if ($this->round($scale)->equals(DecimalConstants::one())) {
901 1
            return DecimalConstants::pi()->div(Decimal::fromInteger(2), $scale + 2)->round($scale);
902
        }
903 3
        if ($this->round($scale)->equals(DecimalConstants::negativeOne())) {
904 1
            return DecimalConstants::pi()->div(Decimal::fromInteger(-2), $scale + 2)->round($scale);
905
        }
906
907 2
        return self::powerSerie(
908 2
            DecimalConstants::one()->div($this, $scale + 2),
909 2
            DecimalConstants::zero(),
910 2
            $scale + 2
911 2
        )->round($scale);
912
    }
913
914
    /**
915
     * Returns exp($this), said in other words: e^$this .
916
     *
917
     * @param integer $scale
918
     * @return Decimal
919
     */
920 11
    public function exp(int $scale = null): Decimal
921
    {
922 11
        if ($this->isZero()) {
923 3
            return DecimalConstants::one();
924
        }
925
926 8
        $scale = $scale ?? \max(
927
            $this->scale,
928 8
            (int)($this->isNegative() ? self::innerLog10($this->value, $this->scale, 0) : self::DEFAULT_SCALE)
929
        );
930
931 8
        return self::factorialSerie(
932
            $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...
933
        );
934
    }
935
936
    /**
937
     * Internal method used to compute sin, cos and exp
938
     *
939
     * @param Decimal $x
940
     * @param Decimal $firstTerm
941
     * @param callable $generalTerm
942
     * @param $scale
943
     * @return Decimal
944
     */
945 28
    private static function factorialSerie (Decimal $x, Decimal $firstTerm, callable $generalTerm, int $scale): Decimal
946
    {
947 28
        $approx = $firstTerm;
948 28
        $change = DecimalConstants::One();
949
950 28
        $faculty = DecimalConstants::One();    // Calculates the faculty under the sign
951 28
        $xPowerN = DecimalConstants::One();    // Calculates x^n
952
953 28
        for ($i = 1; !$change->floor($scale+1)->isZero(); $i++) {
954
            // update x^n and n! for this walkthrough
955 28
            $xPowerN = $xPowerN->mul($x);
956 28
            $faculty = $faculty->mul(Decimal::fromInteger($i));
957
958
            /** @var Decimal $multiplier */
959 28
            $multiplier = $generalTerm($i);
960
961 28
            if (!$multiplier->isZero()) {
962 28
                $change = $multiplier->mul($xPowerN, $scale + 2)->div($faculty, $scale + 2);
963 28
                $approx = $approx->add($change, $scale + 2);
964
            }
965
        }
966
967 28
        return $approx->round($scale);
968
    }
969
970
971
    /**
972
     * Internal method used to compute arcsine and arcosine
973
     *
974
     * @param Decimal $x
975
     * @param Decimal $firstTerm
976
     * @param $scale
977
     * @return Decimal
978
     */
979 6
    private static function powerSerie (Decimal $x, Decimal $firstTerm, int $scale): Decimal
980
    {
981 6
        $approx = $firstTerm;
982 6
        $change = DecimalConstants::One();
983
984 6
        $xPowerN = DecimalConstants::One();     // Calculates x^n
985 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...
986
987 6
        $numerator = DecimalConstants::one();
988 6
        $denominator = DecimalConstants::one();
989
990 6
        for ($i = 1; !$change->floor($scale + 2)->isZero(); $i++) {
991 6
            $xPowerN = $xPowerN->mul($x);
992
993 6
            if ($i % 2 === 0) {
994 6
                $factorN = DecimalConstants::zero();
995 6
            } elseif ($i === 1) {
996 6
                $factorN = DecimalConstants::one();
997
            } else {
998 6
                $incrementNum = Decimal::fromInteger($i - 2);
999 6
                $numerator = $numerator->mul($incrementNum, $scale +2);
1000
1001 6
                $incrementDen = Decimal::fromInteger($i - 1);
1002 6
                $increment = Decimal::fromInteger($i);
1003
                $denominator = $denominator
1004 6
                    ->div($incrementNum, $scale +2)
1005 6
                    ->mul($incrementDen, $scale +2)
1006 6
                    ->mul($increment, $scale +2);
1007
1008 6
                $factorN = $numerator->div($denominator, $scale + 2);
1009
            }
1010
1011 6
            if (!$factorN->isZero()) {
1012 6
                $change = $factorN->mul($xPowerN, $scale + 2);
1013 6
                $approx = $approx->add($change, $scale + 2);
1014
            }
1015
        }
1016
1017 6
        return $approx->round($scale);
1018
    }
1019
1020
    /**
1021
     * Internal method used to compute arctan and arccotan
1022
     *
1023
     * @param Decimal $x
1024
     * @param Decimal $firstTerm
1025
     * @param $scale
1026
     * @return Decimal
1027
     */
1028 2
    private static function simplePowerSerie (Decimal $x, Decimal $firstTerm, int $scale): Decimal
1029
    {
1030 2
        $approx = $firstTerm;
1031 2
        $change = DecimalConstants::One();
1032
1033 2
        $xPowerN = DecimalConstants::One();     // Calculates x^n
1034 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...
1035
1036 2
        for ($i = 1; !$change->floor($scale + 2)->isZero(); $i++) {
1037 2
            $xPowerN = $xPowerN->mul($x);
1038
1039 2
            if ($i % 2 === 0) {
1040 2
                $factorN = DecimalConstants::zero();
1041
            } else {
1042 2
                 if ($i % 4 === 1) {
1043 2
                     $factorN = DecimalConstants::one()->div(Decimal::fromInteger($i), $scale + 2);
1044
                 } else {
1045 2
                     $factorN = DecimalConstants::negativeOne()->div(Decimal::fromInteger($i), $scale + 2);
1046
                 }
1047
            }
1048
1049 2
            if (!$factorN->isZero()) {
1050 2
                $change = $factorN->mul($xPowerN, $scale + 2);
1051 2
                $approx = $approx->add($change, $scale + 2);
1052
            }
1053
        }
1054
1055 2
        return $approx->round($scale);
1056
    }
1057
1058
    /**
1059
     * Calculates the tangent of this method with the highest possible accuracy
1060
     * Note that accuracy is limited by the accuracy of predefined PI;
1061
     *
1062
     * @param integer $scale
1063
     * @return Decimal tan($this)
1064
     */
1065 4
    public function tan(int $scale = null): Decimal
1066
    {
1067 4
	    $cos = $this->cos($scale + 2);
1068 4
	    if ($cos->isZero()) {
1069 1
	        throw new \DomainException(
1070 1
	            "The tangent of this 'angle' is undefined."
1071
	        );
1072
	    }
1073
1074 3
	    return $this->sin($scale + 2)->div($cos)->round($scale);
1075
    }
1076
1077
    /**
1078
     * Calculates the cotangent of this method with the highest possible accuracy
1079
     * Note that accuracy is limited by the accuracy of predefined PI;
1080
     *
1081
     * @param integer $scale
1082
     * @return Decimal cotan($this)
1083
     */
1084 4
    public function cotan(int $scale = null): Decimal
1085
    {
1086 4
	    $sin = $this->sin($scale + 2);
1087 4
	    if ($sin->isZero()) {
1088 1
	        throw new \DomainException(
1089 1
	            "The cotangent of this 'angle' is undefined."
1090
	        );
1091
	    }
1092
1093 3
	    return $this->cos($scale + 2)->div($sin)->round($scale);
1094
    }
1095
1096
    /**
1097
     * Indicates if the passed parameter has the same sign as the method's bound object.
1098
     *
1099
     * @param Decimal $b
1100
     * @return bool
1101
     */
1102
    public function hasSameSign(Decimal $b): bool
1103
    {
1104
        return $this->isPositive() && $b->isPositive() || $this->isNegative() && $b->isNegative();
1105
    }
1106
1107 5
    public function asFloat(): float
1108
    {
1109 5
        return \floatval($this->value);
1110
    }
1111
1112 1
    public function asInteger(): int
1113
    {
1114 1
        return \intval($this->value);
1115
    }
1116
1117
    /**
1118
     * WARNING: use with caution! Return the inner representation of the class.
1119
     */
1120 11
    public function innerValue(): string
1121
    {
1122 11
        return $this->value;
1123
    }
1124
1125
    /**
1126
     * @return string
1127
     */
1128 58
    public function __toString(): string
1129
    {
1130 58
        return $this->value;
1131
    }
1132
1133
    /**
1134
     * @return string
1135
     */
1136
    public function jsonSerialize(): string
1137
    {
1138
        return $this->value;
1139
    }
1140
1141
    /*
1142
     *
1143
     */
1144 7
    private static function fromExpNotationString(
1145
        int $scale = null,
1146
        string $sign,
1147
        string $mantissa,
1148
        int $nDecimals,
1149
        string $expSign,
1150
        int $expVal
1151
    ): array
1152
    {
1153 7
        $mantissaScale = \max($nDecimals, 0);
1154
1155 7
        if (self::normalizeSign($expSign) === '') {
1156 5
            $minScale = \max($mantissaScale - $expVal, 0);
1157 5
            $tmp_multiplier = \bcpow('10', (string)$expVal);
1158
        } else {
1159 2
            $minScale = $mantissaScale + $expVal;
1160 2
            $tmp_multiplier = \bcpow('10', (string)-$expVal, $expVal);
1161
        }
1162
1163
        $value = (
1164 7
            self::normalizeSign($sign) .
1165 7
            \bcmul(
1166 7
                $mantissa,
1167 7
                $tmp_multiplier,
1168 7
                \max($minScale, $scale ?? 0)
1169
            )
1170
        );
1171
1172 7
        return [$minScale, $value];
1173
    }
1174
1175
    /**
1176
     * "Rounds" the decimal string to have at most $scale digits after the point
1177
     *
1178
     * @param  string $value
1179
     * @param  int    $scale
1180
     * @return string
1181
     */
1182 150
    private static function innerRound(string $value, int $scale = 0): string
1183
    {
1184 150
        $rounded = \bcadd($value, '0', $scale);
1185
1186 150
        $diffDigit = \bcsub($value, $rounded, $scale+1);
1187 150
        $diffDigit = (int)$diffDigit[\strlen($diffDigit)-1];
1188
1189 150
        if ($diffDigit >= 5) {
1190 68
            $rounded = ($diffDigit >= 5 && $value[0] !== '-')
1191 64
                ? \bcadd($rounded, \bcpow('10', (string)-$scale, $scale), $scale)
1192 68
                : \bcsub($rounded, \bcpow('10', (string)-$scale, $scale), $scale);
1193
        }
1194
1195 150
        return $rounded;
1196
    }
1197
1198
    /**
1199
     * Calculates the logarithm (in base 10) of $value
1200
     *
1201
     * @param  string $value     The number we want to calculate its logarithm (only positive numbers)
1202
     * @param  int    $in_scale  Expected scale used by $value (only positive numbers)
1203
     * @param  int    $out_scale Scale used by the return value (only positive numbers)
1204
     * @return string
1205
     */
1206 25
    private static function innerLog10(string $value, int $in_scale, int $out_scale): string
1207
    {
1208 25
        $value_len = \strlen($value);
1209
1210 25
        $cmp = \bccomp($value, '1', $in_scale);
1211
1212
        switch ($cmp) {
1213 25
            case 1:
1214 12
                $value_log10_approx = $value_len - ($in_scale > 0 ? ($in_scale+2) : 1);
1215 12
                $value_log10_approx = max(0, $value_log10_approx);
1216
1217 12
                return \bcadd(
1218 12
                    (string)$value_log10_approx,
1219 12
                    (string)\log10((float)\bcdiv(
1220 12
                        $value,
1221 12
                        \bcpow('10', (string)$value_log10_approx),
1222 12
                        \min($value_len, $out_scale)
1223
                    )),
1224 12
                    $out_scale
1225
                );
1226 15
            case -1:
1227 14
                \preg_match('/^0*\.(0*)[1-9][0-9]*$/', $value, $captures);
1228 14
                $value_log10_approx = -\strlen($captures[1])-1;
1229
1230 14
                return \bcadd(
1231 14
                    (string)$value_log10_approx,
1232 14
                    (string)\log10((float)\bcmul(
1233 14
                        $value,
1234 14
                        \bcpow('10', (string)-$value_log10_approx),
1235 14
                        $in_scale + $value_log10_approx
1236
                    )),
1237 14
                    $out_scale
1238
                );
1239
            default: // case 0:
1240 7
                return '0';
1241
        }
1242
    }
1243
1244
    /**
1245
     * Returns $base^$exponent
1246
     *
1247
     * @param  string $base
1248
     * @param  string $exponent   0 < $exponent < 1
1249
     * @param  int    $exp_scale Number of $exponent's significative digits
1250
     * @param  int    $out_scale Number of significative digits that we want to compute
1251
     * @return string
1252
     */
1253 3
    private static function innerPowWithLittleExponent(
1254
        string $base,
1255
        string $exponent,
1256
        int $exp_scale,
1257
        int $out_scale
1258
    ): string
1259
    {
1260 3
        $inner_scale = (int)\ceil($exp_scale * \log(10) / \log(2)) + 1;
1261
1262 3
        $result_a = '1';
1263 3
        $result_b = '0';
1264
1265 3
        $actual_index = 0;
1266 3
        $exponent_remaining = $exponent;
1267
1268 3
        while (\bccomp($result_a, $result_b, $out_scale) !== 0 && \bccomp($exponent_remaining, '0', $inner_scale) !== 0) {
1269 3
            $result_b = $result_a;
1270 3
            $index_info = self::computeSquareIndex($exponent_remaining, $actual_index, $exp_scale, $inner_scale);
1271 3
            $exponent_remaining = $index_info[1];
1272 3
            $result_a = \bcmul(
1273 3
                $result_a,
1274 3
                self::compute2NRoot($base, $index_info[0], 2*($out_scale+1)),
1275 3
                2*($out_scale+1)
1276
            );
1277
        }
1278
1279 3
        return self::innerRound($result_a, $out_scale);
1280
    }
1281
1282
    /**
1283
     * Auxiliar method. It helps us to decompose the exponent into many summands.
1284
     *
1285
     * @param  string $exponent_remaining
1286
     * @param  int    $actual_index
1287
     * @param  int    $exp_scale           Number of $exponent's significative digits
1288
     * @param  int    $inner_scale         ceil($exp_scale*log(10)/log(2))+1;
1289
     * @return array
1290
     */
1291 3
    private static function computeSquareIndex(
1292
        string $exponent_remaining,
1293
        int $actual_index,
1294
        int $exp_scale,
1295
        int $inner_scale
1296
    ): array
1297
    {
1298 3
        $actual_rt = \bcpow('0.5', (string)$actual_index, $exp_scale);
1299 3
        $r = \bcsub($exponent_remaining, $actual_rt, $inner_scale);
1300
1301 3
        while (\bccomp($r, '0', $exp_scale) === -1) {
1302 3
            ++$actual_index;
1303 3
            $actual_rt = \bcmul('0.5', $actual_rt, $inner_scale);
1304 3
            $r = \bcsub($exponent_remaining, $actual_rt, $inner_scale);
1305
        }
1306
1307 3
        return [$actual_index, $r];
1308
    }
1309
1310
    /**
1311
     * Auxiliar method. Computes $base^((1/2)^$index)
1312
     *
1313
     * @param  string  $base
1314
     * @param  integer $index
1315
     * @param  integer $out_scale
1316
     * @return string
1317
     */
1318 3
    private static function compute2NRoot(string $base, int $index, int $out_scale): string
1319
    {
1320 3
        $result = $base;
1321
1322 3
        for ($i = 0; $i < $index; $i++) {
1323 3
            $result = \bcsqrt($result, ($out_scale + 1) * ($index - $i) + 1);
1324
        }
1325
1326 3
        return self::innerRound($result, $out_scale);
1327
    }
1328
1329
    /**
1330
     * Validates basic constructor's arguments
1331
     * @param  mixed    $value
1332
     * @param  null|int  $scale
1333
     */
1334 174
    protected static function paramsValidation($value, int $scale = null)
1335
    {
1336 174
        if (null === $value) {
1337
            throw new \InvalidArgumentException('$value must be a non null number');
1338
        }
1339
1340 174
        if (null !== $scale && $scale < 0) {
1341 2
            throw new \InvalidArgumentException('$scale must be a positive integer');
1342
        }
1343 173
    }
1344
1345
    /**
1346
     * @return string
1347
     */
1348 131
    private static function normalizeSign(string $sign): string
1349
    {
1350 131
        if ('+' === $sign) {
1351 4
            return '';
1352
        }
1353
1354 131
        return $sign;
1355
    }
1356
1357
    /**
1358
     * Counts the number of significant digits of $val.
1359
     * Assumes a consistent internal state (without zeros at the end or the start).
1360
     *
1361
     * @param  Decimal $val
1362
     * @param  Decimal $abs $val->abs()
1363
     * @return int
1364
     */
1365 22
    private static function countSignificativeDigits(Decimal $val, Decimal $abs): int
1366
    {
1367 22
        return \strlen($val->value) - (
1368 22
            ($abs->comp(DecimalConstants::One()) === -1) ? 2 : \max($val->scale, 1)
1369 22
        ) - ($val->isNegative() ? 1 : 0);
1370
    }
1371
}
1372