Completed
Push — master ( 21a74f...4ebb19 )
by Andreu
03:23
created

Decimal::fromString()   B

Complexity

Conditions 5
Paths 7

Size

Total Lines 30
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 5

Importance

Changes 5
Bugs 0 Features 0
Metric Value
c 5
b 0
f 0
dl 0
loc 30
ccs 17
cts 17
cp 1
rs 8.439
cc 5
eloc 19
nc 7
nop 2
crap 5
1
<?php
2
declare(strict_types=1);
3
4
namespace Litipk\BigNumbers;
5
6
use Litipk\BigNumbers\DecimalConstants as DecimalConstants;
7
8
use Litipk\BigNumbers\Errors\InfiniteInputError;
9
use Litipk\BigNumbers\Errors\NaNInputError;
10
use Litipk\BigNumbers\Errors\NotImplementedError;
11
12
/**
13
 * Immutable object that represents a rational number
14
 *
15
 * @author Andreu Correa Casablanca <[email protected]>
16
 */
17
class Decimal
18
{
19
    const DEFAULT_SCALE = 16;
20
    const CLASSIC_DECIMAL_NUMBER_REGEXP = '/^([+\-]?)0*(([1-9][0-9]*|[0-9])(\.[0-9]+)?)$/';
21
    const EXP_NOTATION_NUMBER_REGEXP = '/^ (?P<sign> [+\-]?) 0*(?P<mantissa> [0-9](?P<decimals> \.[0-9]+)?) [eE] (?P<expSign> [+\-]?)(?P<exp> \d+)$/x';
22
    const EXP_NUM_GROUPS_NUMBER_REGEXP = '/^ (?P<int> \d*) (?: \. (?P<dec> \d+) ) E (?P<sign>[\+\-]) (?P<exp>\d+) $/x';
23
24
    /**
25
     * Internal numeric value
26
     * @var string
27
     */
28
    protected $value;
29
30
    /**
31
     * Number of digits behind the point
32
     * @var integer
33
     */
34
    private $scale;
35
36 155
    private function __construct(string $value, int $scale)
37
    {
38 155
        $this->value = $value;
39 155
        $this->scale = $scale;
40 155
    }
41
42
    private function __clone()
43
    {
44
    }
45
46
    /**
47
     * Decimal "constructor".
48
     *
49
     * @param  mixed $value
50
     * @param  int   $scale
51
     * @return Decimal
52
     */
53 7
    public static function create($value, int $scale = null): Decimal
54
    {
55 7
        if (\is_int($value)) {
56 3
            return self::fromInteger($value);
57
        } elseif (\is_float($value)) {
58 3
            return self::fromFloat($value, $scale);
59
        } elseif (\is_string($value)) {
60 1
            return self::fromString($value, $scale);
61 2
        } elseif ($value instanceof Decimal) {
62 1
            return self::fromDecimal($value, $scale);
63
        } else {
64 1
            throw new \TypeError(
65
                'Expected (int, float, string, Decimal), but received ' .
0 ignored issues
show
Unused Code introduced by
The call to TypeError::__construct() has too many arguments starting with 'Expected (int, float, s...ue) : \gettype($value)).

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
66 1
                (\is_object($value) ? \get_class($value) : \gettype($value))
67
            );
68
        }
69
    }
70
71 97
    public static function fromInteger(int $intValue): Decimal
72
    {
73 97
        self::paramsValidation($intValue, null);
74
75 97
        return new static((string)$intValue, 0);
76
    }
77
78
    /**
79
     * @param  float $fltValue
80
     * @param  int   $scale
81
     * @return Decimal
82
     */
83 34
    public static function fromFloat(float $fltValue, int $scale = null): Decimal
84
    {
85 34
        self::paramsValidation($fltValue, $scale);
86
87 34
        if (\is_infinite($fltValue)) {
88
            throw new InfiniteInputError('fltValue must be a finite number');
89 34
        } elseif (\is_nan($fltValue)) {
90 1
            throw new NaNInputError("fltValue can't be NaN");
91
        }
92
93 33
        $strValue = (string) $fltValue;
94 33
        if (\preg_match(self::EXP_NUM_GROUPS_NUMBER_REGEXP, $strValue, $capture)) {
95 6
            if (null === $scale) {
96 3
                $scale = ('-' === $capture['sign'])
97 2
                    ? $capture['exp'] + \strlen($capture['dec'])
98 3
                    : self::DEFAULT_SCALE;
99
            }
100 6
            $strValue = \number_format($fltValue, $scale, '.', '');
101
        } else {
102 27
            $naturalScale = \strlen((string)fmod($fltValue, 1.0)) - 2 - (($fltValue < 0) ? 1 : 0);
103
104 27
            if (null === $scale) {
105 26
                $scale = $naturalScale;
106
            } else {
107 1
                $strValue .= str_pad('', $scale - $naturalScale, '0');
108
            }
109
        }
110
111 33
        return new static($strValue, $scale);
112
    }
113
114
    /**
115
     * @param  string  $strValue
116
     * @param  integer $scale
117
     * @return Decimal
118
     */
119 130
    public static function fromString(string $strValue, int $scale = null): Decimal
120
    {
121 130
        self::paramsValidation($strValue, $scale);
122
123 129
        if (\preg_match(self::CLASSIC_DECIMAL_NUMBER_REGEXP, $strValue, $captures) === 1) {
124
125
            // Now it's time to strip leading zeros in order to normalize inner values
126 124
            $value = self::normalizeSign($captures[1]) . $captures[2];
127 124
            $min_scale = isset($captures[4]) ? \max(0, \strlen($captures[4]) - 1) : 0;
128
129 8
        } elseif (\preg_match(self::EXP_NOTATION_NUMBER_REGEXP, $strValue, $captures) === 1) {
130 7
            list($min_scale, $value) = self::fromExpNotationString(
131
                $scale,
132 7
                $captures['sign'],
133 7
                $captures['mantissa'],
134 7
                \strlen($captures['mantissa']) - 1,
135 7
                $captures['expSign'],
136 7
                (int)$captures['exp']
137
            );
138
        } else {
139 1
            throw new NaNInputError('strValue must be a number');
140
        }
141
142 128
        $scale = $scale ?? $min_scale;
143 128
        if ($scale < $min_scale) {
144 67
            $value = self::innerRound($value, $scale);
145
        }
146
147 128
        return new static($value, $scale);
148
    }
149
150
    /**
151
     * Constructs a new Decimal object based on a previous one,
152
     * but changing it's $scale property.
153
     *
154
     * @param  Decimal  $decValue
155
     * @param  null|int $scale
156
     * @return Decimal
157
     */
158 3
    public static function fromDecimal(Decimal $decValue, int $scale = null): Decimal
159
    {
160 3
        self::paramsValidation($decValue, $scale);
161
162
        // This block protect us from unnecessary additional instances
163 3
        if ($scale === null || $scale >= $decValue->scale) {
164 3
            return $decValue;
165
        }
166
167 2
        return new static(
168 2
            self::innerRound($decValue->value, $scale),
169
            $scale
170
        );
171
    }
172
173
    /**
174
     * Adds two Decimal objects
175
     * @param  Decimal  $b
176
     * @param  null|int $scale
177
     * @return Decimal
178
     */
179 40
    public function add(Decimal $b, int $scale = null): Decimal
180
    {
181 40
        self::paramsValidation($b, $scale);
182
183 40
        return self::fromString(
184 40
            \bcadd($this->value, $b->value, \max($this->scale, $b->scale)),
185
            $scale
186
        );
187
    }
188
189
    /**
190
     * Subtracts two BigNumber objects
191
     * @param  Decimal $b
192
     * @param  integer $scale
193
     * @return Decimal
194
     */
195 33
    public function sub(Decimal $b, int $scale = null): Decimal
196
    {
197 33
        self::paramsValidation($b, $scale);
198
199 33
        return self::fromString(
200 33
            \bcsub($this->value, $b->value, \max($this->scale, $b->scale)),
201
            $scale
202
        );
203
    }
204
205
    /**
206
     * Multiplies two BigNumber objects
207
     * @param  Decimal $b
208
     * @param  integer $scale
209
     * @return Decimal
210
     */
211 48
    public function mul(Decimal $b, int $scale = null): Decimal
212
    {
213 48
        self::paramsValidation($b, $scale);
214
215 47
        if ($b->isZero()) {
216 1
            return DecimalConstants::Zero();
217
        }
218
219 47
        return self::fromString(
220 47
            \bcmul($this->value, $b->value, $this->scale + $b->scale),
221
            $scale
222
        );
223
    }
224
225
    /**
226
     * Divides the object by $b .
227
     * Warning: div with $scale == 0 is not the same as
228
     *          integer division because it rounds the
229
     *          last digit in order to minimize the error.
230
     *
231
     * @param  Decimal $b
232
     * @param  integer $scale
233
     * @return Decimal
234
     */
235 62
    public function div(Decimal $b, int $scale = null): Decimal
236
    {
237 62
        self::paramsValidation($b, $scale);
238
239 62
        if ($b->isZero()) {
240 1
            throw new \DomainException("Division by zero is not allowed.");
241 62
        } elseif ($this->isZero()) {
242 1
            return DecimalConstants::Zero();
243
        } else {
244 61
            if (null !== $scale) {
245 56
                $divscale = $scale;
246
            } else {
247
                // $divscale is calculated in order to maintain a reasonable precision
248 19
                $this_abs = $this->abs();
249 19
                $b_abs    = $b->abs();
250
251
                $log10_result =
252 19
                    self::innerLog10($this_abs->value, $this_abs->scale, 1) -
253 19
                    self::innerLog10($b_abs->value, $b_abs->scale, 1);
254
255 19
                $divscale = (int)\max(
256 19
                    $this->scale + $b->scale,
257 19
                    \max(
258 19
                        self::countSignificativeDigits($this, $this_abs),
259 19
                        self::countSignificativeDigits($b, $b_abs)
260 19
                    ) - \max(\ceil($log10_result), 0),
261 19
                    \ceil(-$log10_result) + 1
262
                );
263
            }
264
265 61
            return self::fromString(
266 61
                \bcdiv($this->value, $b->value, $divscale+1), $divscale
267
            );
268
        }
269
    }
270
271
    /**
272
     * Returns the square root of this object
273
     * @param  integer $scale
274
     * @return Decimal
275
     */
276 4
    public function sqrt(int $scale = null): Decimal
277
    {
278 4
        if ($this->isNegative()) {
279 1
            throw new \DomainException(
280 1
                "Decimal can't handle square roots of negative numbers (it's only for real numbers)."
281
            );
282 3
        } elseif ($this->isZero()) {
283 1
            return DecimalConstants::Zero();
284
        }
285
286 3
        $sqrt_scale = ($scale !== null ? $scale : $this->scale);
287
288 3
        return self::fromString(
289 3
            \bcsqrt($this->value, $sqrt_scale+1),
290
            $sqrt_scale
291
        );
292
    }
293
294
    /**
295
     * Powers this value to $b
296
     *
297
     * @param  Decimal  $b      exponent
298
     * @param  integer  $scale
299
     * @return Decimal
300
     */
301 10
    public function pow(Decimal $b, int $scale = null): Decimal
302
    {
303 10
        if ($this->isZero()) {
304 2
            if ($b->isPositive()) {
305 1
                return Decimal::fromDecimal($this, $scale);
306
            } else {
307 1
                throw new \DomainException("zero can't be powered to zero or negative numbers.");
308
            }
309 8
        } elseif ($b->isZero()) {
310 1
            return DecimalConstants::One();
311 7
        } else if ($b->isNegative()) {
312 2
            return DecimalConstants::One()->div(
313 2
                $this->pow($b->additiveInverse(), max($scale, self::DEFAULT_SCALE)),
314 2
                max($scale, self::DEFAULT_SCALE)
315
            );
316 7
        } elseif (0 === $b->scale) {
317 4
            $pow_scale = \max($this->scale, $b->scale, $scale ?? 0);
318
319 4
            return self::fromString(
320 4
                \bcpow($this->value, $b->value, $pow_scale+1),
321
                $pow_scale
322
            );
323
        } else {
324 4
            if ($this->isPositive()) {
325 3
                $pow_scale = \max($this->scale, $b->scale, $scale ?? 0);
326
327 3
                $truncated_b = \bcadd($b->value, '0', 0);
328 3
                $remaining_b = \bcsub($b->value, $truncated_b, $b->scale);
329
330 3
                $first_pow_approx = \bcpow($this->value, $truncated_b, $pow_scale+1);
331 3
                $intermediate_root = self::innerPowWithLittleExponent(
332 3
                    $this->value,
333
                    $remaining_b,
334 3
                    $b->scale,
335 3
                    $pow_scale+1
336
                );
337
338 3
                return Decimal::fromString(
339 3
                    \bcmul($first_pow_approx, $intermediate_root, $pow_scale+1),
340
                    $pow_scale
341
                );
342
            } else { // elseif ($this->isNegative())
343 1
                if ($b->isInteger()) {
344
                    if (\preg_match('/^[+\-]?[0-9]*[02468](\.0+)?$/', $b->value, $captures) === 1) {
345
                        // $b is an even number
346
                        return $this->additiveInverse()->pow($b, $scale);
347
                    } else {
348
                        // $b is an odd number
349
                        return $this->additiveInverse()->pow($b, $scale)->additiveInverse();
350
                    }
351
                }
352
353 1
                throw new NotImplementedError(
354
                    "Usually negative numbers can't be powered to non integer numbers. " .
355 1
                    "The cases where is possible are not implemented."
356
                );
357
            }
358
        }
359
    }
360
361
    /**
362
     * Returns the object's logarithm in base 10
363
     * @param  integer $scale
364
     * @return Decimal
365
     */
366 5
    public function log10(int $scale = null): Decimal
367
    {
368 5
        if ($this->isNegative()) {
369 1
            throw new \DomainException(
370 1
                "Decimal can't handle logarithms of negative numbers (it's only for real numbers)."
371
            );
372 4
        } elseif ($this->isZero()) {
373 1
            throw new \DomainException(
374 1
                "Decimal can't represent infinite numbers."
375
            );
376
        }
377
378 3
        return self::fromString(
379 3
            self::innerLog10($this->value, $this->scale, $scale !== null ? $scale+1 : $this->scale+1),
380
            $scale
381
        );
382
    }
383
384 93
    public function isZero(int $scale = null): bool
385
    {
386 93
        $cmp_scale = $scale !== null ? $scale : $this->scale;
387
388 93
        return (\bccomp(self::innerRound($this->value, $cmp_scale), '0', $cmp_scale) === 0);
389
    }
390
391 28
    public function isPositive(): bool
392
    {
393 28
        return ($this->value[0] !== '-' && !$this->isZero());
394
    }
395
396 68
    public function isNegative(): bool
397
    {
398 68
        return ($this->value[0] === '-');
399
    }
400
401 3
    public function isInteger(): bool
402
    {
403 3
        return (\preg_match('/^[+\-]?[0-9]+(\.0+)?$/', $this->value, $captures) === 1);
404
    }
405
406
    /**
407
     * Equality comparison between this object and $b
408
     * @param  Decimal $b
409
     * @param integer $scale
410
     * @return boolean
411
     */
412 115
    public function equals(Decimal $b, int $scale = null): bool
413
    {
414 115
        self::paramsValidation($b, $scale);
415
416 115
        if ($this === $b) {
417 2
            return true;
418
        } else {
419 114
            $cmp_scale = $scale !== null ? $scale : \max($this->scale, $b->scale);
420
421
            return (
422 114
                \bccomp(
423 114
                    self::innerRound($this->value, $cmp_scale),
424 114
                    self::innerRound($b->value, $cmp_scale),
425
                    $cmp_scale
426 114
                ) === 0
427
            );
428
        }
429
    }
430
431
    /**
432
     * $this > $b : returns 1 , $this < $b : returns -1 , $this == $b : returns 0
433
     *
434
     * @param  Decimal $b
435
     * @param  integer $scale
436
     * @return integer
437
     */
438 41
    public function comp(Decimal $b, int $scale = null): int
439
    {
440 41
        self::paramsValidation($b, $scale);
441
442 41
        if ($this === $b) {
443 7
            return 0;
444
        }
445
446 40
        $cmp_scale = $scale !== null ? $scale : \max($this->scale, $b->scale);
447
448 40
        return \bccomp(
449 40
            self::innerRound($this->value, $cmp_scale),
450 40
            self::innerRound($b->value, $cmp_scale),
451
            $cmp_scale
452
        );
453
    }
454
455
    /**
456
     * Returns the element's additive inverse.
457
     * @return Decimal
458
     */
459 15
    public function additiveInverse(): Decimal
460
    {
461 15
        if ($this->isZero()) {
462 1
            return $this;
463 14
        } elseif ($this->isNegative()) {
464 12
            $value = \substr($this->value, 1);
465
        } else { // if ($this->isPositive()) {
466 2
            $value = '-' . $this->value;
467
        }
468
469 14
        return new static($value, $this->scale);
470
    }
471
472
473
    /**
474
     * "Rounds" the Decimal to have at most $scale digits after the point
475
     * @param  integer $scale
476
     * @return Decimal
477
     */
478 52
    public function round(int $scale = 0): Decimal
479
    {
480 52
        if ($scale >= $this->scale) {
481 21
            return $this;
482
        }
483
484 51
        return self::fromString(self::innerRound($this->value, $scale));
485
    }
486
487
    /**
488
     * "Ceils" the Decimal to have at most $scale digits after the point
489
     * @param  integer $scale
490
     * @return Decimal
491
     */
492 4
    public function ceil($scale = 0): Decimal
493
    {
494 4
        if ($scale >= $this->scale) {
495 2
            return $this;
496
        }
497
498 3
        if ($this->isNegative()) {
499 1
            return self::fromString(\bcadd($this->value, '0', $scale));
500
        }
501
502 2
        return $this->innerTruncate($scale);
503
    }
504
505 28
    private function innerTruncate(int $scale = 0, bool $ceil = true): Decimal
506
    {
507 28
        $rounded = \bcadd($this->value, '0', $scale);
508
509 28
        $rlen = \strlen($rounded);
510 28
        $tlen = \strlen($this->value);
511
512 28
        $mustTruncate = false;
513 28
        for ($i=$tlen-1; $i >= $rlen; $i--) {
514 28
            if ((int)$this->value[$i] > 0) {
515 28
                $mustTruncate = true;
516 28
                break;
517
            }
518
        }
519
520 28
        if ($mustTruncate) {
521 28
            $rounded = $ceil
522 2
                ? \bcadd($rounded, \bcpow('10', (string)-$scale, $scale), $scale)
523 28
                : \bcsub($rounded, \bcpow('10', (string)-$scale, $scale), $scale);
524
        }
525
526 28
        return self::fromString($rounded, $scale);
527
    }
528
529
    /**
530
     * "Floors" the Decimal to have at most $scale digits after the point
531
     * @param  integer $scale
532
     * @return Decimal
533
     */
534 47
    public function floor(int $scale = 0): Decimal
535
    {
536 47
        if ($scale >= $this->scale) {
537 38
            return $this;
538
        }
539
540 38
        if ($this->isNegative()) {
541 26
            return $this->innerTruncate($scale, false);
542
        }
543
544 35
        return self::fromString(\bcadd($this->value, '0', $scale));
545
    }
546
547
    /**
548
     * Returns the absolute value (always a positive number)
549
     * @return Decimal
550
     */
551 20
    public function abs(): Decimal
552
    {
553 20
        return ($this->isZero() || $this->isPositive())
554 18
            ? $this
555 20
            : $this->additiveInverse();
556
    }
557
558
    /**
559
     * Calculate modulo with a decimal
560
     * @param Decimal $d
561
     * @param integer $scale
562
     * @return $this % $d
563
     */
564 27
    public function mod(Decimal $d, int $scale = null): Decimal
565
    {
566 27
        $div = $this->div($d, 1)->floor();
567 27
        return $this->sub($div->mul($d), $scale);
568
    }
569
570
    /**
571
     * Calculates the sine of this method with the highest possible accuracy
572
     * Note that accuracy is limited by the accuracy of predefined PI;
573
     *
574
     * @param integer $scale
575
     * @return Decimal sin($this)
576
     */
577 13
    public function sin(int $scale = null): Decimal
578
    {
579
        // First normalise the number in the [0, 2PI] domain
580 13
        $x = $this->mod(DecimalConstants::PI()->mul(Decimal::fromString("2")));
581
582
        // PI has only 32 significant numbers
583 13
        $scale = (null === $scale) ? 32 : $scale;
584
585 13
        return self::factorialSerie(
586
            $x,
587 13
            DecimalConstants::zero(),
588
            function ($i) {
589 13
                return ($i % 2 === 1) ? (
590 13
                ($i % 4 === 1) ? DecimalConstants::one() : DecimalConstants::negativeOne()
591 13
                ) : DecimalConstants::zero();
592 13
            },
593
            $scale
594
        );
595
    }
596
597
    /**
598
     * Calculates the cosecant of this with the highest possible accuracy
599
     * Note that accuracy is limited by the accuracy of predefined PI;
600
     *
601
     * @param integer $scale
602
     * @return Decimal
603
     */
604 3
    public function cosec(int $scale = null): Decimal
605
    {
606 3
        $sin = $this->sin($scale + 2);
607 3
        if ($sin->isZero()) {
608
            throw new \DomainException(
609
                "The cosecant of this 'angle' is undefined."
610
            );
611
        }
612
613 3
        return DecimalConstants::one()->div($sin)->round($scale);
614
    }
615
616
    /**
617
     * Calculates the cosine of this method with the highest possible accuracy
618
     * Note that accuracy is limited by the accuracy of predefined PI;
619
     *
620
     * @param integer $scale
621
     * @return Decimal cos($this)
622
     */
623 13
    public function cos(int $scale = null): Decimal
624
    {
625
        // First normalise the number in the [0, 2PI] domain
626 13
        $x = $this->mod(DecimalConstants::PI()->mul(Decimal::fromString("2")));
627
628
        // PI has only 32 significant numbers
629 13
        $scale = ($scale === null) ? 32 : $scale;
630
631 13
        return self::factorialSerie(
632
            $x,
633 13
            DecimalConstants::one(),
634
            function ($i) {
635 13
                return ($i % 2 === 0) ? (
636 13
                    ($i % 4 === 0) ? DecimalConstants::one() : DecimalConstants::negativeOne()
637 13
                ) : DecimalConstants::zero();
638 13
            },
639
            $scale
640
        );
641
    }
642
643
    /**
644
     * Calculates the secant of this with the highest possible accuracy
645
     * Note that accuracy is limited by the accuracy of predefined PI;
646
     *
647
     * @param integer $scale
648
     * @return Decimal
649
     */
650 3
    public function sec(int $scale = null): Decimal
651
    {
652 3
        $cos = $this->cos($scale + 2);
653 3
        if ($cos->isZero()) {
654
            throw new \DomainException(
655
                "The secant of this 'angle' is undefined."
656
            );
657
        }
658
659 3
        return DecimalConstants::one()->div($cos)->round($scale);
660
    }
661
662
    /**
663
     *	Calculates the arcsine of this with the highest possible accuracy
664
     *
665
     * @param integer $scale
666
     * @return Decimal
667
     */
668 5
    public function arcsin(int $scale = null): Decimal
669
    {
670 5
        if($this->comp(DecimalConstants::one(), $scale + 2) === 1 || $this->comp(DecimalConstants::negativeOne(), $scale + 2) === -1) {
671 2
            throw new \DomainException(
672 2
                "The arcsin of this number is undefined."
673
            );
674
        }
675
676 3
        if ($this->round($scale)->isZero()) {
677
            return DecimalConstants::zero();
678
        }
679 3
        if ($this->round($scale)->equals(DecimalConstants::one())) {
680 1
            return DecimalConstants::pi()->div(Decimal::fromInteger(2))->round($scale);
681
        }
682 2
        if ($this->round($scale)->equals(DecimalConstants::negativeOne())) {
683 1
            return DecimalConstants::pi()->div(Decimal::fromInteger(-2))->round($scale);
684
        }
685
686 1
        $scale = ($scale === null) ? 32 : $scale;
687
688 1
        return self::powerSerie(
689
            $this,
690 1
            DecimalConstants::zero(),
691
            $scale
692
        );
693
    }
694
695
    /**
696
     *	Calculates the arccosine of this with the highest possible accuracy
697
     *
698
     * @param integer $scale
699
     * @return Decimal
700
     */
701 5
    public function arccos(int $scale = null): Decimal
702
    {
703 5
        if($this->comp(DecimalConstants::one(), $scale + 2) === 1 || $this->comp(DecimalConstants::negativeOne(), $scale + 2) === -1) {
704 2
            throw new \DomainException(
705 2
                "The arccos of this number is undefined."
706
            );
707
        }
708
709 3
        $piOverTwo = DecimalConstants::pi()->div(Decimal::fromInteger(2), $scale + 2)->round($scale);
710
711 3
        if ($this->round($scale)->isZero()) {
712
            return $piOverTwo;
713
        }
714 3
        if ($this->round($scale)->equals(DecimalConstants::one())) {
715 1
            return DecimalConstants::zero();
716
        }
717 2
        if ($this->round($scale)->equals(DecimalConstants::negativeOne())) {
718 1
            return DecimalConstants::pi()->round($scale);
719
        }
720
721 1
        $scale = ($scale === null) ? 32 : $scale;
722
723 1
        return $piOverTwo->sub(
724 1
            self::powerSerie(
725
                $this,
726 1
                DecimalConstants::zero(),
727
                $scale
728
            )
729 1
        )->round($scale);
730
    }
731
732
    /**
733
     *	Calculates the arctangente of this with the highest possible accuracy
734
     *
735
     * @param integer $scale
736
     * @return Decimal
737
     */
738 3
    public function arctan(int $scale = null): Decimal
739
    {
740 3
        $piOverFour = DecimalConstants::pi()->div(Decimal::fromInteger(4), $scale + 2)->round($scale);
741
742 3
        if ($this->round($scale)->isZero()) {
743 1
            return DecimalConstants::zero();
744
        }
745 2
        if ($this->round($scale)->equals(DecimalConstants::one())) {
746
            return $piOverFour;
747
        }
748 2
        if ($this->round($scale)->equals(DecimalConstants::negativeOne())) {
749 1
            return DecimalConstants::negativeOne()->mul($piOverFour);
750
        }
751
752 1
        $scale = ($scale === null) ? 32 : $scale;
753
754 1
        return self::simplePowerSerie(
755
            $this,
756 1
            DecimalConstants::zero(),
757 1
            $scale + 2
758 1
        )->round($scale);
759
    }
760
761
    /**
762
     * Calculates the arccotangente of this with the highest possible accuracy
763
     *
764
     * @param integer $scale
765
     * @return Decimal
766
     */
767 3
    public function arccot(int $scale = null): Decimal
768
    {
769 3
        $scale = ($scale === null) ? 32 : $scale;
770
771 3
        $piOverTwo = DecimalConstants::pi()->div(Decimal::fromInteger(2), $scale + 2);
772 3
        if ($this->round($scale)->isZero()) {
773 1
            return $piOverTwo->round($scale);
774
        }
775
776 2
        $piOverFour = DecimalConstants::pi()->div(Decimal::fromInteger(4), $scale + 2);
777 2
        if ($this->round($scale)->equals(DecimalConstants::one())) {
778
            return $piOverFour->round($scale);
779
        }
780 2
        if ($this->round($scale)->equals(DecimalConstants::negativeOne())) {
781 1
            return DecimalConstants::negativeOne()->mul($piOverFour, $scale + 2)->round($scale);
782
        }
783
784 1
        return $piOverTwo->sub(
785 1
            self::simplePowerSerie(
786
                $this,
787 1
                DecimalConstants::zero(),
788 1
                $scale + 2
789
            )
790 1
        )->round($scale);
791
    }
792
793
    /**
794
     * Calculates the arcsecant of this with the highest possible accuracy
795
     *
796
     * @param integer $scale
797
     * @return Decimal
798
     */
799 5
    public function arcsec(int $scale = null): Decimal
800
    {
801 5
        if($this->comp(DecimalConstants::one(), $scale + 2) === -1 && $this->comp(DecimalConstants::negativeOne(), $scale + 2) === 1) {
802 1
            throw new \DomainException(
803 1
                "The arcsecant of this number is undefined."
804
            );
805
        }
806
807 4
        $piOverTwo = DecimalConstants::pi()->div(Decimal::fromInteger(2), $scale + 2)->round($scale);
808
809 4
        if ($this->round($scale)->equals(DecimalConstants::one())) {
810 1
            return DecimalConstants::zero();
811
        }
812 3
        if ($this->round($scale)->equals(DecimalConstants::negativeOne())) {
813 1
            return DecimalConstants::pi()->round($scale);
814
        }
815
816 2
        $scale = ($scale === null) ? 32 : $scale;
817
818 2
        return $piOverTwo->sub(
819 2
            self::powerSerie(
820 2
                DecimalConstants::one()->div($this, $scale + 2),
821 2
                DecimalConstants::zero(),
822 2
                $scale + 2
823
            )
824 2
        )->round($scale);
825
    }
826
827
    /**
828
     * Calculates the arccosecant of this with the highest possible accuracy
829
     *
830
     * @param integer $scale
831
     * @return Decimal
832
     */
833 5
    public function arccsc(int $scale = null): Decimal
834
    {
835 5
        if($this->comp(DecimalConstants::one(), $scale + 2) === -1 && $this->comp(DecimalConstants::negativeOne(), $scale + 2) === 1) {
836 1
            throw new \DomainException(
837 1
                "The arccosecant of this number is undefined."
838
            );
839
        }
840
841 4
        $scale = ($scale === null) ? 32 : $scale;
842
843 4
        if ($this->round($scale)->equals(DecimalConstants::one())) {
844 1
            return DecimalConstants::pi()->div(Decimal::fromInteger(2), $scale + 2)->round($scale);
845
        }
846 3
        if ($this->round($scale)->equals(DecimalConstants::negativeOne())) {
847 1
            return DecimalConstants::pi()->div(Decimal::fromInteger(-2), $scale + 2)->round($scale);
848
        }
849
850 2
        return self::powerSerie(
851 2
            DecimalConstants::one()->div($this, $scale + 2),
852 2
            DecimalConstants::zero(),
853 2
            $scale + 2
854 2
        )->round($scale);
855
    }
856
857
    /**
858
     * Returns exp($this), said in other words: e^$this .
859
     *
860
     * @param integer $scale
861
     * @return Decimal
862
     */
863 11
    public function exp(int $scale = null): Decimal
864
    {
865 11
        if ($this->isZero()) {
866 3
            return DecimalConstants::one();
867
        }
868
869 8
        $scale = $scale ?? \max(
870
            $this->scale,
871 8
            (int)($this->isNegative() ? self::innerLog10($this->value, $this->scale, 0) : self::DEFAULT_SCALE)
872
        );
873
874 8
        return self::factorialSerie(
875
            $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...
876
        );
877
    }
878
879
    /**
880
     * Internal method used to compute sin, cos and exp
881
     *
882
     * @param Decimal $x
883
     * @param Decimal $firstTerm
884
     * @param callable $generalTerm
885
     * @param $scale
886
     * @return Decimal
887
     */
888 28
    private static function factorialSerie (Decimal $x, Decimal $firstTerm, callable $generalTerm, int $scale): Decimal
889
    {
890 28
        $approx = $firstTerm;
891 28
        $change = DecimalConstants::One();
892
893 28
        $faculty = DecimalConstants::One();    // Calculates the faculty under the sign
894 28
        $xPowerN = DecimalConstants::One();    // Calculates x^n
895
896 28
        for ($i = 1; !$change->floor($scale+1)->isZero(); $i++) {
897
            // update x^n and n! for this walkthrough
898 28
            $xPowerN = $xPowerN->mul($x);
899 28
            $faculty = $faculty->mul(Decimal::fromInteger($i));
900
901
            /** @var Decimal $multiplier */
902 28
            $multiplier = $generalTerm($i);
903
904 28
            if (!$multiplier->isZero()) {
905 28
                $change = $multiplier->mul($xPowerN, $scale + 2)->div($faculty, $scale + 2);
906 28
                $approx = $approx->add($change, $scale + 2);
907
            }
908
        }
909
910 28
        return $approx->round($scale);
911
    }
912
913
914
    /**
915
     * Internal method used to compute arcsine and arcosine
916
     *
917
     * @param Decimal $x
918
     * @param Decimal $firstTerm
919
     * @param $scale
920
     * @return Decimal
921
     */
922 6
    private static function powerSerie (Decimal $x, Decimal $firstTerm, int $scale): Decimal
923
    {
924 6
        $approx = $firstTerm;
925 6
        $change = DecimalConstants::One();
926
927 6
        $xPowerN = DecimalConstants::One();     // Calculates x^n
928 6
        $factorN = DecimalConstants::One();      // Calculates a_n
0 ignored issues
show
Unused Code introduced by
$factorN is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
929
930 6
        $numerator = DecimalConstants::one();
931 6
        $denominator = DecimalConstants::one();
932
933 6
        for ($i = 1; !$change->floor($scale + 2)->isZero(); $i++) {
934 6
            $xPowerN = $xPowerN->mul($x);
935
936 6
            if ($i % 2 === 0) {
937 6
                $factorN = DecimalConstants::zero();
938 6
            } elseif ($i === 1) {
939 6
                $factorN = DecimalConstants::one();
940
            } else {
941 6
                $incrementNum = Decimal::fromInteger($i - 2);
942 6
                $numerator = $numerator->mul($incrementNum, $scale +2);
943
944 6
                $incrementDen = Decimal::fromInteger($i - 1);
945 6
                $increment = Decimal::fromInteger($i);
946
                $denominator = $denominator
947 6
                    ->div($incrementNum, $scale +2)
948 6
                    ->mul($incrementDen, $scale +2)
949 6
                    ->mul($increment, $scale +2);
950
951 6
                $factorN = $numerator->div($denominator, $scale + 2);
952
            }
953
954 6
            if (!$factorN->isZero()) {
955 6
                $change = $factorN->mul($xPowerN, $scale + 2);
956 6
                $approx = $approx->add($change, $scale + 2);
957
            }
958
        }
959
960 6
        return $approx->round($scale);
961
    }
962
963
    /**
964
     * Internal method used to compute arctan and arccotan
965
     *
966
     * @param Decimal $x
967
     * @param Decimal $firstTerm
968
     * @param $scale
969
     * @return Decimal
970
     */
971 2
    private static function simplePowerSerie (Decimal $x, Decimal $firstTerm, int $scale): Decimal
972
    {
973 2
        $approx = $firstTerm;
974 2
        $change = DecimalConstants::One();
975
976 2
        $xPowerN = DecimalConstants::One();     // Calculates x^n
977 2
        $sign = DecimalConstants::One();      // Calculates a_n
0 ignored issues
show
Unused Code introduced by
$sign is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
978
979 2
        for ($i = 1; !$change->floor($scale + 2)->isZero(); $i++) {
980 2
            $xPowerN = $xPowerN->mul($x);
981
982 2
            if ($i % 2 === 0) {
983 2
                $factorN = DecimalConstants::zero();
984
            } else {
985 2
                 if ($i % 4 === 1) {
986 2
                     $factorN = DecimalConstants::one()->div(Decimal::fromInteger($i), $scale + 2);
987
                 } else {
988 2
                     $factorN = DecimalConstants::negativeOne()->div(Decimal::fromInteger($i), $scale + 2);
989
                 }
990
            }
991
992 2
            if (!$factorN->isZero()) {
993 2
                $change = $factorN->mul($xPowerN, $scale + 2);
994 2
                $approx = $approx->add($change, $scale + 2);
995
            }
996
        }
997
998 2
        return $approx->round($scale);
999
    }
1000
1001
    /**
1002
     * Calculates the tangent of this method with the highest possible accuracy
1003
     * Note that accuracy is limited by the accuracy of predefined PI;
1004
     *
1005
     * @param integer $scale
1006
     * @return Decimal tan($this)
1007
     */
1008 4
    public function tan(int $scale = null): Decimal
1009
    {
1010 4
	    $cos = $this->cos($scale + 2);
1011 4
	    if ($cos->isZero()) {
1012 1
	        throw new \DomainException(
1013 1
	            "The tangent of this 'angle' is undefined."
1014
	        );
1015
	    }
1016
1017 3
	    return $this->sin($scale + 2)->div($cos)->round($scale);
1018
    }
1019
1020
    /**
1021
     * Calculates the cotangent 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
     * @return Decimal cotan($this)
1026
     */
1027 4
    public function cotan(int $scale = null): Decimal
1028
    {
1029 4
	    $sin = $this->sin($scale + 2);
1030 4
	    if ($sin->isZero()) {
1031 1
	        throw new \DomainException(
1032 1
	            "The cotangent of this 'angle' is undefined."
1033
	        );
1034
	    }
1035
1036 3
	    return $this->cos($scale + 2)->div($sin)->round($scale);
1037
    }
1038
1039
    /**
1040
     * Indicates if the passed parameter has the same sign as the method's bound object.
1041
     *
1042
     * @param Decimal $b
1043
     * @return bool
1044
     */
1045
    public function hasSameSign(Decimal $b): bool
1046
    {
1047
        return $this->isPositive() && $b->isPositive() || $this->isNegative() && $b->isNegative();
1048
    }
1049
1050 1
    public function asFloat(): float
1051
    {
1052 1
        return \floatval($this->value);
1053
    }
1054
1055 1
    public function asInteger(): int
1056
    {
1057 1
        return \intval($this->value);
1058
    }
1059
1060
    /**
1061
     * WARNING: use with caution! Return the inner representation of the class.
1062
     */
1063 11
    public function innerValue(): string
1064
    {
1065 11
        return $this->value;
1066
    }
1067
1068
    /**
1069
     * @return string
1070
     */
1071 58
    public function __toString(): string
1072
    {
1073 58
        return $this->value;
1074
    }
1075
1076
    /*
1077
     *
1078
     */
1079 7
    private static function fromExpNotationString(
1080
        int $scale = null,
1081
        string $sign,
1082
        string $mantissa,
1083
        int $nDecimals,
1084
        string $expSign,
1085
        int $expVal
1086
    ): array
1087
    {
1088 7
        $mantissaScale = \max($nDecimals, 0);
1089
1090 7
        if (self::normalizeSign($expSign) === '') {
1091 5
            $minScale = \max($mantissaScale - $expVal, 0);
1092 5
            $tmp_multiplier = \bcpow('10', (string)$expVal);
1093
        } else {
1094 2
            $minScale = $mantissaScale + $expVal;
1095 2
            $tmp_multiplier = \bcpow('10', (string)-$expVal, $expVal);
1096
        }
1097
1098
        $value = (
1099 7
            self::normalizeSign($sign) .
1100 7
            \bcmul(
1101
                $mantissa,
1102
                $tmp_multiplier,
1103 7
                \max($minScale, $scale ?? 0)
1104
            )
1105
        );
1106
1107 7
        return [$minScale, $value];
1108
    }
1109
1110
    /**
1111
     * "Rounds" the decimal string to have at most $scale digits after the point
1112
     *
1113
     * @param  string $value
1114
     * @param  int    $scale
1115
     * @return string
1116
     */
1117 135
    private static function innerRound(string $value, int $scale = 0): string
1118
    {
1119 135
        $rounded = \bcadd($value, '0', $scale);
1120
1121 135
        $diffDigit = \bcsub($value, $rounded, $scale+1);
1122 135
        $diffDigit = (int)$diffDigit[\strlen($diffDigit)-1];
1123
1124 135
        if ($diffDigit >= 5) {
1125 67
            $rounded = ($diffDigit >= 5 && $value[0] !== '-')
1126 63
                ? \bcadd($rounded, \bcpow('10', (string)-$scale, $scale), $scale)
1127 67
                : \bcsub($rounded, \bcpow('10', (string)-$scale, $scale), $scale);
1128
        }
1129
1130 135
        return $rounded;
1131
    }
1132
1133
    /**
1134
     * Calculates the logarithm (in base 10) of $value
1135
     *
1136
     * @param  string $value     The number we want to calculate its logarithm (only positive numbers)
1137
     * @param  int    $in_scale  Expected scale used by $value (only positive numbers)
1138
     * @param  int    $out_scale Scale used by the return value (only positive numbers)
1139
     * @return string
1140
     */
1141 22
    private static function innerLog10(string $value, int $in_scale, int $out_scale): string
1142
    {
1143 22
        $value_len = \strlen($value);
1144
1145 22
        $cmp = \bccomp($value, '1', $in_scale);
1146
1147
        switch ($cmp) {
1148 22
            case 1:
1149 9
                $value_log10_approx = $value_len - ($in_scale > 0 ? ($in_scale+2) : 1);
1150
1151 9
                return \bcadd(
1152 9
                    (string)$value_log10_approx,
1153 9
                    (string)\log10((float)\bcdiv(
1154
                        $value,
1155 9
                        \bcpow('10', (string)$value_log10_approx),
1156
                        \min($value_len, $out_scale)
1157
                    )),
1158
                    $out_scale
1159
                );
1160 14
            case -1:
1161 13
                \preg_match('/^0*\.(0*)[1-9][0-9]*$/', $value, $captures);
1162 13
                $value_log10_approx = -\strlen($captures[1])-1;
1163
1164 13
                return \bcadd(
1165 13
                    (string)$value_log10_approx,
1166 13
                    (string)\log10((float)\bcmul(
1167
                        $value,
1168 13
                        \bcpow('10', (string)-$value_log10_approx),
1169 13
                        $in_scale + $value_log10_approx
1170
                    )),
1171
                    $out_scale
1172
                );
1173
            default: // case 0:
1174 7
                return '0';
1175
        }
1176
    }
1177
1178
    /**
1179
     * Returns $base^$exponent
1180
     *
1181
     * @param  string $base
1182
     * @param  string $exponent   0 < $exponent < 1
1183
     * @param  int    $exp_scale Number of $exponent's significative digits
1184
     * @param  int    $out_scale Number of significative digits that we want to compute
1185
     * @return string
1186
     */
1187 3
    private static function innerPowWithLittleExponent(
1188
        string $base,
1189
        string $exponent,
1190
        int $exp_scale,
1191
        int $out_scale
1192
    ): string
1193
    {
1194 3
        $inner_scale = (int)\ceil($exp_scale * \log(10) / \log(2)) + 1;
1195
1196 3
        $result_a = '1';
1197 3
        $result_b = '0';
1198
1199 3
        $actual_index = 0;
1200 3
        $exponent_remaining = $exponent;
1201
1202 3
        while (\bccomp($result_a, $result_b, $out_scale) !== 0 && \bccomp($exponent_remaining, '0', $inner_scale) !== 0) {
1203 3
            $result_b = $result_a;
1204 3
            $index_info = self::computeSquareIndex($exponent_remaining, $actual_index, $exp_scale, $inner_scale);
1205 3
            $exponent_remaining = $index_info[1];
1206 3
            $result_a = \bcmul(
1207
                $result_a,
1208 3
                self::compute2NRoot($base, $index_info[0], 2*($out_scale+1)),
1209 3
                2*($out_scale+1)
1210
            );
1211
        }
1212
1213 3
        return self::innerRound($result_a, $out_scale);
1214
    }
1215
1216
    /**
1217
     * Auxiliar method. It helps us to decompose the exponent into many summands.
1218
     *
1219
     * @param  string $exponent_remaining
1220
     * @param  int    $actual_index
1221
     * @param  int    $exp_scale           Number of $exponent's significative digits
1222
     * @param  int    $inner_scale         ceil($exp_scale*log(10)/log(2))+1;
1223
     * @return array
1224
     */
1225 3
    private static function computeSquareIndex(
1226
        string $exponent_remaining,
1227
        int $actual_index,
1228
        int $exp_scale,
1229
        int $inner_scale
1230
    ): array
1231
    {
1232 3
        $actual_rt = \bcpow('0.5', (string)$actual_index, $exp_scale);
1233 3
        $r = \bcsub($exponent_remaining, $actual_rt, $inner_scale);
1234
1235 3
        while (\bccomp($r, '0', $exp_scale) === -1) {
1236 3
            ++$actual_index;
1237 3
            $actual_rt = \bcmul('0.5', $actual_rt, $inner_scale);
1238 3
            $r = \bcsub($exponent_remaining, $actual_rt, $inner_scale);
1239
        }
1240
1241 3
        return [$actual_index, $r];
1242
    }
1243
1244
    /**
1245
     * Auxiliar method. Computes $base^((1/2)^$index)
1246
     *
1247
     * @param  string  $base
1248
     * @param  integer $index
1249
     * @param  integer $out_scale
1250
     * @return string
1251
     */
1252 3
    private static function compute2NRoot(string $base, int $index, int $out_scale): string
1253
    {
1254 3
        $result = $base;
1255
1256 3
        for ($i = 0; $i < $index; $i++) {
1257 3
            $result = \bcsqrt($result, ($out_scale + 1) * ($index - $i) + 1);
1258
        }
1259
1260 3
        return self::innerRound($result, $out_scale);
1261
    }
1262
1263
    /**
1264
     * Validates basic constructor's arguments
1265
     * @param  mixed    $value
1266
     * @param  null|int  $scale
1267
     */
1268 158
    protected static function paramsValidation($value, int $scale = null)
1269
    {
1270 158
        if (null === $value) {
1271
            throw new \InvalidArgumentException('$value must be a non null number');
1272
        }
1273
1274 158
        if (null !== $scale && $scale < 0) {
1275 2
            throw new \InvalidArgumentException('$scale must be a positive integer');
1276
        }
1277 157
    }
1278
1279
    /**
1280
     * @return string
1281
     */
1282 128
    private static function normalizeSign(string $sign): string
1283
    {
1284 128
        if ('+' === $sign) {
1285 4
            return '';
1286
        }
1287
1288 128
        return $sign;
1289
    }
1290
1291
    /**
1292
     * Counts the number of significant digits of $val.
1293
     * Assumes a consistent internal state (without zeros at the end or the start).
1294
     *
1295
     * @param  Decimal $val
1296
     * @param  Decimal $abs $val->abs()
1297
     * @return int
1298
     */
1299 19
    private static function countSignificativeDigits(Decimal $val, Decimal $abs): int
1300
    {
1301 19
        return \strlen($val->value) - (
1302 19
            ($abs->comp(DecimalConstants::One()) === -1) ? 2 : \max($val->scale, 1)
1303 19
        ) - ($val->isNegative() ? 1 : 0);
1304
    }
1305
}
1306