Completed
Push — master ( 1eb9be...9028f9 )
by Andreu
02:32
created

Decimal::fromFloat()   D

Complexity

Conditions 10
Paths 17

Size

Total Lines 34
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 10.0145

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 34
ccs 18
cts 19
cp 0.9474
rs 4.8196
cc 10
eloc 22
nc 17
nop 2
crap 10.0145

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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

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

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

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

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
876
        );
877
    }
878
879
    /**
880
     * Internal method used to compute sin, cos and exp
881
     *
882
     * @param Decimal $x
883
     * @param Decimal $firstTerm
884
     * @param callable $generalTerm
885
     * @param $scale
886
     * @return Decimal
887
     */
888 28
    private static function factorialSerie (Decimal $x, Decimal $firstTerm, callable $generalTerm, int $scale): Decimal
889
    {
890 28
        $approx = $firstTerm;
891 28
        $change = DecimalConstants::One();
892
893 28
        $faculty = DecimalConstants::One();    // Calculates the faculty under the sign
894 28
        $xPowerN = DecimalConstants::One();    // Calculates x^n
895
896 28
        for ($i = 1; !$change->floor($scale+1)->isZero(); $i++) {
897
            // update x^n and n! for this walkthrough
898 28
            $xPowerN = $xPowerN->mul($x);
899 28
            $faculty = $faculty->mul(Decimal::fromInteger($i));
900
901
            /** @var Decimal $multiplier */
902 28
            $multiplier = $generalTerm($i);
903
904 28
            if (!$multiplier->isZero()) {
905 28
                $change = $multiplier->mul($xPowerN, $scale + 2)->div($faculty, $scale + 2);
906 28
                $approx = $approx->add($change, $scale + 2);
907
            }
908
        }
909
910 28
        return $approx->round($scale);
911
    }
912
913
914
    /**
915
     * Internal method used to compute arcsine and arcosine
916
     *
917
     * @param Decimal $x
918
     * @param Decimal $firstTerm
919
     * @param $scale
920
     * @return Decimal
921
     */
922 6
    private static function powerSerie (Decimal $x, Decimal $firstTerm, int $scale): Decimal
923
    {
924 6
        $approx = $firstTerm;
925 6
        $change = DecimalConstants::One();
926
927 6
        $xPowerN = DecimalConstants::One();     // Calculates x^n
928 6
        $factorN = DecimalConstants::One();      // Calculates a_n
0 ignored issues
show
Unused Code introduced by
$factorN is not used, you could remove the assignment.

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

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

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

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

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

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

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

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

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

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