Completed
Push — master ( 9028f9...16ba57 )
by Andreu
02:49
created

Decimal   D

Complexity

Total Complexity 188

Size/Duplication

Total Lines 1292
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 96.6%

Importance

Changes 17
Bugs 1 Features 1
Metric Value
wmc 188
c 17
b 1
f 1
lcom 1
cbo 4
dl 0
loc 1292
ccs 511
cts 529
cp 0.966
rs 4.4102

57 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A __clone() 0 3 1
A fromInteger() 0 6 1
A fromDecimal() 0 14 3
A add() 0 9 1
A sub() 0 9 1
A mul() 0 13 2
B div() 0 35 4
A sqrt() 0 17 4
A log10() 0 17 4
A isZero() 0 6 2
A isPositive() 0 4 2
A isNegative() 0 4 1
A isInteger() 0 4 1
A equals() 0 18 3
A comp() 0 16 3
A additiveInverse() 0 12 3
A round() 0 8 2
A ceil() 0 12 3
B innerTruncate() 0 23 5
A floor() 0 12 3
A abs() 0 6 3
A mod() 0 5 1
A cosec() 0 11 2
A sec() 0 11 2
B arcsec() 0 27 6
B arccsc() 0 23 6
A exp() 0 15 3
B factorialSerie() 0 24 3
B powerSerie() 0 40 5
B simplePowerSerie() 0 29 5
A tan() 0 11 2
A cotan() 0 11 2
A hasSameSign() 0 4 4
A asFloat() 0 4 1
A asInteger() 0 4 1
A innerValue() 0 4 1
A __toString() 0 4 1
B fromExpNotationString() 0 30 2
A innerRound() 0 15 4
B innerLog10() 0 36 4
B innerPowWithLittleExponent() 0 28 3
A computeSquareIndex() 0 18 2
A compute2NRoot() 0 10 2
A paramsValidation() 0 10 4
A normalizeSign() 0 8 2
A countSignificativeDigits() 0 6 3
B create() 0 17 6
D fromFloat() 0 34 10
C fromString() 0 33 7
B pow() 0 55 9
A sin() 0 19 4
A cos() 0 19 4
C arcsin() 0 26 7
C arccos() 0 30 7
B arctan() 0 22 5
B arccot() 0 25 5

How to fix   Complexity   

Complex Class

Complex classes like Decimal often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Decimal, and based on these observations, apply Extract Interface, too.

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 157
    private function __construct(string $value, int $scale)
37
    {
38 157
        $this->value = $value;
39 157
        $this->scale = $scale;
40 157
    }
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 35
    public static function fromFloat(float $fltValue, int $scale = null): Decimal
84
    {
85 35
        self::paramsValidation($fltValue, $scale);
86
87 35
        if (\is_infinite($fltValue)) {
88
            throw new InfiniteInputError('fltValue must be a finite number');
89 35
        } elseif (\is_nan($fltValue)) {
90 1
            throw new NaNInputError("fltValue can't be NaN");
91
        }
92
93 34
        $strValue = (string) $fltValue;
94 34
        $hasPoint = (false !== \strpos($strValue, '.'));
95
96 34
        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 28
                \strlen((string)\fmod($fltValue, 1.0)) - 2 - (($fltValue < 0) ? 1 : 0) + (!$hasPoint ? 1 : 0)
106
            );
107
108 28
            if (null === $scale) {
109 26
                $scale = $naturalScale;
110
            } else {
111 2
                $strValue .= ($hasPoint ? '' : '.') . \str_pad('', $scale - $naturalScale, '0');
112
            }
113
        }
114
115 34
        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 42
    public function comp(Decimal $b, int $scale = null): int
442
    {
443 42
        self::paramsValidation($b, $scale);
444
445 42
        if ($this === $b) {
446 7
            return 0;
447
        }
448
449 41
        $cmp_scale = $scale !== null ? $scale : \max($this->scale, $b->scale);
450
451 41
        return \bccomp(
452 41
            self::innerRound($this->value, $cmp_scale),
453 41
            self::innerRound($b->value, $cmp_scale),
454 41
            $cmp_scale
455
        );
456
    }
457
458
    /**
459
     * Returns the element's additive inverse.
460
     * @return Decimal
461
     */
462 15
    public function additiveInverse(): Decimal
