These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | use SMW\DataValues\ValueFormatters\DataValueFormatter; |
||
4 | use SMW\IntlNumberFormatter; |
||
5 | use SMW\Localizer; |
||
6 | use SMW\Message; |
||
7 | |||
8 | /** |
||
9 | * @ingroup SMWDataValues |
||
10 | */ |
||
11 | |||
12 | /** |
||
13 | * This datavalue implements numerical datavalues, and supports optional |
||
14 | * unit conversions. It parses and manages unit strings, since even plain |
||
15 | * numbers may have (not further specified) units that are stored. However, |
||
16 | * only subclasses implement full unit conversion by extending the methods |
||
17 | * convertToMainUnit() and makeConversionValues(). |
||
18 | * |
||
19 | * Units work as follows: a unit is a string, but many such strings might |
||
20 | * refer to the same unit of measurement. There is always one string, that |
||
21 | * canonically represents the unit, and we will call this version of writing |
||
22 | * the unit the /unit id/. IDs for units are needed for tasks like duplicate |
||
23 | * avoidance. If no conversion information is given, any unit is its own ID. |
||
24 | * In any case, units are /normalised/, i.e. given a more standardised meaning |
||
25 | * before being processed. All units, IDs or otherwise, should be suitable for |
||
26 | * printout in wikitext, and main IDs should moreover be suitable for printout |
||
27 | * in HTML. |
||
28 | * |
||
29 | * Subclasses that support unit conversion may interpret the output format set |
||
30 | * via setOutputFormat() to allow a unit to be selected for display. Note that |
||
31 | * this setting does not affect the internal representation of the value |
||
32 | * though. So chosing a specific output format will change the behavior of |
||
33 | * output functions like getLongWikiText(), but not of functions that access |
||
34 | * the value itself, such as getUnit() or getDBKeys(). |
||
35 | * |
||
36 | * @author Markus Krötzsch |
||
37 | * @ingroup SMWDataValues |
||
38 | * |
||
39 | * @todo Wiki-HTML-conversion for unit strings must be revisited, as the current |
||
40 | * solution might be unsafe. |
||
41 | */ |
||
42 | class SMWNumberValue extends SMWDataValue { |
||
43 | |||
44 | /** |
||
45 | * Array with entries unit=>value, mapping a normalized unit to the |
||
46 | * converted value. Used for conversion tooltips. |
||
47 | * @var array |
||
48 | */ |
||
49 | protected $m_unitvalues; |
||
50 | |||
51 | /** |
||
52 | * Whether the unit is preferred as prefix or not |
||
53 | * |
||
54 | * @var array |
||
55 | */ |
||
56 | protected $prefixalUnitPreference = array(); |
||
57 | |||
58 | /** |
||
59 | * Canonical identifier for the unit that the user gave as input. Used |
||
60 | * to avoid printing this in conversion tooltips again. If the |
||
61 | * outputformat was set to show another unit, then the values of |
||
62 | * $m_caption and $m_unitin will be updated as if the formatted string |
||
63 | * had been the original user input, i.e. the two values reflect what |
||
64 | * is currently printed. |
||
65 | * @var string |
||
66 | */ |
||
67 | protected $m_unitin; |
||
68 | |||
69 | /** |
||
70 | * @var integer|null |
||
71 | */ |
||
72 | protected $precision = null; |
||
73 | |||
74 | /** |
||
75 | * @var IntlNumberFormatter |
||
76 | */ |
||
77 | private $intlNumberFormatter = null; |
||
78 | |||
79 | /** |
||
80 | * @since 2.4 |
||
81 | * |
||
82 | * @param string $typeid |
||
83 | */ |
||
84 | 49 | public function __construct( $typeid = '' ) { |
|
85 | 49 | parent::__construct( $typeid ); |
|
86 | 49 | $this->intlNumberFormatter = IntlNumberFormatter::getInstance(); |
|
87 | 49 | $this->intlNumberFormatter->initialize(); |
|
88 | 49 | } |
|
89 | |||
90 | /** |
||
91 | * Parse a string of the form "number unit" where unit is optional. The |
||
92 | * results are stored in the $number and $unit parameters. Returns an |
||
93 | * error code. |
||
94 | * @param $value string to parse |
||
95 | * @param $number call-by-ref parameter that will be set to the numerical value |
||
96 | * @param $unit call-by-ref parameter that will be set to the "unit" string (after the number) |
||
97 | * @return integer 0 (no errors), 1 (no number found at all), 2 (number |
||
98 | * too large for this platform) |
||
99 | */ |
||
100 | 47 | public function parseNumberValue( $value, &$number, &$unit, &$asPrefix = false ) { |
|
101 | |||
102 | 47 | $intlNumberFormatter = $this->getNumberFormatter(); |
|
103 | |||
104 | // Parse to find $number and (possibly) $unit |
||
105 | 47 | $kiloseparator = $intlNumberFormatter->getSeparator( |
|
106 | 47 | IntlNumberFormatter::THOUSANDS_SEPARATOR, |
|
107 | 47 | IntlNumberFormatter::CONTENT_LANGUAGE |
|
108 | ); |
||
109 | |||
110 | 47 | $decseparator = $intlNumberFormatter->getSeparator( |
|
111 | 47 | IntlNumberFormatter::DECIMAL_SEPARATOR, |
|
112 | 47 | IntlNumberFormatter::CONTENT_LANGUAGE |
|
113 | ); |
||
114 | |||
115 | // #753 |
||
116 | $regex = '/([-+]?\s*(?:' . |
||
117 | // Either numbers like 10,000.99 that start with a digit |
||
118 | 47 | '\d+(?:\\' . $kiloseparator . '\d\d\d)*(?:\\' . $decseparator . '\d+)?' . |
|
119 | // or numbers like .001 that start with the decimal separator |
||
120 | 47 | '|\\' . $decseparator . '\d+' . |
|
121 | 47 | ')\s*(?:[eE][-+]?\d+)?)/u'; |
|
122 | |||
123 | 47 | $parts = preg_split( |
|
124 | $regex, |
||
125 | 47 | trim( str_replace( array( ' ', ' ', ' ', ' ' ), '', $value ) ), |
|
126 | 47 | 2, |
|
127 | 47 | PREG_SPLIT_DELIM_CAPTURE |
|
128 | ); |
||
129 | |||
130 | 47 | if ( count( $parts ) >= 2 ) { |
|
131 | 47 | $numstring = str_replace( $kiloseparator, '', preg_replace( '/\s*/u', '', $parts[1] ) ); // simplify |
|
132 | 47 | if ( $decseparator != '.' ) { |
|
133 | 3 | $numstring = str_replace( $decseparator, '.', $numstring ); |
|
134 | } |
||
135 | 47 | list( $number ) = sscanf( $numstring, "%f" ); |
|
136 | 47 | if ( count( $parts ) >= 3 ) { |
|
137 | 47 | $asPrefix = $parts[0] !== ''; |
|
138 | 47 | $unit = $this->normalizeUnit( $parts[0] !== '' ? $parts[0] : $parts[2] ); |
|
139 | } |
||
140 | } |
||
141 | |||
142 | 47 | if ( ( count( $parts ) == 1 ) || ( $numstring === '' ) ) { // no number found |
|
143 | 1 | return 1; |
|
144 | 47 | } elseif ( is_infinite( $number ) ) { // number is too large for this platform |
|
145 | return 2; |
||
146 | } else { |
||
147 | 47 | return 0; |
|
148 | } |
||
149 | } |
||
150 | |||
151 | /** |
||
152 | * @see DataValue::parseUserValue |
||
153 | */ |
||
154 | 48 | protected function parseUserValue( $value ) { |
|
155 | // Set caption |
||
156 | 48 | if ( $this->m_caption === false ) { |
|
157 | 48 | $this->m_caption = $value; |
|
158 | } |
||
159 | |||
160 | 48 | if ( $value !== '' && $value{0} === ':' ) { |
|
161 | 1 | $this->addErrorMsg( array( 'smw-datavalue-invalid-number', $value ) ); |
|
162 | 1 | return; |
|
163 | } |
||
164 | |||
165 | 47 | $this->m_unitin = false; |
|
166 | 47 | $this->m_unitvalues = false; |
|
167 | 47 | $number = $unit = ''; |
|
168 | 47 | $error = $this->parseNumberValue( $value, $number, $unit ); |
|
169 | |||
170 | 47 | if ( $error == 1 ) { // no number found |
|
171 | 1 | $this->addErrorMsg( array( 'smw_nofloat', $value ) ); |
|
172 | 47 | } elseif ( $error == 2 ) { // number is too large for this platform |
|
173 | $this->addErrorMsg( array( 'smw_infinite', $value ) ); |
||
174 | 47 | } elseif ( $this->getTypeID() === '_num' && $unit !== '' ) { |
|
175 | 1 | $this->addErrorMsg( array( 'smw-datavalue-number-textnotallowed', $unit, $number ) ); |
|
176 | 47 | } elseif ( $number === null ) { |
|
177 | $this->addErrorMsg( array( 'smw-datavalue-number-nullnotallowed', $value ) ); // #1628 |
||
178 | 47 | } elseif ( $this->convertToMainUnit( $number, $unit ) === false ) { // so far so good: now convert unit and check if it is allowed |
|
179 | 9 | $this->addErrorMsg( array( 'smw_unitnotallowed', $unit ) ); |
|
180 | } // note that convertToMainUnit() also sets m_dataitem if valid |
||
181 | 47 | } |
|
182 | |||
183 | /** |
||
184 | * @see SMWDataValue::loadDataItem() |
||
185 | * @param $dataitem SMWDataItem |
||
186 | * @return boolean |
||
187 | */ |
||
188 | 28 | protected function loadDataItem( SMWDataItem $dataItem ) { |
|
189 | |||
190 | 28 | if ( $dataItem->getDIType() !== SMWDataItem::TYPE_NUMBER ) { |
|
191 | return false; |
||
192 | } |
||
193 | |||
194 | 28 | $this->m_dataitem = $dataItem; |
|
195 | 28 | $this->m_caption = false; |
|
196 | 28 | $this->m_unitin = false; |
|
197 | 28 | $this->makeUserValue(); |
|
198 | 28 | $this->m_unitvalues = false; |
|
199 | |||
200 | 28 | return true; |
|
201 | } |
||
202 | |||
203 | /** |
||
204 | * @see DataValue::setOutputFormat |
||
205 | * |
||
206 | * @param $string $formatstring |
||
207 | */ |
||
208 | 8 | public function setOutputFormat( $formatstring ) { |
|
209 | |||
210 | 8 | if ( $formatstring == $this->m_outformat ) { |
|
211 | return null; |
||
212 | } |
||
213 | |||
214 | // #1591 |
||
215 | 8 | $this->findPreferredLanguageFrom( $formatstring ); |
|
216 | |||
217 | // #1335 |
||
218 | 8 | $this->m_outformat = $this->findPrecisionFrom( $formatstring ); |
|
219 | |||
220 | 8 | if ( $this->isValid() ) { // update caption/unitin for this format |
|
221 | 8 | $this->m_caption = false; |
|
222 | 8 | $this->m_unitin = false; |
|
223 | 8 | $this->makeUserValue(); |
|
224 | } |
||
225 | 8 | } |
|
226 | |||
227 | /** |
||
228 | * @since 2.4 |
||
229 | * |
||
230 | * @return float |
||
231 | */ |
||
232 | 35 | public function getLocalizedFormattedNumber( $value ) { |
|
233 | 35 | return $this->getNumberFormatter()->format( $value, $this->getPreferredDisplayPrecision() ); |
|
234 | } |
||
235 | |||
236 | /** |
||
237 | * @since 2.4 |
||
238 | * |
||
239 | * @return float |
||
240 | */ |
||
241 | 16 | public function getNormalizedFormattedNumber( $value ) { |
|
242 | 16 | return $this->getNumberFormatter()->format( $value, $this->getPreferredDisplayPrecision(), IntlNumberFormatter::VALUE_FORMAT ); |
|
243 | } |
||
244 | |||
245 | /** |
||
246 | * @see DataValue::getShortWikiText |
||
247 | * |
||
248 | * @return string |
||
249 | */ |
||
250 | 33 | public function getShortWikiText( $linker = null ) { |
|
251 | 33 | return $this->getDataValueFormatter()->format( DataValueFormatter::WIKI_SHORT, $linker ); |
|
252 | } |
||
253 | |||
254 | /** |
||
255 | * @see DataValue::getShortHTMLText |
||
256 | * |
||
257 | * @return string |
||
258 | */ |
||
259 | 1 | public function getShortHTMLText( $linker = null ) { |
|
260 | 1 | return $this->getDataValueFormatter()->format( DataValueFormatter::HTML_SHORT, $linker ); |
|
261 | } |
||
262 | |||
263 | /** |
||
264 | * @see DataValue::getLongWikiText |
||
265 | * |
||
266 | * @return string |
||
267 | */ |
||
268 | public function getLongWikiText( $linker = null ) { |
||
269 | return $this->getDataValueFormatter()->format( DataValueFormatter::WIKI_LONG, $linker ); |
||
270 | } |
||
271 | |||
272 | /** |
||
273 | * @see DataValue::getLongHTMLText |
||
274 | * |
||
275 | * @return string |
||
276 | */ |
||
277 | 2 | public function getLongHTMLText( $linker = null ) { |
|
278 | 2 | return $this->getDataValueFormatter()->format( DataValueFormatter::HTML_LONG, $linker ); |
|
279 | } |
||
280 | |||
281 | 20 | public function getNumber() { |
|
282 | 20 | return $this->isValid() ? $this->m_dataitem->getNumber() : 32202; |
|
283 | } |
||
284 | |||
285 | 13 | public function getWikiValue() { |
|
286 | 13 | return $this->getDataValueFormatter()->format( DataValueFormatter::VALUE ); |
|
287 | } |
||
288 | |||
289 | /** |
||
290 | * @see DataVelue::getInfolinks |
||
291 | * |
||
292 | * @return array |
||
293 | */ |
||
294 | 1 | public function getInfolinks() { |
|
295 | |||
296 | // When generating an infoLink, use the normalized value without any |
||
297 | // precision limitation |
||
298 | 1 | $this->setOption( 'no.displayprecision', true ); |
|
299 | 1 | $this->setOption( 'content.language', Message::CONTENT_LANGUAGE ); |
|
300 | 1 | $infoLinks = parent::getInfolinks(); |
|
301 | 1 | $this->setOption( 'no.displayprecision', false ); |
|
302 | |||
303 | 1 | return $infoLinks; |
|
304 | } |
||
305 | |||
306 | /** |
||
307 | * @since 2.4 |
||
308 | * |
||
309 | * @return string |
||
310 | */ |
||
311 | 31 | public function getCanonicalMainUnit() { |
|
312 | 31 | return $this->m_unitin; |
|
313 | } |
||
314 | |||
315 | /** |
||
316 | * Returns array of converted unit-value-pairs that can be |
||
317 | * printed. |
||
318 | * |
||
319 | * @since 2.4 |
||
320 | * |
||
321 | * @return array |
||
322 | */ |
||
323 | 31 | public function getConvertedUnitValues() { |
|
324 | 31 | $this->makeConversionValues(); |
|
325 | 31 | return $this->m_unitvalues; |
|
326 | } |
||
327 | |||
328 | /** |
||
329 | * Return the unit in which the returned value is to be interpreted. |
||
330 | * This string is a plain UTF-8 string without wiki or html markup. |
||
331 | * The returned value is a canonical ID for the main unit. |
||
332 | * Returns the empty string if no unit is given for the value. |
||
333 | * Overwritten by subclasses that support units. |
||
334 | */ |
||
335 | 10 | public function getUnit() { |
|
336 | 10 | return ''; |
|
337 | } |
||
338 | |||
339 | /** |
||
340 | * @since 2.4 |
||
341 | * |
||
342 | * @param string $unit |
||
343 | * |
||
344 | * @return boolean |
||
345 | */ |
||
346 | 16 | public function hasPrefixalUnitPreference( $unit ) { |
|
347 | 16 | return isset( $this->prefixalUnitPreference[$unit] ) && $this->prefixalUnitPreference[$unit]; |
|
348 | } |
||
349 | |||
350 | /** |
||
351 | * Create links to mapping services based on a wiki-editable message. |
||
352 | * The parameters available to the message are: |
||
353 | * $1: string of numerical value in English punctuation |
||
354 | * $2: string of integer version of value, in English punctuation |
||
355 | * |
||
356 | * @return array |
||
357 | */ |
||
358 | 1 | protected function getServiceLinkParams() { |
|
359 | 1 | if ( $this->isValid() ) { |
|
360 | 1 | return array( strval( $this->m_dataitem->getNumber() ), strval( round( $this->m_dataitem->getNumber() ) ) ); |
|
361 | } else { |
||
362 | return array(); |
||
363 | } |
||
364 | } |
||
365 | |||
366 | /** |
||
367 | * Transform a (typically unit-) string into a normalised form, |
||
368 | * so that, e.g., "km²" and "km<sup>2</sup>" do not need to be |
||
369 | * distinguished. |
||
370 | */ |
||
371 | 47 | public function normalizeUnit( $unit ) { |
|
372 | 47 | $unit = str_replace( array( '[[', ']]' ), '', trim( $unit ) ); // allow simple links to be used inside annotations |
|
373 | 47 | $unit = str_replace( array( '²', '<sup>2</sup>' ), '²', $unit ); |
|
374 | 47 | $unit = str_replace( array( '³', '<sup>3</sup>' ), '³', $unit ); |
|
375 | 47 | return smwfXMLContentEncode( $unit ); |
|
376 | } |
||
377 | |||
378 | /** |
||
379 | * Compute the value based on the given input number and unit string. |
||
380 | * If the unit is not supported, return false, otherwise return true. |
||
381 | * This is called when parsing user input, where the given unit value |
||
382 | * has already been normalized. |
||
383 | * |
||
384 | * This class does not support any (non-empty) units, but subclasses |
||
385 | * may overwrite this behavior. |
||
386 | * @param $number float value obtained by parsing user input |
||
387 | * @param $unit string after the numericla user input |
||
388 | * @return boolean specifying if the unit string is allowed |
||
389 | */ |
||
390 | 37 | protected function convertToMainUnit( $number, $unit ) { |
|
391 | 37 | $this->m_dataitem = new SMWDINumber( $number ); |
|
392 | 37 | $this->m_unitin = ''; |
|
393 | 37 | return ( $unit === '' ); |
|
394 | } |
||
395 | |||
396 | /** |
||
397 | * This method creates an array of unit-value-pairs that should be |
||
398 | * printed. Units are the keys and should be canonical unit IDs. |
||
399 | * The result is stored in $this->m_unitvalues. Again, any class that |
||
400 | * requires effort for doing this should first check whether the array |
||
401 | * is already set (i.e. not false) before doing any work. |
||
402 | * Note that the values should be plain numbers. Output formatting is done |
||
403 | * later when needed. Also, it should be checked if the value is valid |
||
404 | * before trying to calculate with its contents. |
||
405 | * This method also must call or implement convertToMainUnit(). |
||
406 | * |
||
407 | * Overwritten by subclasses that support units. |
||
408 | */ |
||
409 | 24 | protected function makeConversionValues() { |
|
410 | 24 | $this->m_unitvalues = array( '' => $this->m_dataitem->getNumber() ); |
|
411 | 24 | } |
|
412 | |||
413 | /** |
||
414 | * This method is used when no user input was given to find the best |
||
415 | * values for m_unitin and m_caption. After conversion, |
||
416 | * these fields will look as if they were generated from user input, |
||
417 | * and convertToMainUnit() will have been called (if not, it would be |
||
418 | * blocked by the presence of m_unitin). |
||
419 | * |
||
420 | * Overwritten by subclasses that support units. |
||
421 | */ |
||
422 | 22 | protected function makeUserValue() { |
|
423 | 22 | $this->m_caption = ''; |
|
424 | |||
425 | 22 | $number = $this->m_dataitem->getNumber(); |
|
426 | |||
427 | // -u is the format for displaying the unit only |
||
428 | 22 | if ( $this->m_outformat == '-u' ) { |
|
429 | $this->m_caption = ''; |
||
430 | 22 | } elseif ( ( $this->m_outformat != '-' ) && ( $this->m_outformat != '-n' ) ) { |
|
431 | 22 | $this->m_caption = $this->getLocalizedFormattedNumber( $number ); |
|
432 | } else { |
||
433 | 2 | $this->m_caption = $this->getNormalizedFormattedNumber( $number ); |
|
434 | } |
||
435 | |||
436 | // no unit ever, so nothing to do about this |
||
437 | 22 | $this->m_unitin = ''; |
|
438 | 22 | } |
|
439 | |||
440 | /** |
||
441 | * Return an array of major unit strings (ids only recommended) supported by |
||
442 | * this datavalue. |
||
443 | * |
||
444 | * Overwritten by subclasses that support units. |
||
445 | */ |
||
446 | public function getUnitList() { |
||
447 | return array( '' ); |
||
448 | } |
||
449 | |||
450 | 35 | protected function getPreferredDisplayPrecision() { |
|
451 | |||
452 | // In case of a value description, don't restrict the value with a display precision |
||
453 | 35 | if ( $this->getProperty() === null || $this->getOptionValueFor( 'value.description' ) || $this->getOptionValueFor( 'no.displayprecision' ) ) { |
|
454 | 8 | return false; |
|
455 | } |
||
456 | |||
457 | 35 | if ( $this->precision === null ) { |
|
458 | 35 | $this->precision = $this->getPropertySpecificationLookup()->getDisplayPrecisionFor( |
|
459 | 35 | $this->getProperty() |
|
460 | ); |
||
461 | } |
||
462 | |||
463 | 35 | return $this->precision; |
|
464 | } |
||
465 | |||
466 | 8 | private function findPrecisionFrom( $formatstring ) { |
|
467 | |||
468 | 8 | if ( strpos( $formatstring, '-' ) === false ) { |
|
469 | 5 | return $formatstring; |
|
470 | } |
||
471 | |||
472 | 5 | $parts = explode( '-', $formatstring ); |
|
473 | |||
474 | // Find precision from annotated -p<number of digits> formatstring which |
||
475 | // has priority over a possible _PREC value |
||
476 | 5 | foreach ( $parts as $key => $value ) { |
|
477 | 5 | if ( strpos( $value, 'p' ) !== false && is_numeric( substr( $value, 1 ) ) ) { |
|
478 | 2 | $this->precision = strval( substr( $value, 1 ) ); |
|
479 | 5 | unset( $parts[$key] ); |
|
480 | } |
||
481 | } |
||
482 | |||
483 | // Rebuild formatstring without a possible p element to ensure other |
||
484 | // options can be used in combination such as -n-p2 etc. |
||
485 | 5 | return implode( '-', $parts ); |
|
486 | } |
||
487 | |||
488 | 48 | private function getNumberFormatter() { |
|
489 | |||
490 | 48 | $this->intlNumberFormatter->setOption( |
|
491 | 48 | 'user.language', |
|
492 | 48 | $this->getOptionValueFor( 'user.language' ) |
|
493 | ); |
||
494 | |||
495 | 48 | $this->intlNumberFormatter->setOption( |
|
496 | 48 | 'content.language', |
|
497 | 48 | $this->getOptionValueFor( 'content.language' ) |
|
498 | ); |
||
499 | |||
500 | 48 | $this->intlNumberFormatter->setOption( |
|
501 | 48 | 'separator.thousands', |
|
502 | 48 | $this->getOptionValueFor( 'separator.thousands' ) |
|
503 | ); |
||
504 | |||
505 | 48 | $this->intlNumberFormatter->setOption( |
|
506 | 48 | 'separator.decimal', |
|
507 | 48 | $this->getOptionValueFor( 'separator.decimal' ) |
|
508 | ); |
||
509 | |||
510 | 48 | return $this->intlNumberFormatter; |
|
511 | } |
||
512 | |||
513 | 8 | private function findPreferredLanguageFrom( &$formatstring ) { |
|
514 | // Localized preferred user language |
||
515 | 8 | if ( strpos( $formatstring, 'LOCL' ) !== false && ( $languageCode = Localizer::getLanguageCodeFrom( $formatstring ) ) !== false ) { |
|
0 ignored issues
–
show
|
|||
516 | 1 | $this->intlNumberFormatter->setOption( |
|
517 | 1 | 'preferred.language', |
|
518 | $languageCode |
||
519 | ); |
||
520 | } |
||
521 | |||
522 | // Remove any remaining |
||
523 | 8 | $formatstring = str_replace( array( '#LOCL', 'LOCL' ), '', $formatstring ); |
|
524 | 8 | } |
|
525 | |||
526 | } |
||
527 |
This method has been deprecated. The supplier of the class has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.