|
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
@returnannotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.