Completed
Push — master ( 14d2bd...06e609 )
by mw
81:37 queued 59:24
created

includes/datavalues/SMW_DV_Number.php (4 issues)

Upgrade to new PHP Analysis Engine

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\Highlighter;
4
use SMW\IntlNumberFormatter;
5
use SMW\Localizer;
6
use SMW\DataValues\ValueFormatters\DataValueFormatter;
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 46
	public function __construct( $typeid = '' ) {
85 46
		parent::__construct( $typeid );
86 46
		$this->intlNumberFormatter = IntlNumberFormatter::getInstance();
87 46
		$this->intlNumberFormatter->initialize();
88 46
	}
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 44
	public function parseNumberValue( $value, &$number, &$unit, &$asPrefix = false ) {
101
102 44
		$intlNumberFormatter = $this->getNumberFormatter();
103
104
		// Parse to find $number and (possibly) $unit
105 44
		$kiloseparator = $intlNumberFormatter->getSeparator(
106 44
			IntlNumberFormatter::THOUSANDS_SEPARATOR,
107 44
			IntlNumberFormatter::CONTENT_LANGUAGE
108
		);
109
110 44
		$decseparator = $intlNumberFormatter->getSeparator(
111 44
			IntlNumberFormatter::DECIMAL_SEPARATOR,
112 44
			IntlNumberFormatter::CONTENT_LANGUAGE
113
		);
114
115
		// #753
116
		$regex = '/([-+]?\s*(?:' .
117
				// Either numbers like 10,000.99 that start with a digit
118 44
				'\d+(?:\\' . $kiloseparator . '\d\d\d)*(?:\\' . $decseparator . '\d+)?' .
119
				// or numbers like .001 that start with the decimal separator
120 44
				'|\\' . $decseparator . '\d+' .
121 44
				')\s*(?:[eE][-+]?\d+)?)/u';
122
123 44
		$parts = preg_split(
124
			$regex,
125 44
			trim( str_replace( array( '&nbsp;', '&#160;', '&thinsp;', ' ' ), '', $value ) ),
126 44
			2,
127 44
			PREG_SPLIT_DELIM_CAPTURE
128
		);
129
130 44
		if ( count( $parts ) >= 2 ) {
131 43
			$numstring = str_replace( $kiloseparator, '', preg_replace( '/\s*/u', '', $parts[1] ) ); // simplify
132 43
			if ( $decseparator != '.' ) {
133 2
				$numstring = str_replace( $decseparator, '.', $numstring );
134
			}
135 43
			list( $number ) = sscanf( $numstring, "%f" );
136 43
			if ( count( $parts ) >= 3  ) {
137 43
				$asPrefix = $parts[0] !== '';
138 43
				$unit = $this->normalizeUnit( $parts[0] !== '' ? $parts[0] : $parts[2] );
139
			}
140
		}
141
142 44
		if ( ( count( $parts ) == 1 ) || ( $numstring === '' ) ) { // no number found
143 2
			return 1;
144 43
		} elseif ( is_infinite( $number ) ) { // number is too large for this platform
145
			return 2;
146
		} else {
147 43
			return 0;
148
		}
149
	}
150
151
	/**
152
	 * @see DataValue::parseUserValue
153
	 */
154 45
	protected function parseUserValue( $value ) {
155
		// Set caption
156 45
		if ( $this->m_caption === false ) {
157 45
			$this->m_caption = $value;
158
		}
159
160 45
		if ( $value !== '' && $value{0} === ':' ) {
161 1
			$this->addErrorMsg( array( 'smw-datavalue-invalid-number', $value ) );
162 1
			return;
163
		}
164
165 44
		$this->m_unitin = false;
166 44
		$this->m_unitvalues = false;
167 44
		$number = $unit = '';
168 44
		$error = $this->parseNumberValue( $value, $number, $unit );
169
170 44
		if ( $error == 1 ) { // no number found
171 2
			$this->addErrorMsg( array( 'smw_nofloat', $value ) );
172 43
		} elseif ( $error == 2 ) { // number is too large for this platform
173
			$this->addErrorMsg( array( 'smw_infinite', $value ) );
174 43
		} elseif ( $this->getTypeID() === '_num' && $unit !== '' ) {
175 1
			$this->addErrorMsg( array( 'smw-datavalue-number-textnotallowed', $unit, $number ) );
176 43
		} elseif ( $this->convertToMainUnit( $number, $unit ) === false ) { // so far so good: now convert unit and check if it is allowed
177 9
			$this->addErrorMsg( array( 'smw_unitnotallowed', $unit ) );
178
		} // note that convertToMainUnit() also sets m_dataitem if valid
179 44
	}
