Passed
Push — master ( 12aa23...c90132 )
by Fabrice
02:44
created

MathBaseAbstract::fromBase()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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