Base::toBase()   B
last analyzed

Complexity

Conditions 7
Paths 6

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 7

Importance

Changes 0
Metric Value
cc 7
eloc 15
nc 6
nop 2
dl 0
loc 25
ccs 16
cts 16
cp 1
crap 7
rs 8.8333
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of ocubom/base-convert
5
 *
6
 * © Oscar Cubo Medina <https://ocubom.github.io>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Ocubom\Math;
13
14
use Ocubom\Math\Base\Binary;
15
use Ocubom\Math\Base\Numeric;
16
use Ocubom\Math\Exception\InvalidArgumentException;
17
18
/**
19
 * Base convert.
20
 *
21
 * @author Oscar Cubo Medina <[email protected]>
22
 */
23
abstract class Base
24
{
25
    /** @var class-string[] */
26
    private static $bases = [
27
        'bin' => Binary::class,
28
        'binary' => Binary::class,
29
    ];
30
31
    /**
32
     * Convert a number between arbitrary bases.
33
     *
34
     * Supports large arbitrary numbers by using a safe php implementation
35
     *
36
     * @see http://php.net/manual/en/function.base-convert.php
37
     * @see http://php.net/manual/en/function.base-convert.php#109660
38
     *
39
     * @param int|string               $number The number to convert
40
     * @param BaseInterface|int|string $source Original base for number
41
     * @param BaseInterface|int|string $target Desired base for number
42
     */
43 6645
    final public static function convert($number, $source, $target): string
44
    {
45
        // Filter bases
46 6645
        $source = self::filterBase($source);
47 6642
        $target = self::filterBase($target);
48 6639
        if ($source == $target) {
49 143
            return (is_numeric($number) ? strval($number) : $number) ?: '0';
50
        }
51
52
        // Filter number with source base
53 6497
        $number = $source->filterValue($number);
54
55
        // Do base conversions
56 6494
        $number = self::fromBase($number, $source->getMap());
57 6494
        $number = self::toBase($number, $target->getMap());
58
59
        // Prepare the final value
60 6494
        return $target->returnValue($number);
61
    }
62
63
    /**
64
     * Filter & validate base.
65
     *
66
     * @param BaseInterface|string|int $base
67
     */
68 6645
    final public static function filterBase($base): BaseInterface
69
    {
70 6645
        if ($base instanceof BaseInterface) {
71 122
            return $base;
72
        }
73
74 6644
        if ($class = self::$bases[strtolower((string) $base)] ?? null) {
75
            try {
76 276
                $object = (new \ReflectionClass($class))->newInstance();
77
78 276
                if ($object instanceof BaseInterface) {
79 276
                    return $object;
80
                }
81
            } catch (\ReflectionException $exc) { // @codeCoverageIgnore
82
                throw new InvalidArgumentException("Invalid base ({$base})", 0, $exc); // @codeCoverageIgnore
83
            }
84
        }
85
86 6638
        return new Numeric((string) $base);
87
    }
88
89
    /**
90
     * The following method was extracted from symfony/uid (v7.0.1).
91
     *
92
     * Code subject to the MIT license (https://github.com/symfony/symfony/blob/v7.0.1/src/Symfony/Component/Uid/LICENSE)
93
     *
94
     * Copyright (c) 2020-2022 Fabien Potencier
95
     *
96
     * @see https://github.com/symfony/symfony/blob/v7.0.1/src/Symfony/Component/Uid/BinaryUtil.php#L74
97
     */
98 6494
    public static function fromBase(string $digits, array $map): string
99
    {
100 6494
        $base = \strlen($map['']);
101 6494
        $count = \strlen($digits);
102 6494
        $bytes = [];
103
104 6494
        while ($count) {
105 6494
            $quotient = [];
106 6494
            $remainder = 0;
107
108 6494
            for ($i = 0; $i !== $count; ++$i) {
109 6494
                $carry = ($bytes ? $digits[$i] : $map[$digits[$i]]) + $remainder * $base;
110
111 6494
                if (\PHP_INT_SIZE >= 8) {
112 6494
                    $digit = $carry >> 16;
113 6494
                    $remainder = $carry & 0xFFFF;
114
                } else {
115
                    $digit = $carry >> 8;
116
                    $remainder = $carry & 0xFF;
117
                }
118
119 6494
                if ($digit || $quotient) {
120 5214
                    $quotient[] = $digit;
121
                }
122
            }
123
124 6494
            $bytes[] = $remainder;
125 6494
            $count = \count($digits = $quotient);
126
        }
127
128 6494
        return pack(\PHP_INT_SIZE >= 8 ? 'n*' : 'C*', ...array_reverse($bytes));
129
    }
130
131
    /**
132
     * The following method was extracted from symfony/uid (v7.0.1).
133
     *
134
     * Code subject to the MIT license (https://github.com/symfony/symfony/blob/v7.0.1/src/Symfony/Component/Uid/LICENSE)
135
     *
136
     * Copyright (c) 2020-2022 Fabien Potencier
137
     *
138
     * @see https://github.com/symfony/symfony/blob/v7.0.1/src/Symfony/Component/Uid/BinaryUtil.php#L47
139
     */
140 6494
    public static function toBase(string $bytes, array $map): string
141
    {
142 6494
        $base = \strlen($alphabet = $map['']);
143 6494
        $bytes = array_values(unpack(\PHP_INT_SIZE >= 8 ? 'n*' : 'C*', $bytes));
144 6494
        $digits = '';
145
146 6494
        while ($count = \count($bytes)) {
147 6494
            $quotient = [];
148 6494
            $remainder = 0;
149
150 6494
            for ($i = 0; $i !== $count; ++$i) {
151 6494
                $carry = $bytes[$i] + ($remainder << (\PHP_INT_SIZE >= 8 ? 16 : 8));
152 6494
                $digit = intdiv($carry, $base);
153 6494
                $remainder = $carry % $base;
154
155 6494
                if ($digit || $quotient) {
156 6480
                    $quotient[] = $digit;
157
                }
158
            }
159
160 6494
            $digits = $alphabet[$remainder].$digits;
161 6494
            $bytes = $quotient;
162
        }
163
164 6494
        return $digits;
165
    }
166
}
167