Completed
Branch master (bb64b5)
by Andreu
02:01
created

Decimal::pow()   B

Complexity

Conditions 9
Paths 9

Size

Total Lines 55
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 9.0666

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 55
ccs 29
cts 32
cp 0.9063
rs 7.2446
cc 9
eloc 39
nc 9
nop 2
crap 9.0666

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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