463
    {
464 15
        if ($this->isZero()) {
465 1
            return $this;
466 14
        } elseif ($this->isNegative()) {
467 12
            $value = \substr($this->value, 1);
468
        } else { // if ($this->isPositive()) {
469 2
            $value = '-' . $this->value;
470
        }
471
472 14
        return new static($value, $this->scale);
473
    }
474
475
476
    /**
477
     * "Rounds" the Decimal to have at most $scale digits after the point
478
     * @param  integer $scale
479
     * @return Decimal
480
     */
481 52
    public function round(int $scale = 0): Decimal
482
    {
483 52
        if ($scale >= $this->scale) {
484 21
            return $this;
485
        }
486
487 51
        return self::fromString(self::innerRound($this->value, $scale));
488
    }
489
490
    /**
491
     * "Ceils" the Decimal to have at most $scale digits after the point
492
     * @param  integer $scale
493
     * @return Decimal
494
     */
495 4
    public function ceil($scale = 0): Decimal
496
    {
497 4
        if ($scale >= $this->scale) {
498 2
            return $this;
499
        }
500
501 3
        if ($this->isNegative()) {
502 1
            return self::fromString(\bcadd($this->value, '0', $scale));
503
        }
504
505 2
        return $this->innerTruncate($scale);
506
    }
507
508 28
    private function innerTruncate(int $scale = 0, bool $ceil = true): Decimal
509
    {
510 28
        $rounded = \bcadd($this->value, '0', $scale);
511
512 28
        $rlen = \strlen($rounded);
513 28
        $tlen = \strlen($this->value);
514
515 28
        $mustTruncate = false;
516 28
        for ($i=$tlen-1; $i >= $rlen; $i--) {
517 28
            if ((int)$this->value[$i] > 0) {
518 28
                $mustTruncate = true;
519 28
                break;
520
            }
521
        }
522
523 28
        if ($mustTruncate) {
524 28
            $rounded = $ceil
525 2
                ? \bcadd($rounded, \bcpow('10', (string)-$scale, $scale), $scale)
526 28
                : \bcsub($rounded, \bcpow('10', (string)-$scale, $scale), $scale);
527
        }
528
529 28
        return self::fromString($rounded, $scale);
530
    }
531
532
    /**
533
     * "Floors" the Decimal to have at most $scale digits after the point
534
     * @param  integer $scale
535
     * @return Decimal
536
     */
537 47
    public function floor(int $scale = 0): Decimal
538
    {
539 47
        if ($scale >= $this->scale) {
540 38
            return $this;
541
        }
542
543 38
        if ($this->isNegative()) {
544 26
            return $this->innerTruncate($scale, false);
545
        }
546
547 35
        return self::fromString(\bcadd($this->value, '0', $scale));
548
    }
549
550
    /**
551
     * Returns the absolute value (always a positive number)
552
     * @return Decimal
553
     */
554 21
    public function abs(): Decimal
555
    {
556 21
        return ($this->isZero() || $this->isPositive())
557 19
            ? $this
558 21
            : $this->additiveInverse();
559
    }
560
561
    /**
562
     * Calculate modulo with a decimal
563
     * @param Decimal $d
564
     * @param integer $scale
565
     * @return $this % $d
566
     */
567 27
    public function mod(Decimal $d, int $scale = null): Decimal
568
    {
569 27
        $div = $this->div($d, 1)->floor();
570 27
        return $this->sub($div->mul($d), $scale);
571
    }
572
573
    /**
574
     * Calculates the sine of this method with the highest possible accuracy
575
     * Note that accuracy is limited by the accuracy of predefined PI;
576
     *
577
     * @param integer $scale
578
     * @return Decimal sin($this)
579
     */
580 13
    public function sin(int $scale = null): Decimal
581
    {
582
        // First normalise the number in the [0, 2PI] domain
583 13
        $x = $this->mod(DecimalConstants::PI()->mul(Decimal::fromString("2")));
584
585
        // PI has only 32 significant numbers
586 13
        $scale = (null === $scale) ? 32 : $scale;
587
588 13
        return self::factorialSerie(
589 13
            $x,
590 13
            DecimalConstants::zero(),
591
            function ($i) {
592 13
                return ($i % 2 === 1) ? (
593 13
                ($i % 4 === 1) ? DecimalConstants::one() : DecimalConstants::negativeOne()
594 13
                ) : DecimalConstants::zero();
595 13
            },
596 13
            $scale
597
        );
598
    }
