Completed
Push — master ( 857e1b...12aa23 )
by Fabrice
02:47
created

MathBaseAbstract::cleanBaseInteger()   D

Complexity

Conditions 9
Paths 16

Size

Total Lines 33
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 19
nc 16
nop 2
dl 0
loc 33
rs 4.909
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 MathBaseAbstract
14
 */
15
abstract class MathBaseAbstract
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
     * @param string $number
105
     *
106
     * @throws \InvalidArgumentException
107
     *
108
     * @return static
109
     */
110
    public static function number($number)
111
    {
112
        return new static($number);
113
    }
114
115
    /**
116
     * convert any based value bellow or equals to 64 to its decimal value
117
     *
118
     * @param string $number
119
     * @param int    $base
120
     *
121
     * @throws \InvalidArgumentException
122
     *
123
     * @return static
124
     */
125
    public static function fromBase($number, $base)
126
    {
127
        $baseChar = static::getBaseChar($base);
128
        $number   = static::cleanBaseInteger(trim($number), $base);
129
130
        // only support positive integers
131
        $number = ltrim($number, '-');
132
        if ($number === '' || strpos($number, '.') !== false) {
133
            throw new \InvalidArgumentException('Argument number is not an integer');
134
        }
135
136
        if (trim($number, $baseChar[0]) === '') {
137
            return new static('0');
138
        }
139
140
        if (static::$gmpSupport && $base <= 62) {
141
            return new static(static::baseConvert($number, $base, 10));
142
        }
143
144
        // By now we know we have a correct base and number
145
        $result    = '';
146
        $numberLen = strlen($number);
147
        // Now loop through each digit in the number
148
        for ($i = $numberLen - 1; $i >= 0; --$i) {
149
            $char = $number[$i]; // extract the last char from the number
150
            $ord  = strpos($baseChar, $char); // get the decimal value
151
            if ($ord === false || $ord > $base) {
152
                throw new \InvalidArgumentException('Argument number is invalid');
153
            }
154
155
            // Now convert the value+position to decimal
156
            $result = bcadd($result, bcmul($ord, bcpow($base, ($numberLen - $i - 1))));
157
        }
158
159
        return new static($result ? $result : '0');
160
    }
161
162
    /**
163
     * @return string
164
     */
165
    public function getNumber()
166
    {
167
        return $this->number;
168
    }
169
170
    /**
171
     * @return bool
172
     */
173
    public function isPositive()
174
    {
175
        return $this->number[0] !== '-';
176
    }
177
178
    /**
179
     * @return bool
180
     */
181
    public function hasDecimals()
182
    {
183
        return strpos($this->number, '.') !== false;
184
    }
185
186
    /**
187
     * @return $this
188
     */
189
    public function normalize()
190
    {
191
        $this->number = static::normalizeNumber($this->number);
192
193
        return $this;
194
    }
195
196
    /**
197
     * @param int $precision
198
     *
199
     * @return $this
200
     */
201
    public function setPrecision($precision)
202
    {
203
        // even INT_32 should be enough precision
204
        $this->precision = max(0, (int) $precision);
205
206
        return $this;
207
    }
208
209
    /**
210
     * @param int $precision
211
     */
212
    public static function setGlobalPrecision($precision)
213
    {
214
        // even INT_32 should be enough precision
215
        static::$globalPrecision = max(0, (int) $precision);
216
        static::$staticPrecision = static::$globalPrecision;
217
    }
218
219
    /**
220
     * @param bool $disable
221
     *
222
     * @return bool
223
     */
224
    public static function gmpSupport($disable = false)
225
    {
226
        if ($disable) {
227
            return static::$gmpSupport = false;
228
        }
229
230
        return static::$gmpSupport = function_exists('gmp_init');
231
    }
232
233
    /**
234
     * There is no way around it, if you want to trust bcmath
235
     * you need to feed it with VALID numbers
236
     * Things like '1.1.1' or '12E16'are all 0 in bcmath world
237
     *
238
     * @param mixed $number
239
     *
240
     * @return bool
241
     */
242
    public static function isNumber($number)
243
    {
244
        return (bool) preg_match('`^([+-]{1})?([0-9]+(\.[0-9]+)?|\.[0-9]+)$`', $number);
245
    }
246
247
    /**
248
     * removes preceding / trailing 0, + and ws
249
     *
250
     * @param string      $number
251
     * @param string|null $default
252
     *
253
     * @return string|null
254
     */
