Completed
Push — master ( 908ad9...21a74f )
by Andreu
04:14
created

Decimal::arccot()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 25
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 5.0073

Importance

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