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, |
|
|
|
|
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
|
|
|
|
Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable: