Completed
Push — master ( 77ceb5...8e1d61 )
by Fabrice
02:41
created

src/Math.php (3 issues)

Labels
Severity
1
<?php
2
3
/*
4
 * This file is part of OpinHelpers.
5
 *     (c) Fabrice de Stefanis / https://github.com/fab2s/OpinHelpers
6
 * This source file is licensed under the MIT license which you will
7
 * find in the LICENSE file or at https://opensource.org/licenses/MIT
8
 */
9
10
namespace fab2s\OpinHelpers;
11
12
/**
13
 * Class Math
14
 */
15
class Math
16
{
17
    /**
18
     * Default precision
19
     */
20
    const PRECISION = 9;
21
22
    /**
23
     * base <= 64 charlist
24
     */
25
    const BASECHAR_64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
26
27
    /**
28
     * base <= 62 char list
29
     */
30
    const BASECHAR_62 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
31
32
    /**
33
     * base <= 36 charlist
34
     */
35
    const BASECHAR_36 = '0123456789abcdefghijklmnopqrstuvwxyz';
36
37
    /**
38
     * base char cache for all supported bases (bellow 64)
39
     *
40
     * @var string[]
41
     */
42
    protected static $baseChars = [
43
        36 => self::BASECHAR_36,
44
        62 => self::BASECHAR_62,
45
        64 => self::BASECHAR_64,
46
    ];
47
48
    /**
49
     *  if set, will be used as default for all consecutive instances
50
     *
51
     * @var int
52
     */
53
    protected static $globalPrecision;
54
55
    /**
56
     * Used in static context, aligned with $globalPrecision, default to self::PRECISION
57
     *
58
     * @var int
59
     */
60
    protected static $staticPrecision = self::PRECISION;
61
62
    /**
63
     * @var bool
64
     */
65
    protected static $gmpSupport;
66
67
    /**
68
     * @var string
69
     */
70
    protected $number;
71
72
    /**
73
     * Instance precision, initialized with globalPrecision, default to self::PRECISION
74
     *
75
     * @var int
76
     */
77
    protected $precision = self::PRECISION;
78
79
    /**
80
     * Math constructor.
81
     *
82
     * @param string|static $number
83
     *
84
     * @throws \InvalidArgumentException
85
     */
86
    public function __construct($number)
87
    {
88
        if (isset(static::$globalPrecision)) {
89
            $this->precision = static::$globalPrecision;
90
        }
91
92
        $this->number = static::validateInputNumber($number);
93
    }
94
95
    /**
96
     * @return string
97
     */
98
    public function __toString()
99
    {
100
        return static::normalizeNumber($this->number);
101
    }
102
103
    /**
104
     * @return string
105
     */
106
    public function getNumber()
107
    {
108
        return $this->number;
109
    }
110
111
    /**
112
     * @param string $number
113
     *
114
     * @throws \InvalidArgumentException
115
     *
116
     * @return static
117
     */
118
    public static function number($number)
119
    {
120
        return new static($number);
121
    }
122
123
    /**
124
     * @param int $precision
125
     */
126
    public static function setGlobalPrecision($precision)
127
    {
128
        // even INT_32 should be enough precision
129
        static::$globalPrecision = max(0, (int) $precision);
130
        static::$staticPrecision = static::$globalPrecision;
131
    }
132
133
    /**
134
     * @param int $precision
135
     *
136
     * @return $this
137
     */
138
    public function setPrecision($precision)
139
    {
140
        // even INT_32 should be enough precision
141
        $this->precision = max(0, (int) $precision);
142
143
        return $this;
144
    }
145
146
    /**
147
     * @param bool $disable
148
     *
149
     * @return bool
150
     */
151
    public static function gmpSupport($disable = false)
152
    {
153
        if ($disable) {
154
            return static::$gmpSupport = false;
155
        }
156
157
        return static::$gmpSupport = function_exists('gmp_init');
158
    }
159
160
    /**
161
     * @param string[] $numbers
162
     *
163
     * @throws \InvalidArgumentException
164
     *
165
     * @return $this
166
     */
167
    public function add(...$numbers)
168
    {
169
        foreach ($numbers as $number) {
170
            $this->number = bcadd($this->number, static::validateInputNumber($number), $this->precision);
171
        }
172
173
        return $this;
174
    }
175
176
    /**
177
     * @param string[] $numbers
178
     *
179
     * @throws \InvalidArgumentException
180
     *
181
     * @return $this
182
     */
183
    public function sub(...$numbers)
184
    {
185
        foreach ($numbers as $number) {
186
            $this->number = bcsub($this->number, static::validateInputNumber($number), $this->precision);
187
        }
188
189
        return $this;
190
    }
191
192
    /**
193
     * @param string[] $numbers
194
     *
195
     * @throws \InvalidArgumentException
196
     *
197
     * @return $this
198
     */
199
    public function mul(...$numbers)
200
    {
201
        foreach ($numbers as $number) {
202
            $this->number = bcmul($this->number, static::validateInputNumber($number), $this->precision);
203
        }
204
205
        return $this;
206
    }
207
208
    /**
209
     * @param string[] $numbers
210
     *
211
     * @throws \InvalidArgumentException
212
     *
213
     * @return $this
214
     */
215
    public function div(...$numbers)
216
    {
217
        foreach ($numbers as $number) {
218
            $this->number = bcdiv($this->number, static::validateInputNumber($number), $this->precision);
219
        }
220
221
        return $this;
222
    }
223
224
    /**
225
     * @return $this
226
     */
227
    public function sqrt()
228
    {
229
        $this->number = bcsqrt($this->number, $this->precision);
230
231
        return $this;
232
    }
233
234
    /**
235
     * @param string $exponent
236
     *
237
     * @throws \InvalidArgumentException
238
     *
239
     * @return $this
240
     */
241
    public function pow($exponent)
242
    {
243
        $this->number = bcpow($this->number, static::validatePositiveInteger($exponent), $this->precision);
0 ignored issues
show
$exponent of type string is incompatible with the type integer expected by parameter $integer of fab2s\OpinHelpers\Math::validatePositiveInteger(). ( Ignorable by Annotation )

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

243
        $this->number = bcpow($this->number, static::validatePositiveInteger(/** @scrutinizer ignore-type */ $exponent), $this->precision);
Loading history...
244
245
        return $this;
246
    }
247
248
    /**
249
     * @param string $modulus
250
     *
251
     * @throws \InvalidArgumentException
252
     *
253
     * @return $this
254
     */
255
    public function mod($modulus)
256
    {
257
        $this->number = bcmod($this->number, static::validatePositiveInteger($modulus));
0 ignored issues
show
$modulus of type string is incompatible with the type integer expected by parameter $integer of fab2s\OpinHelpers\Math::validatePositiveInteger(). ( Ignorable by Annotation )

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

257
        $this->number = bcmod($this->number, static::validatePositiveInteger(/** @scrutinizer ignore-type */ $modulus));
Loading history...
258
259
        return $this;
260
    }
261
262
    /**
263
     * @param string $exponent
264
     * @param string $modulus
265
     *
266
     * @throws \InvalidArgumentException
267
     *
268
     * @return $this
269
     */
270
    public function powMod($exponent, $modulus)
271
    {
272
        $this->number = bcpowmod($this->number, static::validatePositiveInteger($exponent), static::validatePositiveInteger($modulus));
0 ignored issues
show
$exponent of type string is incompatible with the type integer expected by parameter $integer of fab2s\OpinHelpers\Math::validatePositiveInteger(). ( Ignorable by Annotation )

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

272
        $this->number = bcpowmod($this->number, static::validatePositiveInteger(/** @scrutinizer ignore-type */ $exponent), static::validatePositiveInteger($modulus));
Loading history...
273
274
        return $this;
275
    }
276
277
    /**
278
     * @param int $precision
279
     *
280
     * @return $this
281
     */
282
    public function round($precision = 0)
283
    {
284
        $precision = max(0, (int) $precision);
285
        if ($this->hasDecimals()) {
286
            if ($this->isPositive()) {
287
                $this->number = bcadd($this->number, '0.' . str_repeat('0', $precision) . '5', $precision);
288
289
                return $this;
290
            }
291
292
            $this->number = bcsub($this->number, '0.' . str_repeat('0', $precision) . '5', $precision);
293
        }
294
295
        return $this;
296
    }
297
298
    /**
299
     * @param int    $decimals
300
     * @param string $decPoint
301
     * @param string $thousandsSep
302
     *
303
     * @return string
304
     */
305
    public function format($decimals = 0, $decPoint = '.', $thousandsSep = ' ')
306
    {
307
        $decimals = max(0, (int) $decimals);
308
        $dec      = '';
309
        // do not mutate
310
        $number   = (new static($this))->round($decimals)->normalize();
311
        $sign     = $number->isPositive() ? '' : '-';
312
        if ($number->abs()->hasDecimals()) {
313
            list($number, $dec) = explode('.', (string) $number);
314
        }
315
316
        if ($decimals) {
317
            $dec = sprintf("%'0-" . $decimals . 's', $dec);
318
        }
319
320
        return $sign . preg_replace("/(?<=\d)(?=(\d{3})+(?!\d))/", $thousandsSep, $number) . ($decimals ? $decPoint . $dec : '');
321
    }
322
323
    /**
324
     * @return $this
325
     */
326
    public function ceil()
327
    {
328
        if ($this->hasDecimals()) {
329
            if ($this->isPositive()) {
330
                $this->number = bcadd($this->number, (preg_match('`\.[0]*$`', $this->number) ? '0' : '1'), 0);
331
332
                return $this;
333
            }
334
335
            $this->number = bcsub($this->number, '0', 0);
336
        }
337
338
        return $this;
339
    }
340
341
    /**
342
     * @return $this
343
     */
344
    public function floor()
345
    {
346
        if ($this->hasDecimals()) {
347
            if ($this->isPositive()) {
348
                $this->number = bcadd($this->number, 0, 0);
349
350
                return $this;
351
            }
352
353
            $this->number = bcsub($this->number, (preg_match('`\.[0]*$`', $this->number) ? '0' : '1'), 0);
354
        }
355
356
        return $this;
357
    }
358
359
    /**
360
     * @return $this
361
     */
362
    public function abs()
363
    {
364
        $this->number = ltrim($this->number, '-');
365
366
        return $this;
367
    }
368
369
    /**
370
     * @param string $number
371
     *
372
     * @throws \InvalidArgumentException
373
     *
374
     * @return bool
375
     */
376
    public function gte($number)
377
    {
378
        return (bool) (bccomp($this->number, static::validateInputNumber($number), $this->precision) >= 0);
379
    }
380
381
    /**
382
     * @param string $number
383
     *
384
     * @throws \InvalidArgumentException
385
     *
386
     * @return bool
387
     */
388
    public function gt($number)
389
    {
390
        return (bool) (bccomp($this->number, static::validateInputNumber($number), $this->precision) === 1);
391
    }
392
393
    /**
394
     * @param string $number
395
     *
396
     * @throws \InvalidArgumentException
397
     *
398
     * @return bool
399
     */
400
    public function lte($number)
401
    {
402
        return (bool) (bccomp($this->number, static::validateInputNumber($number), $this->precision) <= 0);
403
    }
404
405
    /**
406
     * @param string $number
407
     *
408
     * @throws \InvalidArgumentException
409
     *
410
     * @return bool
411
     */
412
    public function lt($number)
413
    {
414
        return (bool) (bccomp($this->number, static::validateInputNumber($number), $this->precision) === -1);
415
    }
416
417
    /**
418
     * @param string $number
419
     *
420
     * @throws \InvalidArgumentException
421
     *
422
     * @return bool
423
     */
424
    public function eq($number)
425
    {
426
        return (bool) (bccomp($this->number, static::validateInputNumber($number), $this->precision) === 0);
427
    }
428
429
    /**
430
     * returns the highest number among all arguments
431
     *
432
     * @param string[] $numbers
433
     *
434
     * @throws \InvalidArgumentException
435
     *
436
     * @return $this
437
     */
438
    public function max(...$numbers)
439
    {
440
        foreach ($numbers as $number) {
441
            if (bccomp(static::validateInputNumber($number), $this->number, $this->precision) === 1) {
442
                $this->number = $number;
443
            }
444
        }
445
446
        return $this;
447
    }
448
449
    /**
450
     * returns the smallest number among all arguments
451
     *
452
     * @param string[] $numbers
453
     *
454
     * @throws \InvalidArgumentException
455
     *
456
     * @return $this
457
     */
458
    public function min(...$numbers)
459
    {
460
        foreach ($numbers as $number) {
461
            if (bccomp(static::validateInputNumber($number), $this->number, $this->precision) === -1) {
462
                $this->number = $number;
463
            }
464
        }
465
466
        return $this;
467
    }
468
469
    /**
470
     * convert decimal value to any other base bellow or equals to 64
471
     *
472
     * @param int $base
473
     *
474
     * @throws \InvalidArgumentException
475
     *
476
     * @return string
477
     */
478
    public function toBase($base)
479
    {
480
        if ($this->normalize()->hasDecimals()) {
481
            throw new \InvalidArgumentException('Argument number is not an integer in ' . __METHOD__);
482
        }
483
484
        // do not mutate, only support positive integers
485
        $number = ltrim((string) $this, '-');
486
        if (static::$gmpSupport && $base <= 62) {
487
            return static::baseConvert($number, 10, $base);
488
        }
489
490
        $result   = '';
491
        $baseChar = static::getBaseChar($base);
492
        while (bccomp($number, 0) != 0) { // still data to process
493
            $rem    = bcmod($number, $base); // calc the remainder
494
            $number = bcdiv(bcsub($number, $rem), $base);
495
            $result = $baseChar[$rem] . $result;
496
        }
497
498
        $result = $result ? $result : $baseChar[0];
499
500
        return (string) $result;
501
    }
502
503
    /**
504
     * convert any based value bellow or equals to 64 to its decimal value
505
     *
506
     * @param string $number
507
     * @param int    $base
508
     *
509
     * @throws \InvalidArgumentException
510
     *
511
     * @return static
512
     */
513
    public static function fromBase($number, $base)
514
    {
515
        // cleanup
516
        $number   = trim($number);
517
        $baseChar = static::getBaseChar($base);
518
        // Convert string to lower case since base36 or less is case insensitive
519
        if ($base < 37) {
520
            $number = strtolower($number);
521
        }
522
523
        // clean up the input string if it uses particular input formats
524
        switch ($base) {
525
            case 16:
526
                // remove 0x from start of string
527
                if (substr($number, 0, 2) === '0x') {
528
                    $number = substr($number, 2);
529
                }
530
                break;
531
            case 8:
532
                // remove the 0 from the start if it exists - not really required
533
                if ($number[0] === 0) {
534
                    $number = substr($number, 1);
535
                }
536
                break;
537
            case 2:
538
                // remove an 0b from the start if it exists
539
                if (substr($number, 0, 2) === '0b') {
540
                    $number = substr($number, 2);
541
                }
542
                break;
543
            case 64:
544
                // remove padding chars: =
545
                $number = rtrim($number, '=');
546
                break;
547
        }
548
549
        // only support positive integers
550
        $number = ltrim($number, '-');
551
        if ($number === '' || strpos($number, '.') !== false) {
552
            throw new \InvalidArgumentException('Argument number is not an integer');
553
        }
554
555
        if (trim($number, $baseChar[0]) === '') {
556
            return new static('0');
557
        }
558
559
        if (static::$gmpSupport && $base <= 62) {
560
            return new static(static::baseConvert($number, $base, 10));
561
        }
562
563
        // By now we know we have a correct base and number
564
        $result    = '';
565
        $numberLen = strlen($number);
566
        // Now loop through each digit in the number
567
        for ($i = $numberLen - 1; $i >= 0; --$i) {
568
            $char = $number[$i]; // extract the last char from the number
569
            $ord  = strpos($baseChar, $char); // get the decimal value
570
            if ($ord === false || $ord > $base) {
571
                throw new \InvalidArgumentException('Argument number is invalid');
572
            }
573
574
            // Now convert the value+position to decimal
575
            $result = bcadd($result, bcmul($ord, bcpow($base, ($numberLen - $i - 1))));
576
        }
577
578
        return new static($result ? $result : '0');
579
    }
580
581
    /**
582
     * Convert a from a given base (up to 62) to base 10.
583
     *
584
     * WARNING This method requires ext-gmp
585
     *
586
     * @param string $number
587
     * @param int    $fromBase
588
     * @param int    $toBase
589
     *
590
     * @return string
591
     *
592
     * @internal param int $base
593
     */
594
    public static function baseConvert($number, $fromBase = 10, $toBase = 62)
595
    {
596
        return gmp_strval(gmp_init($number, $fromBase), $toBase);
597
    }
598
599
    /**
600
     * @return bool
601
     */
602
    public function isPositive()
603
    {
604
        return $this->number[0] !== '-';
605
    }
606
607
    /**
608
     * @return bool
609
     */
610
    public function hasDecimals()
611
    {
612
        return strpos($this->number, '.') !== false;
613
    }
614
615
    /**
616
     * @return $this
617
     */
618
    public function normalize()
619
    {
620
        $this->number = static::normalizeNumber($this->number);
621
622
        return $this;
623
    }
624
625
    /**
626
     * There is no way around it, if you want to trust bcmath
627
     * you need to feed it with VALID numbers
628
     * Things like '1.1.1' or '12E16'are all 0 in bcmath world
629
     *
630
     * @param mixed $number
631
     *
632
     * @return bool
633
     */
634
    public static function isNumber($number)
635
    {
636
        return (bool) preg_match('`^([+-]{1})?([0-9]+(\.[0-9]+)?|\.[0-9]+)$`', $number);
637
    }
638
639
    /**
640
     * removes preceding / trailing 0, + and ws
641
     *
642
     * @param string      $number
643
     * @param string|null $default
644
     *
645
     * @return string|null
646
     */
647
    public static function normalizeNumber($number, $default = null)
648
    {
649
        if (!static::isNumber($number)) {
650
            return $default;
651
        }
652
653
        $sign   = $number[0] === '-' ? '-' : '';
654
        $number = ltrim((string) $number, '0+-');
655
656
        if (strpos($number, '.') !== false) {
657
            // also clear trailing 0
658
            list($number, $dec) = explode('.', $number);
659
            $dec                = rtrim($dec, '0.');
660
            $number             = ($number ? $number : '0') . ($dec ? '.' . $dec : '');
661
        }
662
663
        return $number ? $sign . $number : '0';
664
    }
665
666
    /**
667
     * @param int $base
668
     * @param int $max
669
     *
670
     * @throws \InvalidArgumentException
671
     *
672
     * @return string
673
     */
674
    public static function getBaseChar($base, $max = 64)
675
    {
676
        $base = (int) $base;
677
        if ($base < 2 || $base > $max || $base > 64) {
678
            throw new \InvalidArgumentException('Argument base is not valid, base 2 to 64 are supported');
679
        }
680
681
        if (!isset(static::$baseChars[$base])) {
682
            if ($base > 62) {
683
                static::$baseChars[$base] = ($base == 64) ? static::BASECHAR_64 : substr(static::BASECHAR_64, 0, $base);
684
            } elseif ($base > 36) {
685
                static::$baseChars[$base] = ($base == 62) ? static::BASECHAR_62 : substr(static::BASECHAR_62, 0, $base);
686
            } else {
687
                static::$baseChars[$base] = ($base == 36) ? static::BASECHAR_36 : substr(static::BASECHAR_36, 0, $base);
688
            }
689
        }
690
691
        return static::$baseChars[$base];
692
    }
693
694
    /**
695
     * @param string|static $number
696
     *
697
     * @throws \InvalidArgumentException
698
     *
699
     * @return string
700
     */
701
    protected static function validateInputNumber($number)
702
    {
703
        if ($number instanceof static) {
704
            return $number->getNumber();
705
        }
706
707
        $number = trim($number);
708
        if (!static::isNumber($number)) {
709
            throw new \InvalidArgumentException('Argument number is not valid');
710
        }
711
712
        return $number;
713
    }
714
715
    /**
716
     * @param int $integer
717
     *
718
     * @throws \InvalidArgumentException
719
     *
720
     * @return string
721
     */
722
    protected static function validatePositiveInteger($integer)
723
    {
724
        $integer = max(0, (int) $integer);
725
        if (!$integer) {
726
            throw new \InvalidArgumentException('Argument number is not valid');
727
        }
728
729
        return (string) $integer;
730
    }
731
}
732
733
// OMG a dynamic static anti pattern ^^
734
Math::gmpSupport();
735