Completed
Push — master ( 9eae06...a41cb5 )
by mw
16s
created

includes/datavalues/SMW_DV_Number.php (1 issue)

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\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( '&nbsp;', '&#160;', '&thinsp;', ' ' ), '', $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>' ), '&sup2;', $unit );
374 47
		$unit = str_replace( array( '³', '<sup>3</sup>' ), '&sup3;', $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
Deprecated Code introduced by
The method SMW\Localizer::getLanguageCodeFrom() has been deprecated with message: 2.5, use Localizer::getAnnotatedLanguageCodeFrom instead

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.

Loading history...
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