599
600
    /**
601
     * Calculates the cosecant of this with the highest possible accuracy
602
     * Note that accuracy is limited by the accuracy of predefined PI;
603
     *
604
     * @param integer $scale
605
     * @return Decimal
606
     */
607 3
    public function cosec(int $scale = null): Decimal
608
    {
609 3
        $sin = $this->sin($scale + 2);
610 3
        if ($sin->isZero()) {
611
            throw new \DomainException(
612
                "The cosecant of this 'angle' is undefined."
613
            );
614
        }
615
616 3
        return DecimalConstants::one()->div($sin)->round($scale);
617
    }
618
619
    /**
620
     * Calculates the cosine of this method with the highest possible accuracy
621
     * Note that accuracy is limited by the accuracy of predefined PI;
622
     *
623
     * @param integer $scale
624
     * @return Decimal cos($this)
625
     */
626 13
    public function cos(int $scale = null): Decimal
627
    {
628
        // First normalise the number in the [0, 2PI] domain
629 13
        $x = $this->mod(DecimalConstants::PI()->mul(Decimal::fromString("2")));
630
631
        // PI has only 32 significant numbers
632 13
        $scale = ($scale === null) ? 32 : $scale;
633
634 13
        return self::factorialSerie(
635 13
            $x,
636 13
            DecimalConstants::one(),
637
            function ($i) {
638 13
                return ($i % 2 === 0) ? (
639 13
                    ($i % 4 === 0) ? DecimalConstants::one() : DecimalConstants::negativeOne()
640 13
                ) : DecimalConstants::zero();
641 13
            },
642 13
            $scale
643
        );
644
    }
645
646
    /**
647
     * Calculates the secant of this with the highest possible accuracy
648
     * Note that accuracy is limited by the accuracy of predefined PI;
649
     *
650
     * @param integer $scale
651
     * @return Decimal
652
     */
653 3
    public function sec(int $scale = null): Decimal
654
    {
655 3
        $cos = $this->cos($scale + 2);
656 3
        if ($cos->isZero()) {
657
            throw new \DomainException(
658
                "The secant of this 'angle' is undefined."
659
            );
660
        }
661
662 3
        return DecimalConstants::one()->div($cos)->round($scale);
663
    }
664
665
    /**
666
     *	Calculates the arcsine of this with the highest possible accuracy
667
     *
668
     * @param integer $scale
669
     * @return Decimal
670
     */
671 5
    public function arcsin(int $scale = null): Decimal
672
    {
673 5
        if($this->comp(DecimalConstants::one(), $scale + 2) === 1 || $this->comp(DecimalConstants::negativeOne(), $scale + 2) === -1) {
674 2
            throw new \DomainException(
675 2
                "The arcsin of this number is undefined."
676
            );
677
        }
678
679 3
        if ($this->round($scale)->isZero()) {
680
            return DecimalConstants::zero();
681
        }
682 3
        if ($this->round($scale)->equals(DecimalConstants::one())) {
683 1
            return DecimalConstants::pi()->div(Decimal::fromInteger(2))->round($scale);
684
        }
685 2
        if ($this->round($scale)->equals(DecimalConstants::negativeOne())) {
686 1
            return DecimalConstants::pi()->div(Decimal::fromInteger(-2))->round($scale);
687
        }
688
689 1
        $scale = ($scale === null) ? 32 : $scale;
690
691 1
        return self::powerSerie(
692 1
            $this,
693 1
            DecimalConstants::zero(),
694 1
            $scale
695
        );
696
    }
697
698
    /**
699
     *	Calculates the arccosine of this with the highest possible accuracy
700
     *
701
     * @param integer $scale
702
     * @return Decimal
703
     */
704 5
    public function arccos(int $scale = null): Decimal
705
    {
706 5
        if($this->comp(DecimalConstants::one(), $scale + 2) === 1 || $this->comp(DecimalConstants::negativeOne(), $scale + 2) === -1) {
707 2
            throw new \DomainException(
708 2
                "The arccos of this number is undefined."
709
            );
710
        }
711
712 3
        $piOverTwo = DecimalConstants::pi()->div(Decimal::fromInteger(2), $scale + 2)->round($scale);
713
714 3
        if ($this->round($scale)->isZero()) {
715
            return $piOverTwo;
716
        }
717 3
        if ($this->round($scale)->equals(DecimalConstants::one())) {
718 1
            return DecimalConstants::zero();
719
        }
720 2
        if ($this->round($scale)->equals(DecimalConstants::negativeOne())) {
721 1
            return DecimalConstants::pi()->round($scale);
722
        }
723
724 1
        $scale = ($scale === null) ? 32 : $scale;
725
726 1
        return $piOverTwo->sub(
727 1
            self::powerSerie(
728 1
                $this,
729 1
                DecimalConstants::zero(),
730 1
                $scale
731
            )
732 1
        )->round($scale);
733
    }
