Completed
Push — master ( 426134...338ce0 )
by Fabrice
06:11
created

MathAbstract::setGlobalPrecision()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
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\Math\OpinHelpers;
11
12
/**
13
 * Abstract class MathAbstract
14
 */
15
abstract class MathAbstract
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
     * convert any based value bellow or equals to 64 to its decimal value
162
     *
163
     * @param string $number
164
     * @param int    $base
165
     *
166
     * @throws \InvalidArgumentException
167
     *
168
     * @return static
169
     */
170
    public static function fromBase($number, $base)
171
    {
172
        // cleanup
173
        $number   = trim($number);
174
        $baseChar = static::getBaseChar($base);
175
        // Convert string to lower case since base36 or less is case insensitive
176
        if ($base < 37) {
177
            $number = strtolower($number);
178
        }
179
180
        // clean up the input string if it uses particular input formats
181
        switch ($base) {
182
            case 16:
183
                // remove 0x from start of string
184
                if (substr($number, 0, 2) === '0x') {
185
                    $number = substr($number, 2);
186
                }
187
                break;
188
            case 8:
189
                // remove the 0 from the start if it exists - not really required
190
                if ($number[0] === 0) {
191
                    $number = substr($number, 1);
192
                }
193
                break;
194
            case 2:
195
                // remove an 0b from the start if it exists
196
                if (substr($number, 0, 2) === '0b') {
197
                    $number = substr($number, 2);
198
                }
199
                break;
200
            case 64:
201
                // remove padding chars: =
202
                $number = rtrim($number, '=');
203
                break;
204
        }
205
206
        // only support positive integers
207
        $number = ltrim($number, '-');
208
        if ($number === '' || strpos($number, '.') !== false) {
209
            throw new \InvalidArgumentException('Argument number is not an integer');
210
        }
211
212
        if (trim($number, $baseChar[0]) === '') {
213
            return new static('0');
214
        }
215
216
        if (static::$gmpSupport && $base <= 62) {
217
            return new static(static::baseConvert($number, $base, 10));
0 ignored issues
show
Bug introduced by
The method baseConvert() does not exist on fab2s\Math\OpinHelpers\MathAbstract. Since it exists in all sub-types, consider adding an abstract or default implementation to fab2s\Math\OpinHelpers\MathAbstract. ( Ignorable by Annotation )

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

217
            return new static(static::/** @scrutinizer ignore-call */ baseConvert($number, $base, 10));
Loading history...
218
        }
219
220
        // By now we know we have a correct base and number
221
        $result    = '';
222
        $numberLen = strlen($number);
223
        // Now loop through each digit in the number
224
        for ($i = $numberLen - 1; $i >= 0; --$i) {
225
            $char = $number[$i]; // extract the last char from the number
226
            $ord  = strpos($baseChar, $char); // get the decimal value
227
            if ($ord === false || $ord > $base) {
228
                throw new \InvalidArgumentException('Argument number is invalid');
229
            }
230
231
            // Now convert the value+position to decimal
232
            $result = bcadd($result, bcmul($ord, bcpow($base, ($numberLen - $i - 1))));
233
        }
234
235
        return new static($result ? $result : '0');
236
    }
237
238
    /**
239
     * @return bool
240
     */
241
    public function isPositive()
242
    {
243
        return $this->number[0] !== '-';
244
    }
245
246
    /**
247
     * @return bool
248
     */
249
    public function hasDecimals()
250
    {
251
        return strpos($this->number, '.') !== false;
252
    }
253
254
    /**
255
     * @return $this
256
     */
257
    public function normalize()
258
    {
259
        $this->number = static::normalizeNumber($this->number);
260
261
        return $this;
262
    }
263
264
    /**
265
     * There is no way around it, if you want to trust bcmath
266
     * you need to feed it with VALID numbers
267
     * Things like '1.1.1' or '12E16'are all 0 in bcmath world
268
     *
269
     * @param mixed $number
270
     *
271
     * @return bool
272
     */
273
    public static function isNumber($number)
274
    {
275
        return (bool) preg_match('`^([+-]{1})?([0-9]+(\.[0-9]+)?|\.[0-9]+)$`', $number);
276
    }
277
278
    /**
279
     * removes preceding / trailing 0, + and ws
280
     *
281
     * @param string      $number
282
     * @param string|null $default
283
     *
284
     * @return string|null
285
     */
286
    public static function normalizeNumber($number, $default = null)
287
    {
288
        if (!static::isNumber($number)) {
289
            return $default;
290
        }
291
292
        $sign   = $number[0] === '-' ? '-' : '';
293
        $number = ltrim((string) $number, '0+-');
294
295
        if (strpos($number, '.') !== false) {
296
            // also clear trailing 0
297
            list($number, $dec) = explode('.', $number);
298
            $dec                = rtrim($dec, '0.');
299
            $number             = ($number ? $number : '0') . ($dec ? '.' . $dec : '');
300
        }
301
302
        return $number ? $sign . $number : '0';
303
    }
304
305
    /**
306
     * @param int $base
307
     * @param int $max
308
     *
309
     * @throws \InvalidArgumentException
310
     *
311
     * @return string
312
     */
313
    public static function getBaseChar($base, $max = 64)
314
    {
315
        $base = (int) $base;
316
        if ($base < 2 || $base > $max || $base > 64) {
317
            throw new \InvalidArgumentException('Argument base is not valid, base 2 to 64 are supported');
318
        }
319
320
        if (!isset(static::$baseChars[$base])) {
321
            if ($base > 62) {
322
                static::$baseChars[$base] = ($base == 64) ? static::BASECHAR_64 : substr(static::BASECHAR_64, 0, $base);
323
            } elseif ($base > 36) {
324
                static::$baseChars[$base] = ($base == 62) ? static::BASECHAR_62 : substr(static::BASECHAR_62, 0, $base);
325
            } else {
326
                static::$baseChars[$base] = ($base == 36) ? static::BASECHAR_36 : substr(static::BASECHAR_36, 0, $base);
327
            }
328
        }
329
330
        return static::$baseChars[$base];
331
    }
332
333
    /**
334
     * @param string|static $number
335
     *
336
     * @throws \InvalidArgumentException
337
     *
338
     * @return string
339
     */
340
    protected static function validateInputNumber($number)
341
    {
342
        if ($number instanceof static) {
343
            return $number->getNumber();
344
        }
345
346
        $number = trim($number);
347
        if (!static::isNumber($number)) {
348
            throw new \InvalidArgumentException('Argument number is not valid');
349
        }
350
351
        return $number;
352
    }
353
354
    /**
355
     * @param int|string $integer
356
     *
357
     * @throws \InvalidArgumentException
358
     *
359
     * @return string
360
     */
361
    protected static function validatePositiveInteger($integer)
362
    {
363
        $integer = max(0, (int) $integer);
364
        if (!$integer) {
365
            throw new \InvalidArgumentException('Argument number is not valid');
366
        }
367
368
        return (string) $integer;
369
    }
370
}
371