These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | namespace SMW; |
||
4 | |||
5 | use InvalidArgumentException; |
||
6 | |||
7 | /** |
||
8 | * @license GNU GPL v2+ |
||
9 | * @since 2.1 |
||
10 | * |
||
11 | * @author mwjames |
||
12 | * @author Markus Krötzsch |
||
13 | */ |
||
14 | class IntlNumberFormatter { |
||
15 | |||
16 | /** |
||
17 | * Localization related constants |
||
18 | */ |
||
19 | const CONTENT_LANGUAGE = Message::CONTENT_LANGUAGE; |
||
20 | const USER_LANGUAGE = Message::USER_LANGUAGE; |
||
21 | |||
22 | /** |
||
23 | * Separator related constants |
||
24 | */ |
||
25 | const DECIMAL_SEPARATOR = 'DS'; |
||
26 | const THOUSANDS_SEPARATOR = 'TS'; |
||
27 | |||
28 | /** |
||
29 | * Format related constants |
||
30 | */ |
||
31 | const DEFAULT_FORMAT = 'DF'; |
||
32 | const VALUE_FORMAT = 'VF'; |
||
33 | |||
34 | /** |
||
35 | * @var IntlNumberFormatter |
||
36 | */ |
||
37 | private static $instance = null; |
||
38 | |||
39 | /** |
||
40 | * @var Options |
||
41 | */ |
||
42 | private $options = null; |
||
43 | |||
44 | /** |
||
45 | * @var integer |
||
46 | */ |
||
47 | private $maxNonExpNumber = null; |
||
48 | |||
49 | /** |
||
50 | * @var integer |
||
51 | */ |
||
52 | private $defaultPrecision = 3; |
||
53 | |||
54 | /** |
||
55 | * @since 2.1 |
||
56 | * |
||
57 | * @param integer $maxNonExpNumber |
||
58 | */ |
||
59 | 26 | public function __construct( $maxNonExpNumber ) { |
|
60 | 26 | $this->maxNonExpNumber = $maxNonExpNumber; |
|
61 | 26 | $this->options = new Options(); |
|
62 | 26 | } |
|
63 | |||
64 | /** |
||
65 | * @since 2.1 |
||
66 | * |
||
67 | * @return IntlNumberFormatter |
||
68 | */ |
||
69 | 49 | public static function getInstance() { |
|
70 | |||
71 | 49 | if ( self::$instance === null ) { |
|
72 | self::$instance = new self( |
||
73 | $GLOBALS['smwgMaxNonExpNumber'] |
||
74 | ); |
||
75 | } |
||
76 | |||
77 | 49 | return self::$instance; |
|
78 | } |
||
79 | |||
80 | /** |
||
81 | * @since 2.1 |
||
82 | */ |
||
83 | public function clear() { |
||
84 | self::$instance = null; |
||
85 | } |
||
86 | |||
87 | /** |
||
88 | * @since 2.4 |
||
89 | */ |
||
90 | 48 | public function initialize() { |
|
91 | 48 | $this->options->set( 'separator.decimal', false ); |
|
92 | 48 | $this->options->set( 'separator.thousands', false ); |
|
93 | 48 | $this->options->set( 'user.language', false ); |
|
94 | 48 | $this->options->set( 'content.language', false ); |
|
95 | 48 | $this->options->set( 'preferred.language', false ); |
|
96 | 48 | } |
|
97 | |||
98 | /** |
||
99 | * @since 2.4 |
||
100 | * |
||
101 | * @return string $key |
||
102 | * @param mixed $value |
||
103 | */ |
||
104 | 71 | public function setOption( $key, $value ) { |
|
105 | 71 | $this->options->set( $key, $value ); |
|
106 | 71 | } |
|
107 | |||
108 | /** |
||
109 | * @since 2.4 |
||
110 | * |
||
111 | * @param integer $type |
||
112 | * @param string|integer $locale |
||
113 | * |
||
114 | * @return string |
||
115 | */ |
||
116 | 72 | public function getSeparator( $type, $locale = '' ) { |
|
117 | |||
118 | 72 | $language = $locale === self::USER_LANGUAGE ? $this->getUserLanguage() : $this->getContentLanguage(); |
|
119 | |||
120 | 72 | if ( $type === self::DECIMAL_SEPARATOR ) { |
|
121 | 67 | return $this->getPreferredLocalizedSeparator( 'separator.decimal', 'smw_decseparator', $language ); |
|
122 | } |
||
123 | |||
124 | 56 | if ( $type === self::THOUSANDS_SEPARATOR ) { |
|
125 | 55 | return $this->getPreferredLocalizedSeparator( 'separator.thousands', 'smw_kiloseparator', $language ); |
|
126 | } |
||
127 | |||
128 | 1 | throw new InvalidArgumentException( $type . " is unknown" ); |
|
129 | } |
||
130 | |||
131 | /** |
||
132 | * This method formats a float number value according to the given language and |
||
133 | * precision settings, with some intelligence to produce readable output. Used |
||
134 | * to format a number that was not hand-formatted by a user. |
||
135 | * |
||
136 | * @param mixed $value input number |
||
137 | * @param integer|false $precision optional positive integer, controls how many digits after |
||
138 | * the decimal point are shown |
||
139 | * @param string|integer $format |
||
140 | * |
||
141 | * @since 2.1 |
||
142 | * |
||
143 | * @return string |
||
144 | */ |
||
145 | 48 | public function format( $value, $precision = false, $format = '' ) { |
|
146 | |||
147 | 48 | if ( $format === self::VALUE_FORMAT ) { |
|
148 | 26 | return $this->getValueFormattedNumberWithPrecision( $value, $precision ); |
|
149 | } |
||
150 | |||
151 | 37 | if ( $precision !== false || $format === self::DEFAULT_FORMAT ) { |
|
152 | 4 | return $this->getDefaultFormattedNumberWithPrecision( $value, $precision ); |
|
153 | } |
||
154 | |||
155 | 36 | return $this->getFormattedNumberByHeuristicRule( $value, $precision ); |
|
156 | } |
||
157 | |||
158 | /** |
||
159 | * This method formats a float number value according to the given language and |
||
160 | * precision settings, with some intelligence to produce readable output. Used |
||
161 | * to format a number that was not hand-formatted by a user. |
||
162 | * |
||
163 | * @param mixed $value input number |
||
164 | * @param integer|false $precision optional positive integer, controls how many digits after |
||
165 | * the decimal point are shown |
||
166 | * |
||
167 | * @since 2.1 |
||
168 | * |
||
169 | * @return string |
||
170 | */ |
||
171 | 36 | private function getFormattedNumberByHeuristicRule( $value, $precision = false ) { |
|
172 | |||
173 | // BC configuration to keep default behaviour |
||
174 | 36 | $precision = $this->defaultPrecision; |
|
175 | |||
176 | 36 | $decseparator = $this->getSeparator( |
|
177 | 36 | self::DECIMAL_SEPARATOR, |
|
178 | 36 | self::USER_LANGUAGE |
|
179 | ); |
||
180 | |||
181 | // If number is a trillion or more, then switch to scientific |
||
182 | // notation. If number is less than 0.0000001 (i.e. twice precision), |
||
183 | // then switch to scientific notation. Otherwise print number |
||
184 | // using number_format. This may lead to 1.200, so then use trim to |
||
185 | // remove trailing zeroes. |
||
186 | 36 | $doScientific = false; |
|
187 | |||
188 | // @todo: Don't do all this magic for integers, since the formatting does not fit there |
||
189 | // correctly. E.g. one would have integers formatted as 1234e6, not as 1.234e9, right? |
||
190 | // The "$value!=0" is relevant: we want to scientify numbers that are close to 0, but never 0! |
||
191 | 36 | if ( ( $precision > 0 ) && ( $value != 0 ) ) { |
|
192 | 36 | $absValue = abs( $value ); |
|
193 | 36 | if ( $absValue >= $this->maxNonExpNumber ) { |
|
194 | 1 | $doScientific = true; |
|
195 | 35 | } elseif ( $absValue < pow( 10, - $precision ) ) { |
|
196 | 5 | $doScientific = true; |
|
197 | 35 | } elseif ( $absValue < 1 ) { |
|
198 | 7 | if ( $absValue < pow( 10, - $precision ) ) { |
|
199 | $doScientific = true; |
||
200 | } else { |
||
201 | // Increase decimal places for small numbers, e.g. .00123 should be 5 places. |
||
202 | 7 | for ( $i = 0.1; $absValue <= $i; $i *= 0.1 ) { |
|
203 | 4 | $precision++; |
|
204 | } |
||
205 | } |
||
206 | } |
||
207 | } |
||
208 | |||
209 | 36 | if ( $doScientific ) { |
|
210 | // Should we use decimal places here? |
||
211 | 6 | $value = sprintf( "%1.6e", $value ); |
|
212 | // Make it more readable by removing trailing zeroes from n.n00e7. |
||
213 | 6 | $value = preg_replace( '/(\\.\\d+?)0*e/u', '${1}e', $value, 1 ); |
|
214 | // NOTE: do not use the optional $count parameter with preg_replace. We need to |
||
215 | // remain compatible with PHP 4.something. |
||
216 | 6 | if ( $decseparator !== '.' ) { |
|
217 | 6 | $value = str_replace( '.', $decseparator, $value ); |
|
218 | } |
||
219 | } else { |
||
220 | 35 | $value = $this->doFormatWithPrecision( |
|
221 | $value, |
||
222 | $precision, |
||
223 | $decseparator, |
||
224 | 35 | $this->getSeparator( self::THOUSANDS_SEPARATOR, self::USER_LANGUAGE ) |
|
225 | ); |
||
226 | |||
227 | // Make it more readable by removing ending .000 from nnn.000 |
||
228 | // Assumes substr is faster than a regular expression replacement. |
||
229 | 35 | $end = $decseparator . str_repeat( '0', $precision ); |
|
230 | 35 | $lenEnd = strlen( $end ); |
|
231 | |||
232 | 35 | if ( substr( $value, - $lenEnd ) === $end ) { |
|
233 | 31 | $value = substr( $value, 0, - $lenEnd ); |
|
234 | } else { |
||
235 | // If above replacement occurred, no need to do the next one. |
||
236 | // Make it more readable by removing trailing zeroes from nn.n00. |
||
237 | 21 | $value = preg_replace( "/(\\$decseparator\\d+?)0*$/u", '$1', $value, 1 ); |
|
238 | } |
||
239 | } |
||
240 | |||
241 | 36 | return $value; |
|
242 | } |
||
243 | |||
244 | 26 | private function getValueFormattedNumberWithPrecision( $value, $precision = false ) { |
|
245 | |||
246 | // The decimal are in ISO format (.), the separator as plain representation |
||
247 | // may collide with the content language (FR) therefore use the content language |
||
248 | // to match the decimal separator |
||
249 | 26 | if ( $this->isScientific( $value ) ) { |
|
250 | 5 | return $this->doFormatExponentialNotation( $value ); |
|
251 | } |
||
252 | |||
253 | 24 | $precision = $precision === false ? $this->getPrecisionFrom( $value ) : $precision; |
|
254 | |||
255 | 24 | return $this->doFormatWithPrecision( |
|
256 | $value, |
||
257 | $precision, |
||
0 ignored issues
–
show
|
|||
258 | 24 | $this->getSeparator( self::DECIMAL_SEPARATOR, self::CONTENT_LANGUAGE ), |
|
259 | 24 | '' |
|
260 | ); |
||
261 | } |
||
262 | |||
263 | 4 | private function getDefaultFormattedNumberWithPrecision( $value, $precision = false ) { |
|
264 | |||
265 | 4 | if ( $precision === false ) { |
|
266 | return $this->isDecimal( $value ) ? $this->applyDefaultPrecision( $value ) : floatval( $value ); |
||
267 | } |
||
268 | |||
269 | 4 | return $this->doFormatWithPrecision( |
|
270 | $value, |
||
271 | $precision, |
||
272 | 4 | $this->getSeparator( self::DECIMAL_SEPARATOR, self::USER_LANGUAGE ), |
|
273 | 4 | $this->getSeparator( self::THOUSANDS_SEPARATOR, self::USER_LANGUAGE ) |
|
274 | ); |
||
275 | } |
||
276 | |||
277 | private function isDecimal( $value ) { |
||
278 | return floor( $value ) !== $value; |
||
279 | } |
||
280 | |||
281 | 36 | private function isScientific( $value ) { |
|
282 | 36 | return strpos( $value, 'E' ) !== false || strpos( $value, 'e' ) !== false; |
|
283 | } |
||
284 | |||
285 | private function applyDefaultPrecision( $value ) { |
||
286 | return round( $value, $this->defaultPrecision ); |
||
287 | } |
||
288 | |||
289 | 45 | private function getPrecisionFrom( $value ) { |
|
290 | 45 | return strlen( strrchr( $value, "." ) ) - 1; |
|
291 | } |
||
292 | |||
293 | 5 | private function doFormatExponentialNotation( $value ) { |
|
294 | 5 | return str_replace( |
|
295 | 5 | array( '.', 'E' ), |
|
296 | 5 | array( $this->getSeparator( self::DECIMAL_SEPARATOR, self::CONTENT_LANGUAGE ), 'e' ), |
|
297 | $value |
||
298 | ); |
||
299 | } |
||
300 | |||
301 | 45 | private function doFormatWithPrecision( $value, $precision = false, $decimal, $thousand ) { |
|
302 | |||
303 | 45 | $replacement = 0; |
|
304 | |||
305 | // Don't try to be more precise than the actual value (e.g avoid turning |
||
306 | // 72.769482308 into 72.76948230799999350892904) |
||
307 | 45 | if ( ( $actualPrecision = $this->getPrecisionFrom( $value ) ) < $precision && $actualPrecision > 0 && !$this->isScientific( $value ) ) { |
|
308 | 18 | $replacement = $precision - $actualPrecision; |
|
309 | 18 | $precision = $actualPrecision; |
|
310 | } |
||
311 | |||
312 | // Format to some level of precision; number_format does rounding and |
||
313 | // locale formatting, x and y are used temporarily since number_format |
||
314 | // supports only single characters for either |
||
315 | 45 | $value = number_format( (float)$value, $precision, 'x', 'y' ); |
|
316 | 45 | $value = str_replace( |
|
317 | 45 | array( 'x', 'y' ), |
|
318 | array( |
||
319 | 45 | $decimal, |
|
320 | 45 | $thousand |
|
321 | ), |
||
322 | $value |
||
323 | ); |
||
324 | |||
325 | 45 | if ( $replacement > 0 ) { |
|
326 | 18 | $value .= str_repeat( '0', $replacement ); |
|
327 | } |
||
328 | |||
329 | 45 | return $value; |
|
330 | } |
||
331 | |||
332 | 41 | private function getUserLanguage() { |
|
333 | |||
334 | 41 | $language = Message::USER_LANGUAGE; |
|
335 | |||
336 | // The preferred language is set when the output formatter contained |
||
337 | // something like LOCL@es |
||
338 | |||
339 | 41 | if ( $this->options->has( 'preferred.language' ) && $this->options->get( 'preferred.language' ) ) { |
|
340 | 1 | $language = $this->options->get( 'preferred.language' ); |
|
341 | 41 | } elseif ( $this->options->has( 'user.language' ) && $this->options->get( 'user.language' ) ) { |
|
342 | 41 | $language = $this->options->get( 'user.language' ); |
|
343 | } |
||
344 | |||
345 | 41 | return $language; |
|
346 | } |
||
347 | |||
348 | 64 | private function getContentLanguage() { |
|
349 | |||
350 | 64 | $language = Message::CONTENT_LANGUAGE; |
|
351 | |||
352 | 64 | if ( $this->options->has( 'content.language' ) && $this->options->get( 'content.language' ) ) { |
|
353 | 61 | $language = $this->options->get( 'content.language' ); |
|
354 | } |
||
355 | |||
356 | 64 | return $language; |
|
357 | } |
||
358 | |||
359 | 71 | private function getPreferredLocalizedSeparator( $custom, $standard, $language ) { |
|
360 | |||
361 | 71 | if ( $this->options->has( $custom ) && ( $separator = $this->options->get( $custom ) ) !== false ) { |
|
362 | 1 | return $separator; |
|
363 | } |
||
364 | |||
365 | 70 | return Message::get( $standard, Message::TEXT, $language ); |
|
366 | } |
||
367 | |||
368 | } |
||
369 |
If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:
If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.