734
735
    /**
736
     *	Calculates the arctangente of this with the highest possible accuracy
737
     *
738
     * @param integer $scale
739
     * @return Decimal
740
     */
741 3
    public function arctan(int $scale = null): Decimal
742
    {
743 3
        $piOverFour = DecimalConstants::pi()->div(Decimal::fromInteger(4), $scale + 2)->round($scale);
744
745 3
        if ($this->round($scale)->isZero()) {
746 1
            return DecimalConstants::zero();
747
        }
748 2
        if ($this->round($scale)->equals(DecimalConstants::one())) {
749
            return $piOverFour;
750
        }
751 2
        if ($this->round($scale)->equals(DecimalConstants::negativeOne())) {
752 1
            return DecimalConstants::negativeOne()->mul($piOverFour);
753
        }
754
755 1
        $scale = ($scale === null) ? 32 : $scale;
756
757 1
        return self::simplePowerSerie(
758 1
            $this,
759 1
            DecimalConstants::zero(),
760 1
            $scale + 2
761 1
        )->round($scale);
762
    }
763
764
    /**
765
     * Calculates the arccotangente of this with the highest possible accuracy
766
     *
767
     * @param integer $scale
768
     * @return Decimal
769
     */
770 3
    public function arccot(int $scale = null): Decimal
771
    {
772 3
        $scale = ($scale === null) ? 32 : $scale;
773
774 3
        $piOverTwo = DecimalConstants::pi()->div(Decimal::fromInteger(2), $scale + 2);
775 3
        if ($this->round($scale)->isZero()) {
776 1
            return $piOverTwo->round($scale);
777
        }
778
779 2
        $piOverFour = DecimalConstants::pi()->div(Decimal::fromInteger(4), $scale + 2);
780 2
        if ($this->round($scale)->equals(DecimalConstants::one())) {
781
            return $piOverFour->round($scale);
782
        }
783 2
        if ($this->round($scale)->equals(DecimalConstants::negativeOne())) {
784 1
            return DecimalConstants::negativeOne()->mul($piOverFour, $scale + 2)->round($scale);
785
        }
786
787 1
        return $piOverTwo->sub(
788 1
            self::simplePowerSerie(
789 1
                $this,
790 1
                DecimalConstants::zero(),
791 1
                $scale + 2
792
            )
793 1
        )->round($scale);
794
    }
795
796
    /**
797
     * Calculates the arcsecant of this with the highest possible accuracy
798
     *
799
     * @param integer $scale
800
     * @return Decimal
801
     */
802 5
    public function arcsec(int $scale = null): Decimal
803
    {
804 5
        if($this->comp(DecimalConstants::one(), $scale + 2) === -1 && $this->comp(DecimalConstants::negativeOne(), $scale + 2) === 1) {
805 1
            throw new \DomainException(
806 1
                "The arcsecant of this number is undefined."
807
            );
808
        }
809
810 4
        $piOverTwo = DecimalConstants::pi()->div(Decimal::fromInteger(2), $scale + 2)->round($scale);
811
812 4
        if ($this->round($scale)->equals(DecimalConstants::one())) {
813 1
            return DecimalConstants::zero();
814
        }
815 3
        if ($this->round($scale)->equals(DecimalConstants::negativeOne())) {
816 1
            return DecimalConstants::pi()->round($scale);
817
        }
818
819 2
        $scale = ($scale === null) ? 32 : $scale;
820
821 2
        return $piOverTwo->sub(
822 2
            self::powerSerie(
823 2
                DecimalConstants::one()->div($this, $scale + 2),
824 2
                DecimalConstants::zero(),
825 2
                $scale + 2
826
            )
827 2
        )->round($scale);
828
    }
829
830
    /**
831
     * Calculates the arccosecant of this with the highest possible accuracy
832
     *
833
     * @param integer $scale
834
     * @return Decimal
835
     */
836 5
    public function arccsc(int $scale = null): Decimal
