Completed
Push — master ( 348b6b...4b9358 )
by Andreu
04:35
created

Decimal::fromString()   C

Complexity

Conditions 7
Paths 25

Size

Total Lines 33
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 7.0046

Importance

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