Completed
Push — master ( d0e894...b9f967 )
by Andreu
7s
created

Decimal::arccot()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 24
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 5.2742

Importance

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