837
    {
838 5
        if($this->comp(DecimalConstants::one(), $scale + 2) === -1 && $this->comp(DecimalConstants::negativeOne(), $scale + 2) === 1) {
839 1
            throw new \DomainException(
840 1
                "The arccosecant of this number is undefined."
841
            );
842
        }
843
844 4
        $scale = ($scale === null) ? 32 : $scale;
845
846 4
        if ($this->round($scale)->equals(DecimalConstants::one())) {
847 1
            return DecimalConstants::pi()->div(Decimal::fromInteger(2), $scale + 2)->round($scale);
848
        }
849 3
        if ($this->round($scale)->equals(DecimalConstants::negativeOne())) {
850 1
            return DecimalConstants::pi()->div(Decimal::fromInteger(-2), $scale + 2)->round($scale);
851
        }
852
853 2
        return self::powerSerie(
854 2
            DecimalConstants::one()->div($this, $scale + 2),
855 2
            DecimalConstants::zero(),
856 2
            $scale + 2
857 2
        )->round($scale);
858
    }
859
860
    /**
861
     * Returns exp($this), said in other words: e^$this .
862
     *
863
     * @param integer $scale
864
     * @return Decimal
865
     */
866 11
    public function exp(int $scale = null): Decimal
867
    {
868 11
        if ($this->isZero()) {
869 3
            return DecimalConstants::one();
870
        }
871
872 8
        $scale = $scale ?? \max(
873
            $this->scale,
874 8
            (int)($this->isNegative() ? self::innerLog10($this->value, $this->scale, 0) : self::DEFAULT_SCALE)
875
        );
876
877 8
        return self::factorialSerie(
878
            $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...
879
        );
880
    }
881
882
    /**
883
     * Internal method used to compute sin, cos and exp
884
     *
885
     * @param Decimal $x
886
     * @param Decimal $firstTerm
887
     * @param callable $generalTerm
888
     * @param $scale
889
     * @return Decimal
890
     */
891 28
    private static function factorialSerie (Decimal $x, Decimal $firstTerm, callable $generalTerm, int $scale): Decimal
892
    {
893 28
        $approx = $firstTerm;
894 28
        $change = DecimalConstants::One();
895
896 28
        $faculty = DecimalConstants::One();    // Calculates the faculty under the sign
897 28
        $xPowerN = DecimalConstants::One();    // Calculates x^n
898
899 28
        for ($i = 1; !$change->floor($scale+1)->isZero(); $i++) {
900
            // update x^n and n! for this walkthrough
901 28
            $xPowerN = $xPowerN->mul($x);
902 28
            $faculty = $faculty->mul(Decimal::fromInteger($i));
903
904
            /** @var Decimal $multiplier */
905 28
            $multiplier = $generalTerm($i);
906
907 28
            if (!$multiplier->isZero()) {
908 28
                $change = $multiplier->mul($xPowerN, $scale + 2)->div($faculty, $scale + 2);
909 28
                $approx = $approx->add($change, $scale + 2);
910
            }
911
        }
912
913 28
        return $approx->round($scale);
914
    }
915
916
917
    /**
918
     * Internal method used to compute arcsine and arcosine
919
     *
920
     * @param Decimal $x
921
     * @param Decimal $firstTerm
922
     * @param $scale
923
     * @return Decimal
924
     */
925 6
    private static function powerSerie (Decimal $x, Decimal $firstTerm, int $scale): Decimal
926
    {
927 6
        $approx = $firstTerm;
928 6
        $change = DecimalConstants::One();
929
930 6
        $xPowerN = DecimalConstants::One();     // Calculates x^n
931 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...
932
933 6
        $numerator = DecimalConstants::one();
934 6
        $denominator = DecimalConstants::one();
935
936 6
        for ($i = 1; !$change->floor($scale + 2)->isZero(); $i++) {
937 6
            $xPowerN = $xPowerN->mul($x);
938
939 6
            if ($i % 2 === 0) {
940 6
                $factorN = DecimalConstants::zero();
941 6
            } elseif ($i === 1) {
942 6
                $factorN = DecimalConstants::one();
943
            } else {
944 6
                $incrementNum = Decimal::fromInteger($i - 2);
945 6
                $numerator = $numerator->mul($incrementNum, $scale +2);
946
947 6
                $incrementDen = Decimal::fromInteger($i - 1);
948 6
                $increment = Decimal::fromInteger($i);
949
                $denominator = $denominator
950 6
                    ->div($incrementNum, $scale +2)
951 6
                    ->mul($incrementDen, $scale +2)
952 6
                    ->mul($increment, $scale +2);
953
954 6
                $factorN = $numerator->div($denominator, $scale + 2);
955
            }
956
957 6
            if (!$factorN->isZero()) {
958 6
                $change = $factorN->mul($xPowerN, $scale + 2);
959 6
                $approx = $approx->add($change, $scale + 2);
960
            }
961
        }
962
963 6
        return $approx->round($scale);
964
    }
