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

Decimal::innerPowWithLittleExponent()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 23
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 3

Importance

Changes 4
Bugs 2 Features 2
Metric Value
c 4
b 2
f 2
dl 0
loc 23
ccs 17
cts 17
cp 1
rs 9.0856
cc 3
eloc 15
nc 2
nop 4
crap 3
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 159
    private function __construct($value, $scale)
36
    {
37 159
        $this->value = $value;
38 159
        $this->scale = $scale;
39 159
    }
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 104
    public static function fromInteger($intValue)
99
    {
100 104
        self::paramsValidation($intValue, null);
101
102 103
        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 102
        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 121
    public static function fromString($strValue, $scale = null, $removeZeros = false)
173
    {
174 121
        self::paramsValidation($strValue, $scale);
175
176 119
        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 118
        } 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 112
            $value = self::normalizeSign($captures[1]) . $captures[2];
186 112
            $min_scale = isset($captures[4]) ? max(0, strlen($captures[4]) - 1) : 0;
187
188 118
        } 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 116
        $scale = ($scale!==null) ? $scale : $min_scale;
221 116
        if ($scale < $min_scale) {
222 57
            $value = self::innerRound($value, $scale);
223 57
        }
224 116
        if ($removeZeros) {
225
            $value = self::removeTrailingZeros($value, $scale);
226
        }
227
228 116
        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 37
    public function add(Decimal $b, $scale = null)
261
    {
262 37
        self::paramsValidation($b, $scale);
263
264 37
        if ($b->isInfinite()) {
265 1
            return $b;
266
        }
267
268 36
        return self::fromString(
269 36
            bcadd($this->value, $b->value, max($this->scale, $b->scale)),
270
            $scale
271 36
        );
272
    }
273
274
    /**
275
     * Subtracts two BigNumber objects
276
     * @param  Decimal $b
277
     * @param  integer $scale
278
     * @return Decimal
279
     */
280 32
    public function sub(Decimal $b, $scale = null)
281
    {
282 32
        self::paramsValidation($b, $scale);
283
284 32
        if ($b->isInfinite()) {
285 1
            return $b->additiveInverse();
286
        }
287
288 31
        return self::fromString(
289 31
            bcsub($this->value, $b->value, max($this->scale, $b->scale)),
290
            $scale
291 31
        );
292
    }
293
294
    /**
295
     * Multiplies two BigNumber objects
296
     * @param  Decimal $b
297
     * @param  integer $scale
298
     * @return Decimal
299
     */
300 47
    public function mul(Decimal $b, $scale = null)