180
181
	/**
182
	 * @see SMWDataValue::loadDataItem()
183
	 * @param $dataitem SMWDataItem
184
	 * @return boolean
185
	 */
186 24
	protected function loadDataItem( SMWDataItem $dataItem ) {
187
188 24
		if ( $dataItem->getDIType() !== SMWDataItem::TYPE_NUMBER ) {
189
			return false;
190
		}
191
192 24
		$this->m_dataitem = $dataItem;
193 24
		$this->m_caption = false;
194 24
		$this->m_unitin = false;
195 24
		$this->makeUserValue();
196 24
		$this->m_unitvalues = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type array of property $m_unitvalues.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
197
198 24
		return true;
199
	}
200
201
	/**
202
	 * @see DataValue::setOutputFormat
203
	 *
204
	 * @param $string $formatstring
205
	 */
206 6
	public function setOutputFormat( $formatstring ) {
207
208 6
		if ( $formatstring == $this->m_outformat ) {
209
			return null;
210
		}
211
212
		// #1591
213 6
		$this->findPreferredLanguageFrom( $formatstring );
214
215
		// #1335
216 6
		$this->m_outformat = $this->findPrecisionFrom( $formatstring );
217
218 6
		if ( $this->isValid() ) { // update caption/unitin for this format
219 6
			$this->m_caption = false;
220 6
			$this->m_unitin = false;
221 6
			$this->makeUserValue();
222
		}
223 6
	}
224
225
	/**
226
	 * @since 2.4
227
	 *
228
	 * @return float
229
	 */
230 31
	public function getLocalizedFormattedNumber( $value ) {
231 31
		return $this->getNumberFormatter()->format( $value, $this->getPreferredDisplayPrecision() );
232
	}
233
234
	/**
235
	 * @since 2.4
236
	 *
237
	 * @return float
238
	 */
239 14
	public function getNormalizedFormattedNumber( $value ) {
240 14
		return $this->getNumberFormatter()->format( $value, $this->getPreferredDisplayPrecision(), IntlNumberFormatter::VALUE_FORMAT );
241
	}
242
243
	/**
244
	 * @see DataValue::getShortWikiText
245
	 *
246
	 * @return string
247
	 */
248 30
	public function getShortWikiText( $linker = null ) {
249 30
		return $this->getDataValueFormatter()->format( DataValueFormatter::WIKI_SHORT, $linker );
250
	}
251
252
	/**
253
	 * @see DataValue::getShortHTMLText
254
	 *
255
	 * @return string
256
	 */
257 1
	public function getShortHTMLText( $linker = null ) {
258 1
		return $this->getDataValueFormatter()->format( DataValueFormatter::HTML_SHORT, $linker );
259
	}
260
261
	/**
262
	 * @see DataValue::getLongWikiText
263
	 *
264
	 * @return string
265
	 */
266
	public function getLongWikiText( $linker = null ) {
267
		return $this->getDataValueFormatter()->format( DataValueFormatter::WIKI_LONG, $linker );
268
	}
269
270
	/**
271
	 * @see DataValue::getLongHTMLText
272
	 *
273
	 * @return string
274
	 */
275 1
	public function getLongHTMLText( $linker = null ) {
276 1
		return $this->getDataValueFormatter()->format( DataValueFormatter::HTML_LONG, $linker );
277
	}
278
279 18
	public function getNumber() {
280 18
		return $this->isValid() ? $this->m_dataitem->getNumber() : 32202;
281
	}
282
283 11
	public function getWikiValue() {
284 11
		return $this->getDataValueFormatter()->format( DataValueFormatter::VALUE );
285
	}
286
287
	/**
288
	 * @see DataVelue::getInfolinks
289
	 *
290
	 * @return array
291
	 */
292
	public function getInfolinks() {
293
294
		// When generating an infoLink, use the normalized value without any
295
		// precision limitation
296
		$this->setOption( 'no.displayprecision', true );
297
		$infoLinks = parent::getInfolinks();
298
		$this->setOption( 'no.displayprecision', false );
299
300
		return $infoLinks;
301
	}