965
966
    /**
967
     * Internal method used to compute arctan and arccotan
968
     *
969
     * @param Decimal $x
970
     * @param Decimal $firstTerm
971
     * @param $scale
972
     * @return Decimal
973
     */
974 2
    private static function simplePowerSerie (Decimal $x, Decimal $firstTerm, int $scale): Decimal
975
    {
976 2
        $approx = $firstTerm;
977 2
        $change = DecimalConstants::One();
978
979 2
        $xPowerN = DecimalConstants::One();     // Calculates x^n
980 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...
981
982 2
        for ($i = 1; !$change->floor($scale + 2)->isZero(); $i++) {
983 2
            $xPowerN = $xPowerN->mul($x);
984
985 2
            if ($i % 2 === 0) {
986 2
                $factorN = DecimalConstants::zero();
987
            } else {
988 2
                 if ($i % 4 === 1) {
989 2
                     $factorN = DecimalConstants::one()->div(Decimal::fromInteger($i), $scale + 2);
990
                 } else {
991 2
                     $factorN = DecimalConstants::negativeOne()->div(Decimal::fromInteger($i), $scale + 2);
992
                 }
993
            }
994
995 2
            if (!$factorN->isZero()) {
996 2
                $change = $factorN->mul($xPowerN, $scale + 2);
997 2
                $approx = $approx->add($change, $scale + 2);
998
            }
999
        }
1000
1001 2
        return $approx->round($scale);
1002
    }
1003
1004
    /**
1005
     * Calculates the tangent of this method with the highest possible accuracy
1006
     * Note that accuracy is limited by the accuracy of predefined PI;
1007
     *
1008
     * @param integer $scale
1009
     * @return Decimal tan($this)
1010
     */
1011 4
    public function tan(int $scale = null): Decimal
1012
    {
1013 4
	    $cos = $this->cos($scale + 2);
1014 4
	    if ($cos->isZero()) {
1015 1
	        throw new \DomainException(
1016 1
	            "The tangent of this 'angle' is undefined."
1017
	        );
1018
	    }
1019
1020 3
	    return $this->sin($scale + 2)->div($cos)->round($scale);
1021
    }
1022
1023
    /**
1024
     * Calculates the cotangent of this method with the highest possible accuracy
1025
     * Note that accuracy is limited by the accuracy of predefined PI;
1026
     *
1027
     * @param integer $scale
1028
     * @return Decimal cotan($this)
1029
     */
1030 4
    public function cotan(int $scale = null): Decimal
1031
    {
1032 4
	    $sin = $this->sin($scale + 2);
1033 4
	    if ($sin->isZero()) {
1034 1
	        throw new \DomainException(
1035 1
	            "The cotangent of this 'angle' is undefined."
1036
	        );
1037
	    }
1038
1039 3
	    return $this->cos($scale + 2)->div($sin)->round($scale);
1040
    }
1041
1042
    /**
1043
     * Indicates if the passed parameter has the same sign as the method's bound object.
1044
     *
1045
     * @param Decimal $b
1046
     * @return bool
1047
     */
1048
    public function hasSameSign(Decimal $b): bool
1049
    {
1050
        return $this->isPositive() && $b->isPositive() || $this->isNegative() && $b->isNegative();
1051
    }
1052
1053 3
    public function asFloat(): float
1054
    {
1055 3
        return \floatval($this->value);
1056
    }
1057
1058 1
    public function asInteger(): int
1059
    {
1060 1
        return \intval($this->value);
1061
    }
1062
1063
    /**
1064
     * WARNING: use with caution! Return the inner representation of the class.
1065
     */
1066 11
    public function innerValue(): string
1067
    {
1068 11
        return $this->value;
1069
    }
1070
1071
    /**
1072
     * @return string
1073
     */
1074 58
    public function __toString(): string
1075
    {
1076 58
        return $this->value;
1077
    }
1078
1079
    /*
1080
     *
1081
     */
