1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace TreeHouse\Feeder\Modifier\Data\Transformer; |
4
|
|
|
|
5
|
|
|
use TreeHouse\Feeder\Exception\TransformationFailedException; |
6
|
|
|
|
7
|
|
|
/** |
8
|
|
|
* Transforms between a number type and a localized number with grouping (each thousand) and comma separators. |
9
|
|
|
* |
10
|
|
|
* Copied from Symfony's Form component |
11
|
|
|
*/ |
12
|
|
|
class LocalizedStringToNumberTransformer implements TransformerInterface |
13
|
|
|
{ |
14
|
|
|
/** |
15
|
|
|
* The locale to use. |
16
|
|
|
* |
17
|
|
|
* @var string |
18
|
|
|
*/ |
19
|
|
|
protected $locale; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* Number of fraction digits. |
23
|
|
|
* |
24
|
|
|
* @var int |
25
|
|
|
*/ |
26
|
|
|
protected $precision; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* Whether to use a grouping separator. |
30
|
|
|
* |
31
|
|
|
* @var bool |
32
|
|
|
*/ |
33
|
|
|
protected $grouping; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* The rounding mode to use. |
37
|
|
|
* |
38
|
|
|
* @var int |
39
|
|
|
*/ |
40
|
|
|
protected $roundingMode; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* @param string $locale |
44
|
|
|
* @param int $precision |
45
|
|
|
* @param bool $grouping |
46
|
|
|
* @param int $roundingMode |
47
|
|
|
*/ |
48
|
224 |
|
public function __construct($locale = null, $precision = null, $grouping = null, $roundingMode = null) |
49
|
|
|
{ |
50
|
224 |
|
if (null === $locale) { |
51
|
188 |
|
$locale = \Locale::getDefault(); |
52
|
188 |
|
} |
53
|
|
|
|
54
|
224 |
|
if (null === $grouping) { |
55
|
182 |
|
$grouping = false; |
56
|
182 |
|
} |
57
|
|
|
|
58
|
224 |
|
if (null === $roundingMode) { |
59
|
70 |
|
$roundingMode = \NumberFormatter::ROUND_HALFUP; |
60
|
70 |
|
} |
61
|
|
|
|
62
|
224 |
|
$this->locale = $locale; |
63
|
224 |
|
$this->precision = $precision; |
64
|
224 |
|
$this->grouping = $grouping; |
65
|
224 |
|
$this->roundingMode = $roundingMode; |
66
|
224 |
|
} |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* @inheritdoc |
70
|
|
|
*/ |
71
|
222 |
|
public function transform($value) |
72
|
|
|
{ |
73
|
222 |
|
if (!is_string($value) && !is_numeric($value)) { |
74
|
4 |
|
throw new TransformationFailedException( |
75
|
4 |
|
sprintf('Expected a string to transform, got "%s" instead.', json_encode($value)) |
76
|
4 |
|
); |
77
|
|
|
} |
78
|
|
|
|
79
|
218 |
|
if ('' === $value) { |
80
|
4 |
|
return null; |
81
|
|
|
} |
82
|
|
|
|
83
|
214 |
|
if ('NaN' === $value) { |
84
|
2 |
|
throw new TransformationFailedException('"NaN" is not a valid number'); |
85
|
|
|
} |
86
|
|
|
|
87
|
212 |
|
$position = 0; |
88
|
212 |
|
$formatter = $this->getNumberFormatter(); |
89
|
212 |
|
$groupSep = $formatter->getSymbol(\NumberFormatter::GROUPING_SEPARATOR_SYMBOL); |
90
|
212 |
|
$decSep = $formatter->getSymbol(\NumberFormatter::DECIMAL_SEPARATOR_SYMBOL); |
91
|
|
|
|
92
|
212 |
View Code Duplication |
if ('.' !== $decSep && (!$this->grouping || '.' !== $groupSep)) { |
|
|
|
|
93
|
182 |
|
$value = str_replace('.', $decSep, $value); |
94
|
182 |
|
} |
95
|
|
|
|
96
|
212 |
View Code Duplication |
if (',' !== $decSep && (!$this->grouping || ',' !== $groupSep)) { |
|
|
|
|
97
|
2 |
|
$value = str_replace(',', $decSep, $value); |
98
|
2 |
|
} |
99
|
|
|
|
100
|
212 |
|
$result = $formatter->parse($value, \NumberFormatter::TYPE_DOUBLE, $position); |
101
|
|
|
|
102
|
212 |
|
if (intl_is_failure($formatter->getErrorCode())) { |
103
|
10 |
|
throw new TransformationFailedException($formatter->getErrorMessage()); |
104
|
|
|
} |
105
|
|
|
|
106
|
202 |
|
if ($result >= PHP_INT_MAX || $result <= -PHP_INT_MAX) { |
107
|
4 |
|
throw new TransformationFailedException('I don\'t have a clear idea what infinity looks like'); |
108
|
|
|
} |
109
|
|
|
|
110
|
198 |
|
$encoding = mb_detect_encoding($value); |
111
|
198 |
|
$length = mb_strlen($value, $encoding); |
112
|
|
|
|
113
|
|
|
// After parsing, position holds the index of the character where the parsing stopped |
114
|
198 |
|
if ($position < $length) { |
115
|
|
|
// Check if there are unrecognized characters at the end of the |
116
|
|
|
// number (excluding whitespace characters) |
117
|
12 |
|
$remainder = trim(mb_substr($value, $position, $length, $encoding), " \t\n\r\0\x0b\xc2\xa0"); |
118
|
|
|
|
119
|
12 |
|
if ('' !== $remainder) { |
120
|
12 |
|
throw new TransformationFailedException( |
121
|
12 |
|
sprintf('The number contains unrecognized characters: "%s"', $remainder) |
122
|
12 |
|
); |
123
|
|
|
} |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
// Only the format() method in the NumberFormatter rounds, whereas parse() does not |
127
|
186 |
|
return $this->round($result); |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
/** |
131
|
|
|
* Returns a preconfigured \NumberFormatter instance. |
132
|
|
|
* |
133
|
|
|
* @return \NumberFormatter |
134
|
|
|
*/ |
135
|
212 |
|
protected function getNumberFormatter() |
136
|
|
|
{ |
137
|
212 |
|
$formatter = new \NumberFormatter($this->locale, \NumberFormatter::DECIMAL); |
138
|
|
|
|
139
|
212 |
|
if (null !== $this->precision) { |
140
|
152 |
|
$formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $this->precision); |
141
|
152 |
|
$formatter->setAttribute(\NumberFormatter::ROUNDING_MODE, $this->roundingMode); |
142
|
152 |
|
} |
143
|
|
|
|
144
|
212 |
|
$formatter->setAttribute(\NumberFormatter::GROUPING_USED, $this->grouping); |
145
|
|
|
|
146
|
212 |
|
return $formatter; |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
/** |
150
|
|
|
* Rounds a number according to the configured precision and rounding mode. |
151
|
|
|
* |
152
|
|
|
* @param int|float $number A number. |
153
|
|
|
* |
154
|
|
|
* @return int|float The rounded number. |
155
|
|
|
*/ |
156
|
186 |
|
private function round($number) |
157
|
|
|
{ |
158
|
186 |
|
if (null !== $this->precision && null !== $this->roundingMode) { |
159
|
|
|
// shift number to maintain the correct precision during rounding |
160
|
152 |
|
$roundingCoef = pow(10, $this->precision); |
161
|
152 |
|
$number *= $roundingCoef; |
162
|
|
|
|
163
|
152 |
|
switch ($this->roundingMode) { |
164
|
152 |
|
case \NumberFormatter::ROUND_CEILING: |
165
|
16 |
|
$number = ceil($number); |
166
|
16 |
|
break; |
167
|
136 |
|
case \NumberFormatter::ROUND_FLOOR: |
168
|
16 |
|
$number = floor($number); |
169
|
16 |
|
break; |
170
|
120 |
View Code Duplication |
case \NumberFormatter::ROUND_UP: |
|
|
|
|
171
|
16 |
|
$number = $number > 0 ? ceil($number) : floor($number); |
172
|
16 |
|
break; |
173
|
104 |
View Code Duplication |
case \NumberFormatter::ROUND_DOWN: |
|
|
|
|
174
|
16 |
|
$number = $number > 0 ? floor($number) : ceil($number); |
175
|
16 |
|
break; |
176
|
88 |
|
case \NumberFormatter::ROUND_HALFEVEN: |
177
|
40 |
|
$number = round($number, 0, PHP_ROUND_HALF_EVEN); |
178
|
40 |
|
break; |
179
|
48 |
|
case \NumberFormatter::ROUND_HALFUP: |
180
|
24 |
|
$number = round($number, 0, PHP_ROUND_HALF_UP); |
181
|
24 |
|
break; |
182
|
24 |
|
case \NumberFormatter::ROUND_HALFDOWN: |
183
|
24 |
|
$number = round($number, 0, PHP_ROUND_HALF_DOWN); |
184
|
24 |
|
break; |
185
|
152 |
|
} |
186
|
|
|
|
187
|
152 |
|
$number /= $roundingCoef; |
188
|
152 |
|
} |
189
|
|
|
|
190
|
186 |
|
return $number; |
191
|
|
|
} |
192
|
|
|
} |
193
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.