301
    {
302 47
        self::paramsValidation($b, $scale);
303
304 46
        if ($b->isInfinite()) {
305 3
            return $b->mul($this);
306 43
        } elseif ($b->isZero()) {
307 1
            return DecimalConstants::Zero();
308
        }
309
310 43
        return self::fromString(
311 43
            bcmul($this->value, $b->value, $this->scale + $b->scale),
312
            $scale
313 43
        );
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 53
    public function div(Decimal $b, $scale = null)
327
    {
328 53
        self::paramsValidation($b, $scale);
329
330 53
        if ($b->isZero()) {
331 1
            throw new \DomainException("Division by zero is not allowed.");
332 53
        } elseif ($this->isZero() || $b->isInfinite()) {
333 2
            return DecimalConstants::Zero();
334
        } else {
335 51
            if ($scale !== null) {
336 46
                $divscale = $scale;
337 46
            } 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 51
            return self::fromString(
357 51
                bcdiv($this->value, $b->value, $divscale+1), $divscale
358 51
            );
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 95
    public function isZero($scale = null)
481
    {
482 95
        $cmp_scale = $scale !== null ? $scale : $this->scale;
483
484 95
        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 119
    public function isInfinite()
515
    {
516 119
        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 109
    public function equals(Decimal $b, $scale = null)
526
    {
527 109
        self::paramsValidation($b, $scale);
528
529 109
        if ($this === $b) {
530 4
            return true;
531 106
        } elseif ($b->isInfinite()) {
532
            return false;
533
        } else {
534 106
            $cmp_scale = $scale !== null ? $scale : max($this->scale, $b->scale);
535
536
            return (
537 106
                bccomp(
538 106
                    self::innerRound($this->value, $cmp_scale),
539 106
                    self::innerRound($b->value, $cmp_scale),
540
                    $cmp_scale
541 106
                ) == 0
542 106
            );
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 44
    public function round($scale = 0)
596
    {
597 44
        if ($scale >= $this->scale) {
598 13
            return $this;
599
        }
600
601 43
        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 43
    public function floor($scale = 0)
652
    {
653 43
        if ($scale >= $this->scale) {
654 5
            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 3
    public function arccot($scale = null) {
887 3
        $scale = ($scale === null) ? 32 : $scale;
888
889 3
        $piOverTwo = DecimalConstants::pi()->div(Decimal::fromInteger(2), $scale + 2);
890 3
        if ($this->round($scale)->isZero()) {
891 1
            return $piOverTwo->round($scale);
892
        }
893
894 2
        $piOverFour = DecimalConstants::pi()->div(Decimal::fromInteger(4), $scale + 2);
895 2
        if ($this->round($scale)->equals(DecimalConstants::one())) {
896
            return $piOverFour->round($scale);
897
        }
898 2
        if ($this->round($scale)->equals(DecimalConstants::negativeOne())) {
899 1
            return DecimalConstants::negativeOne()->mul($piOverFour, $scale + 2)->round($scale);
900
        }
901
902 1
        return $piOverTwo->sub(
903 1
            self::simplePowerSerie(
904 1
                $this,
905 1
                DecimalConstants::zero(),
906
                $scale + 2
907 1
            )
908 1
        )->round($scale);
909
    }
910
911
    /**
912
     * Returns exp($this), said in other words: e^$this .
913
     *
914
     * @param integer $scale
915
     * @return Decimal
916
     */
917 11
    public function exp($scale = null)
918
    {
919 11
        if ($this->isZero()) {
920 3
            return DecimalConstants::one();
921
        }
922
923 8
        $scale = ($scale === null) ? max(
924
            $this->scale,
925
            (int)($this->isNegative() ? self::innerLog10($this->value, $this->scale, 0) : 16)
926 8
        ) : $scale;
927
928 8
        return self::factorialSerie(
929
            $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 8
        );
931
    }
932
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 28
    private static function factorialSerie (Decimal $x, Decimal $firstTerm, callable $generalTerm, $scale)
943
    {
944 28
        $approx = $firstTerm;
945 28
        $change = InfiniteDecimal::getPositiveInfinite();
946
947 28
        $faculty = DecimalConstants::One();    // Calculates the faculty under the sign
948 28
        $xPowerN = DecimalConstants::One();    // Calculates x^n
949
950 28
        for ($i = 1; !$change->floor($scale+1)->isZero(); $i++) {
951
            // update x^n and n! for this walkthrough
952 28
            $xPowerN = $xPowerN->mul($x);
953 28
            $faculty = $faculty->mul(Decimal::fromInteger($i));
954
955 28
            $multiplier = $generalTerm($i);
956
957 28
            if (!$multiplier->isZero()) {
958 28
                $change = $multiplier->mul($xPowerN, $scale + 2)->div($faculty, $scale + 2);
959 28
                $approx = $approx->add($change, $scale + 2);
960 28
            }
961 28
        }
962
963 28
        return $approx->round($scale);
964
    }
965
966
967
    /**
968
     * Internal method used to compute arcsine and arcosine
969
     *
970
     * @param Decimal $x
971
     * @param Decimal $firstTerm
972
     * @param $scale
973
     * @return Decimal
974
     */
975 2
    private static function powerSerie (Decimal $x, Decimal $firstTerm, $scale)
976
    {
977 2
        $approx = $firstTerm;
978 2
        $change = InfiniteDecimal::getPositiveInfinite();
979
980 2
        $xPowerN = DecimalConstants::One();     // Calculates x^n
981 2
        $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
983 2
        $numerator = DecimalConstants::one();
984 2
        $denominator = DecimalConstants::one();
985
986 2
        for ($i = 1; !$change->floor($scale + 2)->isZero(); $i++) {
987 2
            $xPowerN = $xPowerN->mul($x);
988
989 2
            if ($i % 2 == 0) {
990 2
                $factorN = DecimalConstants::zero();
991 2
            } elseif ($i == 1) {
992 2
                $factorN = DecimalConstants::one();
993 2
            } else {
994 2
                $incrementNum = Decimal::fromInteger($i - 2);
995 2
                $numerator = $numerator->mul($incrementNum, $scale +2);
996
997 2
                $incrementDen = Decimal::fromInteger($i - 1);
998 2
                $increment = Decimal::fromInteger($i);
999
                $denominator = $denominator
1000 2
                    ->div($incrementNum, $scale +2)
1001 2
                    ->mul($incrementDen, $scale +2)
1002 2
                    ->mul($increment, $scale +2);
1003
1004 2
                $factorN = $numerator->div($denominator, $scale + 2);
1005
            }
1006
1007 2
            if (!$factorN->isZero()) {
1008 2
                $change = $factorN->mul($xPowerN, $scale + 2);
1009 2
                $approx = $approx->add($change, $scale + 2);
1010 2
            }
1011 2
        }
1012
1013 2
        return $approx->round($scale);
1014
    }
1015
1016
    /**
1017
     * Internal method used to compute arctan and arccotan
1018
     *
1019
     * @param Decimal $x
1020
     * @param Decimal $firstTerm
1021
     * @param $scale
1022
     * @return Decimal
1023
     */
1024 2
    private static function simplePowerSerie (Decimal $x, Decimal $firstTerm, $scale)
1025
    {
1026 2
        $approx = $firstTerm;
1027 2
        $change = InfiniteDecimal::getPositiveInfinite();
1028
1029 2
        $xPowerN = DecimalConstants::One();     // Calculates x^n
1030 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...
1031
1032 2
        for ($i = 1; !$change->floor($scale + 2)->isZero(); $i++) {
1033 2
            $xPowerN = $xPowerN->mul($x);
1034
1035 2
            if ($i % 2 === 0) {
1036 2
                $factorN = DecimalConstants::zero();
1037 2
            } else {
1038 2
                 if ($i % 4 === 1) {
1039 2
                     $factorN = DecimalConstants::one()->div(Decimal::fromInteger($i), $scale + 2);
1040 2
                 } else {
1041 2
                     $factorN = DecimalConstants::negativeOne()->div(Decimal::fromInteger($i), $scale + 2);
1042
                 }
1043
            }
1044
1045 2
            if (!$factorN->isZero()) {
1046 2
                $change = $factorN->mul($xPowerN, $scale + 2);
1047 2
                $approx = $approx->add($change, $scale + 2);
1048 2
            }
1049 2
        }
1050
1051 2
        return $approx->round($scale);
1052
    }
1053
1054
    /**
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
     *
1058
     * @param integer $scale
1059
     * @return Decimal tan($this)
1060
     */
1061 4
    public function tan($scale = null) {
1062 4
	    $cos = $this->cos($scale + 2);
1063 4
	    if ($cos->isZero()) {
1064 1
	        throw new \DomainException(
1065
	            "The tangent of this 'angle' is undefined."
1066 1
	        );
1067
	    }
1068
1069 3
	    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
     * @param integer $scale
1077
     * @return Decimal cotan($this)
1078
     */
1079 4
    public function cotan($scale = null)
1080
    {
1081 4
	    $sin = $this->sin($scale + 2);
1082 4
	    if ($sin->isZero()) {
1083 1
	        throw new \DomainException(
1084
	            "The cotangent of this 'angle' is undefined."
1085 1
	        );
1086
	    }
1087
1088 3
	    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 3
    public function hasSameSign(Decimal $b)
1098
    {
1099 3
        return $this->isPositive() && $b->isPositive() || $this->isNegative() && $b->isNegative();
1100
    }
1101
1102
    /**
1103
     * Return value as a float
1104
     *
1105
     * @return float
1106
     */
1107 1
    public function asFloat()
1108
    {
1109 1
        return floatval($this->value);
1110
    }
1111
1112
    /**
1113
     * Return value as a integer
1114
     *
1115
     * @return float
1116
     */
1117 1
    public function asInteger()
1118
    {
1119 1
        return intval($this->value);
1120
    }
1121
1122
    /**
1123
     * Return the inner representation of the class
1124
     * use with caution
1125
     *
1126
     * @return number
1127
     */
1128 13
    public function innerValue()
1129
    {
1130 13
        return $this->value;
1131
    }
1132
1133
    /**
1134
     * @return string
1135
     */
1136 50
    public function __toString()
1137
    {
1138 50
        return $this->value;
1139
    }
1140
1141
    /**
1142
     * "Rounds" the decimal string to have at most $scale digits after the point
1143
     *
1144
     * @param  string  $value
1145
     * @param  integer $scale
1146
     * @return string
1147
     */
1148 135
    private static function innerRound($value, $scale = 0)
1149
    {
1150 135
        $rounded = bcadd($value, '0', $scale);
1151
1152 135
        $diffDigit = bcsub($value, $rounded, $scale+1);
1153 135
        $diffDigit = (int)$diffDigit[strlen($diffDigit)-1];
1154
1155 135
        if ($diffDigit >= 5) {
1156 56
            if ($diffDigit >= 5 && $value[0] !== '-') {
1157 53
                $rounded = bcadd($rounded, bcpow('10', -$scale, $scale), $scale);
1158 53
            } else {
1159 28
                $rounded = bcsub($rounded, bcpow('10', -$scale, $scale), $scale);
1160
            }
1161 56
        }
1162
1163 135
        return $rounded;
1164
    }
1165
1166
    /**
1167
     * Calculates the logarithm (in base 10) of $value
1168
     *
1169
     * @param  string  $value     The number we want to calculate its logarithm (only positive numbers)
1170
     * @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
     * @return string
1173
     */
1174 22
    private static function innerLog10($value, $in_scale, $out_scale)
1175
    {
1176 22
        $value_len = strlen($value);
1177
1178 22
        $cmp = bccomp($value, '1', $in_scale);
1179
1180
        switch ($cmp) {
1181 22
            case 1:
1182 9
                $value_log10_approx = $value_len - ($in_scale > 0 ? ($in_scale+2) : 1);
1183
1184 9
                return bcadd(
1185 9
                    $value_log10_approx,
1186 9
                    log10(bcdiv(
1187 9
                        $value,
1188 9
                        bcpow('10', $value_log10_approx),
1189 9
                        min($value_len, $out_scale)
1190 9
                    )),
1191
                    $out_scale
1192 9
                );
1193 16
            case -1:
1194 13
                preg_match('/^0*\.(0*)[1-9][0-9]*$/', $value, $captures);
1195 13
                $value_log10_approx = -strlen($captures[1])-1;
1196
1197 13
                return bcadd(
1198 13
                    $value_log10_approx,
1199 13
                    log10(bcmul(
1200 13
                        $value,
1201 13
                        bcpow('10', -$value_log10_approx),
1202
                        $in_scale + $value_log10_approx
1203 13
                    )),
1204
                    $out_scale
1205 13
                );
1206 9
            default: // case 0:
1207 9
                return '0';
1208 9
        }
1209
    }
1210
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 3
    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
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 3
            $index_info = self::computeSquareIndex($exponent_remaining, $actual_index, $exp_scale, $inner_scale);
1233 3
            $exponent_remaining = $index_info[1];
1234 3
            $result_a = bcmul(
1235 3
                $result_a,
1236 3
                self::compute2NRoot($base, $index_info[0], 2*($out_scale+1)),
1237 3
                2*($out_scale+1)
1238 3
            );
1239 3
        }
1240
1241 3
        return self::innerRound($result_a, $out_scale);
1242
    }
1243
1244
    /**
1245
     * Auxiliar method. It helps us to decompose the exponent into many summands.
1246
     *
1247
     * @param  string  $exponent_remaining
1248
     * @param  integer $actual_index
1249
     * @param  integer $exp_scale           Number of $exponent's significative digits
1250
     * @param  integer $inner_scale         ceil($exp_scale*log(10)/log(2))+1;
1251
     * @return string
1252
     */
1253 3
    private static function computeSquareIndex($exponent_remaining, $actual_index, $exp_scale, $inner_scale)
1254
    {
1255 3
        $actual_rt = bcpow('0.5', $actual_index, $exp_scale);
1256 3
        $r = bcsub($exponent_remaining, $actual_rt, $inner_scale);
1257
1258 3
        while (bccomp($r, 0, $exp_scale) === -1) {
1259 3
            ++$actual_index;
1260 3
            $actual_rt = bcmul('0.5', $actual_rt, $inner_scale);
1261 3
            $r = bcsub($exponent_remaining, $actual_rt, $inner_scale);
1262 3
        }
1263
1264 3
        return [$actual_index, $r];
1265
    }
1266
1267
    /**
1268
     * Auxiliar method. Computes $base^((1/2)^$index)
1269
     *
1270
     * @param  string  $base
1271
     * @param  integer $index
1272
     * @param  integer $out_scale
1273
     * @return string
1274
     */
1275 3
    private static function compute2NRoot($base, $index, $out_scale)
1276
    {
1277 3
        $result = $base;
1278
1279 3
        for ($i=0; $i<$index; $i++) {
1280 3
            $result = bcsqrt($result, ($out_scale+1)*($index-$i)+1);
1281 3
        }
1282
1283 3
        return self::innerRound($result, $out_scale);
1284
    }
1285
1286
    /**
1287
     * Validates basic constructor's arguments
1288
     * @param  mixed    $value
1289
     * @param  integer  $scale
1290
     */
1291 173
    protected static function paramsValidation($value, $scale)
1292
    {
1293 173
        if ($value === null) {
1294 1
            throw new \InvalidArgumentException('$value must be a non null number');
1295
        }
1296
1297 172
        if ($scale !== null && (!is_int($scale) || $scale < 0)) {
1298 3
            throw new \InvalidArgumentException('$scale must be a positive integer');
1299
        }
1300 170
    }
1301
1302
    /**
1303
     * @return string
1304
     */
1305 116
    private static function normalizeSign($sign)
1306
    {
1307 116
        if ($sign==='+') {
1308 4
            return '';
1309
        }
1310
1311 116
        return $sign;
1312
    }
1313
1314 1
    private static function removeTrailingZeros($strValue, &$scale)
1315
    {
1316 1
        preg_match('/^[+\-]?[0-9]+(\.([0-9]*[1-9])?(0+)?)?$/', $strValue, $captures);
1317
1318 1
        if (count($captures) === 4) {
1319 1
            $toRemove = strlen($captures[3]);
1320 1
            $scale = strlen($captures[2]);
1321 1
            $strValue = substr($strValue, 0, strlen($strValue)-$toRemove-($scale===0 ? 1 : 0));
1322 1
        }
1323
1324 1
        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 19
    private static function countSignificativeDigits(Decimal $val, Decimal $abs)
1336
    {
1337 19
        return strlen($val->value) - (
1338 19
            ($abs->comp(DecimalConstants::One()) === -1) ? 2 : max($val->scale, 1)
1339 19
        ) - ($val->isNegative() ? 1 : 0);
1340
    }
1341
}
1342