1082 7
    private static function fromExpNotationString(
1083
        int $scale = null,
1084
        string $sign,
1085
        string $mantissa,
1086
        int $nDecimals,
1087
        string $expSign,
1088
        int $expVal
1089
    ): array
1090
    {
1091 7
        $mantissaScale = \max($nDecimals, 0);
1092
1093 7
        if (self::normalizeSign($expSign) === '') {
1094 5
            $minScale = \max($mantissaScale - $expVal, 0);
1095 5
            $tmp_multiplier = \bcpow('10', (string)$expVal);
1096
        } else {
1097 2
            $minScale = $mantissaScale + $expVal;
1098 2
            $tmp_multiplier = \bcpow('10', (string)-$expVal, $expVal);
1099
        }
1100
1101
        $value = (
1102 7
            self::normalizeSign($sign) .
1103 7
            \bcmul(
1104 7
                $mantissa,
1105 7
                $tmp_multiplier,
1106 7
                \max($minScale, $scale ?? 0)
1107
            )
1108
        );
1109
1110 7
        return [$minScale, $value];
1111
    }
1112
1113
    /**
1114
     * "Rounds" the decimal string to have at most $scale digits after the point
1115
     *
1116
     * @param  string $value
1117
     * @param  int    $scale
1118
     * @return string
1119
     */
1120 136
    private static function innerRound(string $value, int $scale = 0): string
1121
    {
1122 136
        $rounded = \bcadd($value, '0', $scale);
1123
1124 136
        $diffDigit = \bcsub($value, $rounded, $scale+1);
1125 136
        $diffDigit = (int)$diffDigit[\strlen($diffDigit)-1];
1126
1127 136
        if ($diffDigit >= 5) {
1128 67
            $rounded = ($diffDigit >= 5 && $value[0] !== '-')
1129 63
                ? \bcadd($rounded, \bcpow('10', (string)-$scale, $scale), $scale)
1130 67
                : \bcsub($rounded, \bcpow('10', (string)-$scale, $scale), $scale);
1131
        }
1132
1133 136
        return $rounded;
1134
    }
1135
1136
    /**
1137
     * Calculates the logarithm (in base 10) of $value
1138
     *
1139
     * @param  string $value     The number we want to calculate its logarithm (only positive numbers)
1140
     * @param  int    $in_scale  Expected scale used by $value (only positive numbers)
1141
     * @param  int    $out_scale Scale used by the return value (only positive numbers)
1142
     * @return string
1143
     */
1144 23
    private static function innerLog10(string $value, int $in_scale, int $out_scale): string
1145
    {
1146 23
        $value_len = \strlen($value);
1147
1148 23
        $cmp = \bccomp($value, '1', $in_scale);
1149
1150
        switch ($cmp) {
1151 23
            case 1:
1152 10
                $value_log10_approx = $value_len - ($in_scale > 0 ? ($in_scale+2) : 1);
1153
1154 10
                return \bcadd(
1155 10
                    (string)$value_log10_approx,
1156 10
                    (string)\log10((float)\bcdiv(
1157 10
                        $value,
1158 10
                        \bcpow('10', (string)$value_log10_approx),
1159 10
                        \min($value_len, $out_scale)
1160
                    )),
1161 10
                    $out_scale
1162
                );
1163 14
            case -1:
1164 13
                \preg_match('/^0*\.(0*)[1-9][0-9]*$/', $value, $captures);
1165 13
                $value_log10_approx = -\strlen($captures[1])-1;
1166
1167 13
                return \bcadd(
1168 13
                    (string)$value_log10_approx,
1169 13
                    (string)\log10((float)\bcmul(
1170 13
                        $value,
1171 13
                        \bcpow('10', (string)-$value_log10_approx),
1172 13
                        $in_scale + $value_log10_approx
1173
                    )),
1174 13
                    $out_scale
1175
                );
1176
            default: // case 0:
1177 7
                return '0';
1178
        }
1179
    }
1180
1181
    /**
1182
     * Returns $base^$exponent
1183
     *
1184
     * @param  string $base
1185
     * @param  string $exponent   0 < $exponent < 1
1186
     * @param  int    $exp_scale Number of $exponent's significative digits
1187
     * @param  int    $out_scale Number of significative digits that we want to compute
1188
     * @return string
1189
     */
1190 3
    private static function innerPowWithLittleExponent(
1191
        string $base,
1192
        string $exponent,
1193
        int $exp_scale,
1194
        int $out_scale
1195
    ): string
