Issues (2)

src/BCMathExtended/BC.php (2 issues)

Labels
Severity
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BCMathExtended;
6
7
use Closure;
8
use InvalidArgumentException;
9
10
class BC
11
{
12
    public const COMPARE_EQUAL = 0;
13
14
    public const COMPARE_LEFT_GRATER = 1;
15
16
    public const COMPARE_RIGHT_GRATER = -1;
17
18
    protected const DEFAULT_SCALE = 100;
19
20
    protected const MAX_BASE = 256;
21
22
    protected const BIT_OPERATOR_AND = 'and';
23
24
    protected const BIT_OPERATOR_OR = 'or';
25
26
    protected const BIT_OPERATOR_XOR = 'xor';
27
28
    protected static bool $trimTrailingZeroes = true;
29
30 2
    public static function rand(string $min, string $max): string
31
    {
32 2
        $max = static::convertScientificNotationToString($max);
33 2
        $min = static::convertScientificNotationToString($min);
34
35 2
        $difference = static::add(static::sub($max, $min), '1');
36 2
        $randPercent = static::div((string)mt_rand(), (string)mt_getrandmax(), 8);
37
38 2
        return static::add($min, static::mul($difference, $randPercent, 8), 0);
39
    }
40
41 397
    public static function convertScientificNotationToString(string $number): string
42
    {
43
        // check if number is in scientific notation, first use stripos as is faster than preg_match
44 397
        if (stripos($number, 'E') !== false && preg_match('/(-?(\d+\.)?\d+)E([+-]?)(\d+)/i', $number, $regs)) {
45
            // calculate final scale of number
46 76
            $scale = (int)$regs[4] + static::getDecimalsLength($regs[1]);
47 76
            $pow = static::pow('10', $regs[4], $scale);
48 76
            if ($regs[3] === '-') {
49 31
                $number = static::div($regs[1], $pow, $scale);
50
            } else {
51 45
                $number = static::mul($pow, $regs[1], $scale);
52
            }
53 76
            $number = static::formatTrailingZeroes($number);
54
        }
55
56 397
        return static::parseNumber($number);
57
    }
58
59 116
    public static function getDecimalsLength(string $number): int
60
    {
61 116
        if (static::isFloat($number)) {
62 105
            return strcspn(strrev($number), '.');
63
        }
64
65 11
        return 0;
66
    }
67
68 362
    protected static function isFloat(string $number): bool
69
    {
70 362
        return strpos($number, '.') !== false;
71
    }
72
73 170
    public static function pow(string $base, string $exponent, ?int $scale = null): string
74
    {
75 170
        $base = static::convertScientificNotationToString($base);
76 170
        $exponent = static::convertScientificNotationToString($exponent);
77
78 170
        if (static::isFloat($exponent)) {
79 9
            $r = static::powFractional($base, $exponent, $scale);
80 162
        } elseif ($scale === null) {
81 78
            $r = bcpow($base, $exponent);
82
        } else {
83 87
            $r = bcpow($base, $exponent, $scale);
84
        }
85
86 170
        return static::formatTrailingZeroes($r);
87
    }
88
89 9
    protected static function powFractional(string $base, string $exponent, ?int $scale = null): string
90
    {
91
        // we need to increased scale to get correct results and avoid rounding error
92 9
        $currentScale = $scale ?? static::getScale();
93 9
        $increasedScale = $currentScale * 2;
94
95
        // add zero to trim scale
96 9
        return static::parseNumber(
97 9
            static::add(
98 9
                static::exp(static::mul($exponent, static::log($base), $increasedScale)),
99 9
                '0',
100 9
                $currentScale
101 9
            )
102 9
        );
103
    }
104
105 40
    public static function getScale(): int
106
    {
107 40
        return bcscale();
108
    }
109
110 397
    protected static function parseNumber(string $number): string
111
    {
112 397
        $number = str_replace('+', '', (string)filter_var($number, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION));
113 397
        if ($number === '-0' || !is_numeric($number)) {
114 29
            return '0';
115
        }
116
117 386
        return $number;
118
    }
119
120 183
    public static function add(string $leftOperand, string $rightOperand, ?int $scale = null): string
121
    {
122 183
        $leftOperand = static::convertScientificNotationToString($leftOperand);
123 183
        $rightOperand = static::convertScientificNotationToString($rightOperand);
124
125 183
        if ($scale === null) {
126 41
            $r = bcadd($leftOperand, $rightOperand);
127
        } else {
128 145
            $r = bcadd($leftOperand, $rightOperand, $scale);
129
        }
130
131 183
        return static::formatTrailingZeroes($r);
132
    }
133
134 309
    protected static function formatTrailingZeroes(string $number): string
135
    {
136 309
        if (self::$trimTrailingZeroes) {
137 309
            return static::trimTrailingZeroes($number);
138
        }
139
140 1
        return $number;
141
    }
142
143 335
    protected static function trimTrailingZeroes(string $number): string
144
    {
145 335
        if (static::isFloat($number)) {
146 229
            $number = rtrim($number, '0');
147
        }
148
149 335
        return rtrim($number, '.') ?: '0';
150
    }
151
152 17
    public static function exp(string $number): string
153
    {
154 17
        $scale = static::DEFAULT_SCALE;
155 17
        $result = '1';
156 17
        for ($i = 299; $i > 0; --$i) {
157 17
            $result = static::add(static::mul(static::div($result, (string)$i, $scale), $number, $scale), '1', $scale);
158
        }
159
160 17
        return $result;
161
    }
162
163 184
    public static function mul(string $leftOperand, string $rightOperand, ?int $scale = null): string
164
    {
165 184
        $leftOperand = static::convertScientificNotationToString($leftOperand);
166 184
        $rightOperand = static::convertScientificNotationToString($rightOperand);
167
168 184
        if ($scale === null) {
169 55
            $r = bcmul($leftOperand, $rightOperand);
170
        } else {
171 132
            $r = bcmul($leftOperand, $rightOperand, $scale);
172
        }
173
174 184
        return static::formatTrailingZeroes($r);
175
    }
176
177 145
    public static function div(string $dividend, string $divisor, ?int $scale = null): string
178
    {
179 145
        $dividend = static::convertScientificNotationToString($dividend);
180 145
        $divisor = static::convertScientificNotationToString($divisor);
181
182 145
        if (static::trimTrailingZeroes($divisor) === '0') {
183 1
            throw new InvalidArgumentException('Division by zero');
184
        }
185
186 144
        if ($scale === null) {
187 42
            $r = bcdiv($dividend, $divisor);
188
        } else {
189 111
            $r = bcdiv($dividend, $divisor, $scale);
190
        }
191
192 144
        return static::formatTrailingZeroes((string)$r);
193
    }
194
195 14
    public static function log(string $number): string
196
    {
197 14
        $number = static::convertScientificNotationToString($number);
198 14
        if ($number === '0') {
199 1
            return '-INF';
200
        }
201 13
        if (static::COMPARE_RIGHT_GRATER === static::comp($number, '0')) {
202 1
            return 'NAN';
203
        }
204 12
        $scale = static::DEFAULT_SCALE;
205 12
        $m = (string)log((float)$number);
206 12
        $x = static::sub(static::div($number, static::exp($m), $scale), '1', $scale);
207 12
        $res = '0';
208 12
        $pow = '1';
209 12
        $i = 1;
210
        do {
211 12
            $pow = static::mul($pow, $x, $scale);
212 12
            $sum = static::div($pow, (string)$i, $scale);
213 12
            if ($i % 2 === 1) {
214 12
                $res = static::add($res, $sum, $scale);
215
            } else {
216 11
                $res = static::sub($res, $sum, $scale);
217
            }
218 12
            ++$i;
219 12
        } while (static::comp($sum, '0', $scale));
220
221 12
        return static::add($res, $m, $scale);
222
    }
223
224 66
    public static function comp(string $leftOperand, string $rightOperand, ?int $scale = null): int
225
    {
226 66
        $leftOperand = static::convertScientificNotationToString($leftOperand);
227 66
        $rightOperand = static::convertScientificNotationToString($rightOperand);
228
229 66
        if ($scale === null) {
230 54
            return bccomp($leftOperand, $rightOperand, max(strlen($leftOperand), strlen($rightOperand)));
231
        }
232
233 24
        return bccomp(
234 24
            $leftOperand,
235 24
            $rightOperand,
236 24
            $scale
237 24
        );
238
    }
239
240 83
    public static function sub(string $leftOperand, string $rightOperand, ?int $scale = null): string
241
    {
242 83
        $leftOperand = static::convertScientificNotationToString($leftOperand);
243 83
        $rightOperand = static::convertScientificNotationToString($rightOperand);
244
245 83
        if ($scale === null) {
246 44
            $r = bcsub($leftOperand, $rightOperand);
247
        } else {
248 42
            $r = bcsub($leftOperand, $rightOperand, $scale);
249
        }
250
251 83
        return static::formatTrailingZeroes($r);
252
    }
253
254 6
    public static function sqrt(string $number, ?int $scale = null): string
255
    {
256 6
        $number = static::convertScientificNotationToString($number);
257
258 6
        if ($scale === null) {
259 1
            $r = bcsqrt($number);
260
        } else {
261 6
            $r = bcsqrt($number, $scale);
262
        }
263
264 6
        return static::formatTrailingZeroes((string)$r);
265
    }
266
267 1
    public static function setTrimTrailingZeroes(bool $flag): void
268
    {
269 1
        self::$trimTrailingZeroes = $flag;
270
    }
271
272
    /**
273
     * @param  mixed  $values
274
     */
275 1
    public static function max(...$values): ?string
276
    {
277 1
        $max = null;
278 1
        foreach (static::parseValues($values) as $number) {
279 1
            $number = static::convertScientificNotationToString((string)$number);
280 1
            if ($max === null) {
281 1
                $max = $number;
282 1
            } elseif (static::comp($max, $number) === static::COMPARE_RIGHT_GRATER) {
0 ignored issues
show
$max of type null is incompatible with the type string expected by parameter $leftOperand of BCMathExtended\BC::comp(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

282
            } elseif (static::comp(/** @scrutinizer ignore-type */ $max, $number) === static::COMPARE_RIGHT_GRATER) {
Loading history...
283 1
                $max = $number;
284
            }
285
        }
286
287 1
        return $max;
288
    }
289
290 2
    protected static function parseValues(array $values): array
291
    {
292 2
        if (is_array($values[0])) {
293 2
            $values = $values[0];
294
        }
295
296 2
        return $values;
297
    }
298
299
    /**
300
     * @param  mixed  $values
301
     */
302 1
    public static function min(...$values): ?string
303
    {
304 1
        $min = null;
305 1
        foreach (static::parseValues($values) as $number) {
306 1
            $number = static::convertScientificNotationToString((string)$number);
307 1
            if ($min === null) {
308 1
                $min = $number;
309 1
            } elseif (static::comp($min, $number) === static::COMPARE_LEFT_GRATER) {
0 ignored issues
show
$min of type null is incompatible with the type string expected by parameter $leftOperand of BCMathExtended\BC::comp(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

309
            } elseif (static::comp(/** @scrutinizer ignore-type */ $min, $number) === static::COMPARE_LEFT_GRATER) {
Loading history...
310 1
                $min = $number;
311
            }
312
        }
313
314 1
        return $min;
315
    }
316
317 14
    public static function powMod(
318
        string $base,
319
        string $exponent,
320
        string $modulus,
321
        ?int $scale = null
322
    ): string {
323 14
        $base = static::convertScientificNotationToString($base);
324 14
        $exponent = static::convertScientificNotationToString($exponent);
325
326 14
        if (static::isNegative($exponent)) {
327 1
            throw new InvalidArgumentException('Exponent can\'t be negative');
328
        }
329
330 13
        if (static::trimTrailingZeroes($modulus) === '0') {
331 1
            throw new InvalidArgumentException('Modulus can\'t be zero');
332
        }
333
334
        // bcpowmod don't support floats
335
        if (
336 12
            static::isFloat($base)
337 9
            || static::isFloat($exponent)
338 12
            || static::isFloat($modulus)
339
        ) {
340 5
            $r = static::mod(static::pow($base, $exponent, $scale), $modulus, $scale);
341 7
        } elseif ($scale === null) {
342 1
            $r = bcpowmod($base, $exponent, $modulus);
343
        } else {
344 6
            $r = bcpowmod($base, $exponent, $modulus, $scale);
345
        }
346
347 12
        return static::formatTrailingZeroes((string)$r);
348
    }
349
350 182
    protected static function isNegative(string $number): bool
351
    {
352 182
        return strncmp('-', $number, 1) === 0;
353
    }
354
355 55
    public static function mod(string $dividend, string $divisor, ?int $scale = null): string
356
    {
357
        // bcmod is not working properly - for example bcmod(9.9999E-10, -0.00056, 9) should return '-0.000559999' but returns 0.0000000
358
        // let use this $x - floor($x/$y) * $y;
359 55
        return static::formatTrailingZeroes(
360 55
            static::sub(
361 55
                $dividend,
362 55
                static::mul(static::floor(static::div($dividend, $divisor, $scale)), $divisor, $scale),
363 55
                $scale
364 55
            )
365 55
        );
366
    }
367
368 102
    public static function floor(string $number): string
369
    {
370 102
        $number = static::trimTrailingZeroes(static::convertScientificNotationToString($number));
371 102
        if (static::isFloat($number)) {
372 46
            $result = 0;
373 46
            if (static::isNegative($number)) {
374 9
                --$result;
375
            }
376 46
            $number = static::add($number, (string)$result, 0);
377
        }
378
379 102
        return static::parseNumber($number);
380
    }
381
382 8
    public static function fact(string $number): string
383
    {
384 8
        $number = static::convertScientificNotationToString($number);
385
386 8
        if (static::isFloat($number)) {
387 1
            throw new InvalidArgumentException('Number has to be an integer');
388
        }
389 7
        if (static::isNegative($number)) {
390 1
            throw new InvalidArgumentException('Number has to be greater than or equal to 0');
391
        }
392
393 6
        $return = '1';
394 6
        for ($i = 2; $i <= $number; ++$i) {
395 5
            $return = static::mul($return, (string)$i);
396
        }
397
398 6
        return $return;
399
    }
400
401 7
    public static function hexdec(string $hex): string
402
    {
403 7
        $remainingDigits = str_replace('0x', '', substr($hex, 0, -1));
404 7
        $lastDigitToDecimal = (string)hexdec(substr($hex, -1));
405
406 7
        if ($remainingDigits === '') {
407 7
            return $lastDigitToDecimal;
408
        }
409
410 7
        return static::add(static::mul('16', static::hexdec($remainingDigits)), $lastDigitToDecimal, 0);
411
    }
412
413 6
    public static function dechex(string $decimal): string
414
    {
415 6
        $quotient = static::div($decimal, '16', 0);
416 6
        $remainderToHex = dechex((int)static::mod($decimal, '16'));
417
418 6
        if (static::comp($quotient, '0') === static::COMPARE_EQUAL) {
419 6
            return $remainderToHex;
420
        }
421
422 6
        return static::dechex($quotient) . $remainderToHex;
423
    }
424
425 12
    public static function bitAnd(
426
        string $leftOperand,
427
        string $rightOperand
428
    ): string {
429 12
        return static::bitOperatorHelper($leftOperand, $rightOperand, static::BIT_OPERATOR_AND);
430
    }
431
432 36
    protected static function bitOperatorHelper(string $leftOperand, string $rightOperand, string $operator): string
433
    {
434 36
        $leftOperand = static::convertScientificNotationToString($leftOperand);
435 36
        $rightOperand = static::convertScientificNotationToString($rightOperand);
436
437 36
        if (static::isFloat($leftOperand)) {
438 3
            throw new InvalidArgumentException('Left operator has to be an integer');
439
        }
440 33
        if (static::isFloat($rightOperand)) {
441 3
            throw new InvalidArgumentException('Right operator has to be an integer');
442
        }
443
444 30
        $leftOperandNegative = static::isNegative($leftOperand);
445 30
        $rightOperandNegative = static::isNegative($rightOperand);
446
447 30
        $leftOperand = static::dec2bin(static::abs($leftOperand));
448 30
        $rightOperand = static::dec2bin(static::abs($rightOperand));
449
450 30
        $maxLength = max(strlen($leftOperand), strlen($rightOperand));
451
452 30
        $leftOperand = static::alignBinLength($leftOperand, $maxLength);
453 30
        $rightOperand = static::alignBinLength($rightOperand, $maxLength);
454
455 30
        if ($leftOperandNegative) {
456 7
            $leftOperand = static::recalculateNegative($leftOperand);
457
        }
458 30
        if ($rightOperandNegative) {
459 7
            $rightOperand = static::recalculateNegative($rightOperand);
460
        }
461
462 30
        $isNegative = false;
463 30
        $result = '';
464 30
        if (static::BIT_OPERATOR_AND === $operator) {
465 10
            $result = $leftOperand & $rightOperand;
466 10
            $isNegative = ($leftOperandNegative and $rightOperandNegative);
467 20
        } elseif (static::BIT_OPERATOR_OR === $operator) {
468 12
            $result = $leftOperand | $rightOperand;
469 12
            $isNegative = ($leftOperandNegative or $rightOperandNegative);
470 8
        } elseif (static::BIT_OPERATOR_XOR === $operator) {
471 8
            $result = $leftOperand ^ $rightOperand;
472 8
            $isNegative = ($leftOperandNegative xor $rightOperandNegative);
473
        }
474
475 30
        if ($isNegative) {
476 8
            $result = static::recalculateNegative($result);
477
        }
478
479 30
        $result = static::bin2dec($result);
480
481 30
        return $isNegative ? '-' . $result : $result;
482
    }
483
484 36
    public static function dec2bin(string $number, int $base = self::MAX_BASE): string
485
    {
486 36
        return static::decBaseHelper(
487 36
            $base,
488 36
            static function (int $base) use ($number) {
489 35
                $value = '';
490 35
                if ($number === '0') {
491 30
                    return chr((int)$number);
492
                }
493
494 33
                while (self::comp($number, '0') !== self::COMPARE_EQUAL) {
495 33
                    $rest = self::mod($number, (string)$base);
496 33
                    $number = self::div($number, (string)$base);
497 33
                    $value = chr((int)$rest) . $value;
498
                }
499
500 33
                return $value;
501 36
            }
502 36
        );
503
    }
504
505 37
    protected static function decBaseHelper(int $base, Closure $closure): string
506
    {
507 37
        if ($base < 2 || $base > static::MAX_BASE) {
508 2
            throw new InvalidArgumentException('Invalid Base: ' . $base);
509
        }
510 35
        $orgScale = static::getScale();
511 35
        static::setScale(0);
512
513 35
        $value = $closure($base);
514
515 35
        static::setScale($orgScale);
516
517 35
        return $value;
518
    }
519
520 402
    public static function setScale(int $scale): void
521
    {
522 402
        bcscale($scale);
523
    }
524
525 45
    public static function abs(string $number): string
526
    {
527 45
        $number = static::convertScientificNotationToString($number);
528
529 45
        if (static::isNegative($number)) {
530 19
            $number = (string)substr($number, 1);
531
        }
532
533 45
        return static::parseNumber($number);
534
    }
535
536 30
    protected static function alignBinLength(string $string, int $length): string
537
    {
538 30
        return str_pad($string, $length, static::dec2bin('0'), STR_PAD_LEFT);
539
    }
540
541 11
    protected static function recalculateNegative(string $number): string
542
    {
543 11
        $xor = str_repeat(static::dec2bin((string)(static::MAX_BASE - 1)), strlen($number));
544 11
        $number ^= $xor;
545 11
        for ($i = strlen($number) - 1; $i >= 0; --$i) {
546 11
            $byte = ord($number[$i]);
547 11
            if (++$byte !== static::MAX_BASE) {
548 11
                $number[$i] = chr($byte);
549 11
                break;
550
            }
551
        }
552
553 11
        return $number;
554
    }
555
556 36
    public static function bin2dec(string $binary, int $base = self::MAX_BASE): string
557
    {
558 36
        return static::decBaseHelper(
559 36
            $base,
560 36
            static function (int $base) use ($binary) {
561 35
                $size = strlen($binary);
562 35
                $return = '0';
563 35
                for ($i = 0; $i < $size; ++$i) {
564 35
                    $element = ord($binary[$i]);
565 35
                    $power = self::pow((string)$base, (string)($size - $i - 1));
566 35
                    $return = self::add($return, self::mul((string)$element, $power));
567
                }
568
569 35
                return $return;
570 36
            }
571 36
        );
572
    }
573
574 14
    public static function bitOr(string $leftOperand, string $rightOperand): string
575
    {
576 14
        return static::bitOperatorHelper($leftOperand, $rightOperand, static::BIT_OPERATOR_OR);
577
    }
578
579 10
    public static function bitXor(string $leftOperand, string $rightOperand): string
580
    {
581 10
        return static::bitOperatorHelper($leftOperand, $rightOperand, static::BIT_OPERATOR_XOR);
582
    }
583
584 26
    public static function roundHalfEven(string $number, int $precision = 0): string
585
    {
586 26
        $number = static::convertScientificNotationToString($number);
587 26
        if (!static::isFloat($number)) {
588 1
            return $number;
589
        }
590
591 25
        $precessionPos = strpos($number, '.') + $precision + 1;
592 25
        if (strlen($number) <= $precessionPos) {
593 1
            return static::round($number, $precision);
594
        }
595
596 24
        if ($number[-1] !== '5') {
597 9
            return static::round($number, $precision);
598
        }
599
600 15
        $isPrevEven = $number[$precessionPos - 1] === '.'
601 4
            ? (int)$number[$precessionPos - 2] % 2 === 0
602 15
            : (int)$number[$precessionPos - 1] % 2 === 0;
603 15
        $isNegative = static::isNegative($number);
604
605 15
        if ($isPrevEven === $isNegative) {
606 7
            return static::roundUp($number, $precision);
607
        }
608
609 8
        return static::roundDown($number, $precision);
610
    }
611
612 58
    public static function round(string $number, int $precision = 0): string
613
    {
614 58
        $number = static::convertScientificNotationToString($number);
615 58
        if (static::isFloat($number)) {
616 47
            if (static::isNegative($number)) {
617 9
                $number = static::sub($number, '0.' . str_repeat('0', $precision) . '5', $precision);
618
            } else {
619 38
                $number = static::add($number, '0.' . str_repeat('0', $precision) . '5', $precision);
620
            }
621
        }
622
623 58
        return static::parseNumber($number);
624
    }
625
626 24
    public static function roundUp(string $number, int $precision = 0): string
627
    {
628 24
        $number = static::convertScientificNotationToString($number);
629 24
        if (!static::isFloat($number)) {
630 4
            return $number;
631
        }
632 20
        $multiply = static::pow('10', (string)abs($precision));
633
634 20
        return static::parseNumber(
635 20
            $precision < 0
636 20
                ?
637 4
                static::mul(
638 4
                    static::ceil(static::div($number, $multiply, static::getDecimalsLength($number))),
639 4
                    $multiply,
640 4
                    (int)abs($precision)
641 4
                )
642 20
                :
643 20
                static::div(
644 20
                    static::ceil(static::mul($number, $multiply, static::getDecimalsLength($number))),
645 20
                    $multiply,
646 20
                    $precision
647 20
                )
648 20
        );
649
    }
650
651 44
    public static function ceil(string $number): string
652
    {
653 44
        $number = static::trimTrailingZeroes(static::convertScientificNotationToString($number));
654 44
        if (static::isFloat($number)) {
655 29
            $result = 1;
656 29
            if (static::isNegative($number)) {
657 6
                --$result;
658
            }
659 29
            $number = static::add($number, (string)$result, 0);
660
        }
661
662 44
        return static::parseNumber($number);
663
    }
664
665 26
    public static function roundDown(string $number, int $precision = 0): string
666
    {
667 26
        $number = static::convertScientificNotationToString($number);
668 26
        if (!static::isFloat($number)) {
669 4
            return $number;
670
        }
671 22
        $multiply = static::pow('10', (string)abs($precision));
672
673 22
        return static::parseNumber(
674 22
            $precision < 0
675 22
                ?
676 4
                static::mul(
677 4
                    static::floor(static::div($number, $multiply, static::getDecimalsLength($number))),
678 4
                    $multiply,
679 4
                    (int)abs($precision)
680 4
                )
681 22
                :
682 22
                static::div(
683 22
                    static::floor(static::mul($number, $multiply, static::getDecimalsLength($number))),
684 22
                    $multiply,
685 22
                    $precision
686 22
                )
687 22
        );
688
    }
689
}
690