302
303
	/**
304
	 * @since 2.4
305
	 *
306
	 * @return string
307
	 */
308 27
	public function getCanonicalMainUnit() {
309 27
		return $this->m_unitin;
310
	}
311
312
	/**
313
	 * Returns array of converted unit-value-pairs that can be
314
	 * printed.
315
	 *
316
	 * @since 2.4
317
	 *
318
	 * @return array
319
	 */
320 27
	public function getConvertedUnitValues() {
321 27
		$this->makeConversionValues();
322 27
		return $this->m_unitvalues;
323
	}
324
325
	/**
326
	 * Return the unit in which the returned value is to be interpreted.
327
	 * This string is a plain UTF-8 string without wiki or html markup.
328
	 * The returned value is a canonical ID for the main unit.
329
	 * Returns the empty string if no unit is given for the value.
330
	 * Overwritten by subclasses that support units.
331
	 */
332 8
	public function getUnit() {
333 8
		return '';
334
	}
335
336
	/**
337
	 * @since 2.4
338
	 *
339
	 * @param string $unit
340
	 *
341
	 * @return boolean
342
	 */
343 15
	public function hasPrefixalUnitPreference( $unit ) {
344 15
		return isset( $this->prefixalUnitPreference[$unit] ) && $this->prefixalUnitPreference[$unit];
345
	}
346
347
	/**
348
	 * Create links to mapping services based on a wiki-editable message.
349
	 * The parameters available to the message are:
350
	 * $1: string of numerical value in English punctuation
351
	 * $2: string of integer version of value, in English punctuation
352
	 *
353
	 * @return array
354
	 */
355
	protected function getServiceLinkParams() {
356
		if ( $this->isValid() ) {
357
			return array( strval( $this->m_dataitem->getNumber() ), strval( round( $this->m_dataitem->getNumber() ) ) );
358
		} else {
359
			return array();
360
		}
361
	}
362
363
	/**
364
	 * Transform a (typically unit-) string into a normalised form,
365
	 * so that, e.g., "km²" and "km<sup>2</sup>" do not need to be
366
	 * distinguished.
367
	 */
368 43
	public function normalizeUnit( $unit ) {
369 43
		$unit = str_replace( array( '[[', ']]' ), '', trim( $unit ) ); // allow simple links to be used inside annotations
370 43
		$unit = str_replace( array( '²', '<sup>2</sup>' ), '&sup2;', $unit );
371 43
		$unit = str_replace( array( '³', '<sup>3</sup>' ), '&sup3;', $unit );
372 43
		return smwfXMLContentEncode( $unit );
373
	}
374
375
	/**
376
	 * Compute the value based on the given input number and unit string.
377
	 * If the unit is not supported, return false, otherwise return true.
378
	 * This is called when parsing user input, where the given unit value
379
	 * has already been normalized.
380
	 *
381
	 * This class does not support any (non-empty) units, but subclasses
382
	 * may overwrite this behavior.
383
	 * @param $number float value obtained by parsing user input
384
	 * @param $unit string after the numericla user input
385
	 * @return boolean specifying if the unit string is allowed
386
	 */
387 33
	protected function convertToMainUnit( $number, $unit ) {
388 33
		$this->m_dataitem = new SMWDINumber( $number );
389 33
		$this->m_unitin = '';
390 33
		return ( $unit === '' );
391
	}
392
393
	/**
394
	 * This method creates an array of unit-value-pairs that should be
395
	 * printed. Units are the keys and should be canonical unit IDs.
396
	 * The result is stored in $this->m_unitvalues. Again, any class that
397
	 * requires effort for doing this should first check whether the array
398
	 * is already set (i.e. not false) before doing any work.
399
	 * Note that the values should be plain numbers. Output formatting is done
400
	 * later when needed.  Also, it should be checked if the value is valid
401
	 * before trying to calculate with its contents.
402
	 * This method also must call or implement convertToMainUnit().
403
	 *
404
	 * Overwritten by subclasses that support units.
405
	 */
406 20
	protected function makeConversionValues() {
407 20
		$this->m_unitvalues = array( '' => $this->m_dataitem->getNumber() );
408 20
	}
409
410
	/**
411
	 * This method is used when no user input was given to find the best
412
	 * values for m_unitin and m_caption. After conversion,
413
	 * these fields will look as if they were generated from user input,
414
	 * and convertToMainUnit() will have been called (if not, it would be
415
	 * blocked by the presence of m_unitin).
416
	 *
417
	 * Overwritten by subclasses that support units.
418
	 */