1196
    {
1197 3
        $inner_scale = (int)\ceil($exp_scale * \log(10) / \log(2)) + 1;
1198
1199 3
        $result_a = '1';
1200 3
        $result_b = '0';
1201
1202 3
        $actual_index = 0;
1203 3
        $exponent_remaining = $exponent;
1204
1205 3
        while (\bccomp($result_a, $result_b, $out_scale) !== 0 && \bccomp($exponent_remaining, '0', $inner_scale) !== 0) {
1206 3
            $result_b = $result_a;
1207 3
            $index_info = self::computeSquareIndex($exponent_remaining, $actual_index, $exp_scale, $inner_scale);
1208 3
            $exponent_remaining = $index_info[1];
1209 3
            $result_a = \bcmul(
1210 3
                $result_a,
1211 3
                self::compute2NRoot($base, $index_info[0], 2*($out_scale+1)),
1212 3
                2*($out_scale+1)
1213
            );
1214
        }
1215
1216 3
        return self::innerRound($result_a, $out_scale);
1217
    }
1218
1219
    /**
1220
     * Auxiliar method. It helps us to decompose the exponent into many summands.
1221
     *
1222
     * @param  string $exponent_remaining
1223
     * @param  int    $actual_index
1224
     * @param  int    $exp_scale           Number of $exponent's significative digits
1225
     * @param  int    $inner_scale         ceil($exp_scale*log(10)/log(2))+1;
1226
     * @return array
1227
     */
1228 3
    private static function computeSquareIndex(
1229
        string $exponent_remaining,
1230
        int $actual_index,
1231
        int $exp_scale,
1232
        int $inner_scale
1233
    ): array
1234
    {
1235 3
        $actual_rt = \bcpow('0.5', (string)$actual_index, $exp_scale);
1236 3
        $r = \bcsub($exponent_remaining, $actual_rt, $inner_scale);
1237
1238 3
        while (\bccomp($r, '0', $exp_scale) === -1) {
1239 3
            ++$actual_index;
1240 3
            $actual_rt = \bcmul('0.5', $actual_rt, $inner_scale);
1241 3
            $r = \bcsub($exponent_remaining, $actual_rt, $inner_scale);
1242
        }
1243
1244 3
        return [$actual_index, $r];
1245
    }
1246
1247
    /**
1248
     * Auxiliar method. Computes $base^((1/2)^$index)
1249
     *
1250
     * @param  string  $base
1251
     * @param  integer $index
1252
     * @param  integer $out_scale
1253
     * @return string
1254
     */
1255 3
    private static function compute2NRoot(string $base, int $index, int $out_scale): string
1256
    {
1257 3
        $result = $base;
1258
1259 3
        for ($i = 0; $i < $index; $i++) {
1260 3
            $result = \bcsqrt($result, ($out_scale + 1) * ($index - $i) + 1);
1261
        }
1262
1263 3
        return self::innerRound($result, $out_scale);
1264
    }
1265
1266
    /**
1267
     * Validates basic constructor's arguments
1268
     * @param  mixed    $value
1269
     * @param  null|int  $scale
1270
     */
1271 160
    protected static function paramsValidation($value, int $scale = null)
1272
    {
1273 160
        if (null === $value) {
1274
            throw new \InvalidArgumentException('$value must be a non null number');
1275
        }
1276
1277 160
        if (null !== $scale && $scale < 0) {
1278 2
            throw new \InvalidArgumentException('$scale must be a positive integer');
1279
        }
1280 159
    }
1281
1282
    /**
1283
     * @return string
1284
     */
1285 129
    private static function normalizeSign(string $sign): string
1286
    {
1287 129
        if ('+' === $sign) {
1288 4
            return '';
1289
        }
1290
1291 129
        return $sign;
1292
    }
1293
1294
    /**
1295
     * Counts the number of significant digits of $val.
1296
     * Assumes a consistent internal state (without zeros at the end or the start).
1297
     *
1298
     * @param  Decimal $val
1299
     * @param  Decimal $abs $val->abs()
1300
     * @return int
1301
     */
1302 20
    private static function countSignificativeDigits(Decimal $val, Decimal $abs): int
1303
    {
1304 20
        return \strlen($val->value) - (
1305 20
            ($abs->comp(DecimalConstants::One()) === -1) ? 2 : \max($val->scale, 1)
1306 20
        ) - ($val->isNegative() ? 1 : 0);
1307
    }
1308
}
1309