Completed
Pull Request — master (#67)
by Petr
08:45
created

Decimal   F

Complexity

Total Complexity 195

Size/Duplication

Total Lines 1348
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 96.69%

Importance

Changes 0
Metric Value
wmc 195
lcom 1
cbo 4
dl 0
loc 1348
ccs 526
cts 544
cp 0.9669
rs 0.8
c 0
b 0
f 0

61 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A __clone() 0 3 1
A create() 0 17 6
A fromInteger() 0 6 1
B fromFloat() 0 36 11
B fromString() 0 33 7
A fromDecimal() 0 14 3
A add() 0 9 1
A sub() 0 9 1
A mul() 0 13 2
A div() 0 35 4
A sqrt() 0 17 4
B pow() 0 55 9
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 isGreaterThan() 0 4 1
A isGreaterOrEqualTo() 0 6 2
A isLessThan() 0 4 1
A isLessOrEqualTo() 0 6 2
A additiveInverse() 0 12 3
A round() 0 8 2
A ceil() 0 12 3
A innerTruncate() 0 23 5
A floor() 0 12 3
A abs() 0 6 3
A mod() 0 5 1
A sin() 0 19 4
A cosec() 0 11 2
A cos() 0 19 4
A sec() 0 11 2
B arcsin() 0 26 7
B arccos() 0 30 7
A arctan() 0 22 5
A arccot() 0 25 5
B arcsec() 0 27 6
B arccsc() 0 23 6
A exp() 0 15 3
A factorialSerie() 0 24 3
B powerSerie() 0 40 5
A 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
A fromExpNotationString() 0 30 2
A innerRound() 0 15 4
A innerLog10() 0 37 4
A 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

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