419 18
	protected function makeUserValue() {
420 18
		$this->m_caption = '';
421
422 18
		$number = $this->m_dataitem->getNumber();
423
424
		// -u is the format for displaying the unit only
425 18
		if ( $this->m_outformat == '-u' ) {
426
			$this->m_caption = '';
427 18
		} elseif ( ( $this->m_outformat != '-' ) && ( $this->m_outformat != '-n' ) ) {
428 18
			$this->m_caption = $this->getLocalizedFormattedNumber( $number );
429
		} else {
430 2
			$this->m_caption = $this->getNormalizedFormattedNumber( $number );
431
		}
432
433
		// no unit ever, so nothing to do about this
434 18
		$this->m_unitin = '';
435 18
	}
436
437
	/**
438
	 * Return an array of major unit strings (ids only recommended) supported by
439
	 * this datavalue.
440
	 *
441
	 * Overwritten by subclasses that support units.
442
	 */
443
	public function getUnitList() {
444
		return array( '' );
445
	}
446
447 31
	protected function getPreferredDisplayPrecision() {
448
449
		// In case of a value description, don't restrict the value with a display precision
450 31
		if ( $this->getProperty() === null || $this->getOptionValueFor( 'value.description' ) || $this->getOptionValueFor( 'no.displayprecision' ) ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getOptionValueFor('value.description') of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
Bug Best Practice introduced by
The expression $this->getOptionValueFor('no.displayprecision') of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
451 4
			return false;
452
		}
453
454 31
		if ( $this->precision === null ) {
455 31
			$this->precision = $this->getPropertySpecificationLookup()->getDisplayPrecisionFor(
456 31
				$this->getProperty()
457
			);
458
		}
459
460 31
		return $this->precision;
461
	}
462
463 6
	private function findPrecisionFrom( $formatstring ) {
464
465 6
		if ( strpos( $formatstring, '-' ) === false ) {
466 4
			return $formatstring;
467
		}
468
469 4
		$parts = explode( '-', $formatstring );
470
471
		// Find precision from annotated -p<number of digits> formatstring which
472
		// has priority over a possible _PREC value
473 4
		foreach ( $parts as $key => $value ) {
474 4
			if ( strpos( $value, 'p' ) !== false && is_numeric( substr( $value, 1 ) ) ) {
475 1
				$this->precision = strval( substr( $value, 1 ) );
0 ignored issues
show
Documentation Bug introduced by
It seems like strval(substr($value, 1)) of type string is incompatible with the declared type integer|null of property $precision.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
476 4
				unset( $parts[$key] );
477
			}
478
		}
479
480
		// Rebuild formatstring without a possible p element to ensure other
481
		// options can be used in combination such as -n-p2 etc.
482 4
		return implode( '-', $parts );
483
	}
484
485 45
	private function getNumberFormatter() {
486
487 45
		$this->intlNumberFormatter->setOption(
488 45
			'user.language',
489 45
			$this->getOptionValueFor( 'user.language' )
490
		);
491
492 45
		$this->intlNumberFormatter->setOption(
493 45
			'content.language',
494 45
			$this->getOptionValueFor( 'content.language' )
495
		);
496
497 45
		$this->intlNumberFormatter->setOption(
498 45
			'separator.thousands',
499 45
			$this->getOptionValueFor( 'separator.thousands' )
500
		);
501
502 45
		$this->intlNumberFormatter->setOption(
503 45
			'separator.decimal',
504 45
			$this->getOptionValueFor( 'separator.decimal' )
505
		);
506
507 45
		return $this->intlNumberFormatter;
508
	}
509
510 6
	private function findPreferredLanguageFrom( &$formatstring ) {
511
		// Localized preferred user language
512 6
		if ( strpos( $formatstring, 'LOCL' ) !== false && ( $languageCode = Localizer::getLanguageCodeFrom( $formatstring ) ) !== false ) {
513 1
			$this->intlNumberFormatter->setOption(
514 1
				'preferred.language',
515
				$languageCode
516
			);
517
		}
518
519
		// Remove any remaining
520 6
		$formatstring = str_replace( array( '#LOCL', 'LOCL' ), '', $formatstring );
521 6
	}
522
523
}
524