Completed
Push — master ( f91b9b...348b6b )
by Andreu
06:32
created

Decimal::fromString()   D

Complexity

Conditions 9
Paths 33

Size

Total Lines 44
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 9.0036

Importance

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