1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Riimu\Kit\BaseConversion; |
4
|
|
|
|
5
|
|
|
/** |
6
|
|
|
* Converts numbers by using a mathematical algorithm that relies on integers. |
7
|
|
|
* |
8
|
|
|
* DecimalConverter employs arbitrary-precision integer arithmetic to first |
9
|
|
|
* convert the digits to decimal system and then to convert the digits to the |
10
|
|
|
* target base. DecimalConverter depends on the GMP extension to perform the |
11
|
|
|
* required arbitrary precision integer calculations. |
12
|
|
|
* |
13
|
|
|
* @author Riikka Kalliomäki <[email protected]> |
14
|
|
|
* @copyright Copyright (c) 2014-2017 Riikka Kalliomäki |
15
|
|
|
* @license http://opensource.org/licenses/mit-license.php MIT License |
16
|
|
|
*/ |
17
|
|
|
class DecimalConverter implements Converter |
18
|
|
|
{ |
19
|
|
|
/** @var int Precision for fraction conversions */ |
20
|
|
|
private $precision; |
21
|
|
|
|
22
|
|
|
/** @var NumberBase Number base used by provided numbers */ |
23
|
|
|
private $source; |
24
|
|
|
|
25
|
|
|
/** @var NumberBase Number base used by returned numbers */ |
26
|
|
|
private $target; |
27
|
|
|
|
28
|
|
|
/** @var string Number base used by GMP for standard conversions */ |
29
|
|
|
private static $standardBase = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Creates a new instance of DecimalConverter. |
33
|
|
|
* @param NumberBase $source Number base used by the provided numbers |
34
|
|
|
* @param NumberBase $target Number base used by the returned numbers |
35
|
|
|
*/ |
36
|
114 |
|
public function __construct(NumberBase $source, NumberBase $target) |
37
|
|
|
{ |
38
|
114 |
|
$this->precision = -1; |
39
|
114 |
|
$this->source = $source; |
40
|
114 |
|
$this->target = $target; |
41
|
114 |
|
} |
42
|
|
|
|
43
|
33 |
|
public function setPrecision($precision) |
44
|
|
|
{ |
45
|
33 |
|
$this->precision = (int) $precision; |
46
|
33 |
|
} |
47
|
|
|
|
48
|
60 |
|
public function convertInteger(array $number) |
49
|
|
|
{ |
50
|
60 |
|
$decimal = $this->getDecimal($number); |
51
|
|
|
|
52
|
60 |
|
if ($this->isStandardBase($this->target->getDigitList())) { |
53
|
48 |
|
return $this->target->canonizeDigits(str_split(gmp_strval($decimal, $this->target->getRadix()))); |
54
|
|
|
} |
55
|
|
|
|
56
|
12 |
|
$zero = gmp_init('0'); |
57
|
12 |
|
$radix = gmp_init($this->target->getRadix()); |
58
|
12 |
|
$result = []; |
59
|
|
|
|
60
|
12 |
|
while (gmp_cmp($decimal, $zero) > 0) { |
61
|
12 |
|
list($decimal, $modulo) = gmp_div_qr($decimal, $radix); |
62
|
12 |
|
$result[] = gmp_intval($modulo); |
63
|
4 |
|
} |
64
|
|
|
|
65
|
12 |
|
return $this->target->getDigits(empty($result) ? [0] : array_reverse($result)); |
66
|
|
|
} |
67
|
|
|
|
68
|
60 |
|
public function convertFractions(array $number) |
69
|
|
|
{ |
70
|
60 |
|
$target = gmp_init($this->target->getRadix()); |
71
|
60 |
|
$dividend = $this->getDecimal($number); |
72
|
60 |
|
$divisor = $this->getDecimal( |
73
|
60 |
|
[$this->source->getDigit(1)] + array_fill(1, max(count($number), 1), $this->source->getDigit(0)) |
74
|
20 |
|
); |
75
|
60 |
|
$digits = $this->getFractionDigitCount(count($number)); |
76
|
60 |
|
$zero = gmp_init('0'); |
77
|
60 |
|
$result = []; |
78
|
|
|
|
79
|
60 |
|
for ($i = 0; $i < $digits && gmp_cmp($dividend, $zero) > 0; $i++) { |
80
|
57 |
|
list($digit, $dividend) = gmp_div_qr(gmp_mul($dividend, $target), $divisor); |
81
|
57 |
|
$result[] = gmp_intval($digit); |
82
|
19 |
|
} |
83
|
|
|
|
84
|
60 |
|
return $this->target->getDigits(empty($result) ? [0] : $result); |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* Converts the number from source base to a decimal GMP resource. |
89
|
|
|
* @param array $number Digits for the number to convert |
90
|
|
|
* @return resource resulting number as a GMP resource |
|
|
|
|
91
|
|
|
*/ |
92
|
105 |
|
private function getDecimal(array $number) |
93
|
|
|
{ |
94
|
105 |
|
if ($this->isStandardBase($this->source->getDigitList())) { |
95
|
96 |
|
return gmp_init(implode('', $this->source->canonizeDigits($number)), $this->source->getRadix()); |
96
|
|
|
} |
97
|
|
|
|
98
|
12 |
|
$number = $this->source->getValues($number); |
99
|
12 |
|
$decimal = gmp_init('0'); |
100
|
12 |
|
$count = count($number); |
101
|
12 |
|
$radix = gmp_init($this->source->getRadix()); |
102
|
|
|
|
103
|
12 |
|
for ($i = 0; $i < $count; $i++) { |
104
|
12 |
|
$decimal = gmp_add($decimal, gmp_mul(gmp_init($number[$i]), gmp_pow($radix, $count - $i - 1))); |
105
|
4 |
|
} |
106
|
|
|
|
107
|
12 |
|
return $decimal; |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
/** |
111
|
|
|
* Tells if the list of digits match those used by GMP. |
112
|
|
|
* @param array $digits List of digits for the number base |
113
|
|
|
* @return bool True if the digits match, false if they do not |
114
|
|
|
*/ |
115
|
105 |
|
private function isStandardBase(array $digits) |
116
|
|
|
{ |
117
|
105 |
|
if (count($digits) > strlen(self::$standardBase)) { |
118
|
3 |
|
return false; |
119
|
|
|
} |
120
|
|
|
|
121
|
102 |
|
return $digits === str_split(substr(self::$standardBase, 0, count($digits))); |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* Determines the number of digits required in the target base. |
126
|
|
|
* @param int $count Number of digits in the original number |
127
|
|
|
* @return int Number of digits required in the target base |
128
|
|
|
*/ |
129
|
60 |
|
private function getFractionDigitCount($count) |
130
|
|
|
{ |
131
|
60 |
|
if ($this->precision > 0) { |
132
|
24 |
|
return $this->precision; |
133
|
|
|
} |
134
|
|
|
|
135
|
39 |
|
$target = gmp_init($this->target->getRadix()); |
136
|
39 |
|
$maxFraction = gmp_pow(gmp_init($this->source->getRadix()), $count); |
137
|
39 |
|
$digits = 1; |
138
|
|
|
|
139
|
39 |
|
while (gmp_cmp(gmp_pow($target, $digits), $maxFraction) < 0) { |
140
|
30 |
|
$digits++; |
141
|
10 |
|
} |
142
|
|
|
|
143
|
39 |
|
return $digits - $this->precision; |
144
|
|
|
} |
145
|
|
|
} |
146
|
|
|
|
This check compares the return type specified in the
@return
annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.