255
    public static function normalizeNumber($number, $default = null)
256
    {
257
        if (!static::isNumber($number)) {
258
            return $default;
259
        }
260
261
        $sign   = $number[0] === '-' ? '-' : '';
262
        $number = ltrim((string) $number, '0+-');
263
264
        if (strpos($number, '.') !== false) {
265
            // also clear trailing 0
266
            list($number, $dec) = explode('.', $number);
267
            $dec                = rtrim($dec, '0.');
268
            $number             = ($number ? $number : '0') . ($dec ? '.' . $dec : '');
269
        }
270
271
        return $number ? $sign . $number : '0';
272
    }
273
274
    /**
275
     * @param int $base
276
     * @param int $max
277
     *
278
     * @throws \InvalidArgumentException
279
     *
280
     * @return string
281
     */
282
    public static function getBaseChar($base, $max = 64)
283
    {
284
        $base = (int) $base;
285
        if ($base < 2 || $base > $max || $base > 64) {
286
            throw new \InvalidArgumentException('Argument base is not valid, base 2 to 64 are supported');
287
        }
288
289
        if (!isset(static::$baseChars[$base])) {
290
            if ($base > 62) {
291
                static::$baseChars[$base] = ($base == 64) ? static::BASECHAR_64 : substr(static::BASECHAR_64, 0, $base);
292
            } elseif ($base > 36) {
293
                static::$baseChars[$base] = ($base == 62) ? static::BASECHAR_62 : substr(static::BASECHAR_62, 0, $base);
294
            } else {
295
                static::$baseChars[$base] = ($base == 36) ? static::BASECHAR_36 : substr(static::BASECHAR_36, 0, $base);
296
            }
297
        }
298
299
        return static::$baseChars[$base];
300
    }
301
302
    /**
303
     * @param string     $integer
304
     * @param string|int $base
305
     *
306
     * @return string
307
     */
308
    public static function cleanBaseInteger($integer, $base)
309
    {
310
        if ($base < 37) {
311
            $integer = strtolower($integer);
312
        }
313
314
        // clean up the input string if it uses particular input formats
315
        switch ($base) {
316
            case 16:
317
                // remove 0x from start of string
318
                if (substr($integer, 0, 2) === '0x') {
319
                    $integer = substr($integer, 2);
320
                }
321
                break;
322
            case 8:
323
                // remove the 0 from the start if it exists - not really required
324
                if ($integer[0] === 0) {
325
                    $integer = substr($integer, 1);
326
                }
327
                break;
328
            case 2:
329
                // remove an 0b from the start if it exists
330
                if (substr($integer, 0, 2) === '0b') {
331
                    $integer = substr($integer, 2);
332
                }
333
                break;
334
            case 64:
335
                // remove padding chars: =
336
                $integer = rtrim($integer, '=');
337
                break;
338
        }
339
340
        return $integer;
341
    }
342
343
    /**
344
     * Convert a from a given base (up to 62) to base 10.
345
     *
346
     * WARNING This method requires ext-gmp
347
     *
348
     * @param string $number
349
     * @param int    $fromBase
350
     * @param int    $toBase
351
     *
352
     * @return string
353
     *
354
     * @internal param int $base
355
     */
356
    public static function baseConvert($number, $fromBase = 10, $toBase = 62)
357
    {
358
        return gmp_strval(gmp_init($number, $fromBase), $toBase);
359
    }
360
361
    /**
362
     * @param string|static $number
363
     *
364
     * @throws \InvalidArgumentException
365
     *
366
     * @return string
367
     */
368
    protected static function validateInputNumber($number)
369
    {
370
        if ($number instanceof static) {
371
            return $number->getNumber();
372
        }
373
374
        $number = trim($number);
375
        if (!static::isNumber($number)) {
376
            throw new \InvalidArgumentException('Argument number is not valid');
377
        }
378
379
        return $number;
380
    }
381
382
    /**
383
     * @param int|string $integer
384
     *
385
     * @throws \InvalidArgumentException
386
     *
387
     * @return string
388
     */
389
    protected static function validatePositiveInteger($integer)
390
    {
391
        $integer = max(0, (int) $integer);
392
        if (!$integer) {
393
            throw new \InvalidArgumentException('Argument number is not valid');
394
        }
395
396
        return (string) $integer;
397
    }
398
}
399