Completed
Push — master ( d2d28e...1c2760 )
by mw
35:37
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
use SMW\ApplicationFactory;
8
9
/**
10
 * @ingroup SMWDataValues
11
 */
12
13
/**
14
 * This datavalue implements numerical datavalues, and supports optional
15
 * unit conversions. It parses and manages unit strings, since even plain
16
 * numbers may have (not further specified) units that are stored. However,
17
 * only subclasses implement full unit conversion by extending the methods
18
 * convertToMainUnit() and makeConversionValues().
19
 *
20
 * Units work as follows: a unit is a string, but many such strings might
21
 * refer to the same unit of measurement. There is always one string, that
22
 * canonically represents the unit, and we will call this version of writing
23
 * the unit the /unit id/. IDs for units are needed for tasks like duplicate
24
 * avoidance. If no conversion information is given, any unit is its own ID.
25
 * In any case, units are /normalised/, i.e. given a more standardised meaning
26
 * before being processed. All units, IDs or otherwise, should be suitable for
27
 * printout in wikitext, and main IDs should moreover be suitable for printout
28
 * in HTML.
29
 *
30
 * Subclasses that support unit conversion may interpret the output format set
31
 * via setOutputFormat() to allow a unit to be selected for display. Note that
32
 * this setting does not affect the internal representation of the value
33
 * though. So chosing a specific output format will change the behavior of
34
 * output functions like getLongWikiText(), but not of functions that access
35
 * the value itself, such as getUnit() or getDBKeys().
36
 *
37
 * @author Markus Krötzsch
38
 * @ingroup SMWDataValues
39
 *
40
 * @todo Wiki-HTML-conversion for unit strings must be revisited, as the current
41
 * solution might be unsafe.
42
 */
43
class SMWNumberValue extends SMWDataValue {
44
45
	/**
46
	 * Array with entries unit=>value, mapping a normalized unit to the
47
	 * converted value. Used for conversion tooltips.
48
	 * @var array
49
	 */
50
	protected $m_unitvalues;
51
52
	/**
53
	 * Whether the unit is preferred as prefix or not
54
	 *
55
	 * @var array
56
	 */
57
	protected $prefixalUnitPreference = array();
58
59
	/**
60
	 * Canonical identifier for the unit that the user gave as input. Used
61
	 * to avoid printing this in conversion tooltips again. If the
62
	 * outputformat was set to show another unit, then the values of
63
	 * $m_caption and $m_unitin will be updated as if the formatted string
64
	 * had been the original user input, i.e. the two values reflect what
65
	 * is currently printed.
66
	 * @var string
67
	 */
68
	protected $m_unitin;
69
70
	/**
71
	 * @var integer|null
72
	 */
73
	protected $precision = null;
74
75
	/**
76
	 * @var IntlNumberFormatter
77
	 */
78
	private $intlNumberFormatter = null;
79
80
	/**
81
	 * @since 2.4
82
	 *
83
	 * @param string $typeid
84
	 */
85 52
	public function __construct( $typeid = '' ) {
86 52
		parent::__construct( $typeid );
87 52
		$this->intlNumberFormatter = IntlNumberFormatter::getInstance();
88 52
		$this->intlNumberFormatter->reset();
89 52
	}
90
91
	/**
92
	 * Parse a string of the form "number unit" where unit is optional. The
93
	 * results are stored in the $number and $unit parameters. Returns an
94
	 * error code.
95
	 * @param $value string to parse
96
	 * @param $number call-by-ref parameter that will be set to the numerical value
97
	 * @param $unit call-by-ref parameter that will be set to the "unit" string (after the number)
98
	 * @return integer 0 (no errors), 1 (no number found at all), 2 (number
99
	 * too large for this platform)
100
	 */
101 50
	public function parseNumberValue( $value, &$number, &$unit, &$asPrefix = false ) {
102
103 50
		$intlNumberFormatter = $this->getNumberFormatter();
104
105
		// Parse to find $number and (possibly) $unit
106 50
		$kiloseparator = $intlNumberFormatter->getSeparatorByLanguage(
107 50
			IntlNumberFormatter::THOUSANDS_SEPARATOR,
108 50
			IntlNumberFormatter::CONTENT_LANGUAGE
109
		);
110
111 50
		$decseparator = $intlNumberFormatter->getSeparatorByLanguage(
112 50
			IntlNumberFormatter::DECIMAL_SEPARATOR,
113 50
			IntlNumberFormatter::CONTENT_LANGUAGE
114
		);
115
116
		// #753
117
		$regex = '/([-+]?\s*(?:' .
118
				// Either numbers like 10,000.99 that start with a digit
119 50
				'\d+(?:\\' . $kiloseparator . '\d\d\d)*(?:\\' . $decseparator . '\d+)?' .
120
				// or numbers like .001 that start with the decimal separator
121 50
				'|\\' . $decseparator . '\d+' .
122 50
				')\s*(?:[eE][-+]?\d+)?)/u';
123
124
		// #1718 Whether to preserve spaces in unit labels or not (e.g. sq mi, sqmi)
125 50
		$space = $this->isEnabledFeature( SMW_DV_NUMV_USPACE ) ? ' ' : '';
126
127 50
		$parts = preg_split(
128
			$regex,
129 50
			trim( str_replace( array( '&nbsp;', '&#160;', '&thinsp;', ' ' ), $space, $value ) ),
130 50
			2,
131 50
			PREG_SPLIT_DELIM_CAPTURE
132
		);
133
134 50
		if ( count( $parts ) >= 2 ) {
135 50
			$numstring = str_replace( $kiloseparator, '', preg_replace( '/\s*/u', '', $parts[1] ) ); // simplify
136 50
			if ( $decseparator != '.' ) {
137 3
				$numstring = str_replace( $decseparator, '.', $numstring );
138
			}
139 50
			list( $number ) = sscanf( $numstring, "%f" );
140 50
			if ( count( $parts ) >= 3  ) {
141 50
				$asPrefix = $parts[0] !== '';
142 50
				$unit = $this->normalizeUnit( $parts[0] !== '' ? $parts[0] : $parts[2] );
143
			}
144
		}
145
146 50
		if ( ( count( $parts ) == 1 ) || ( $numstring === '' ) ) { // no number found
0 ignored issues
show
The variable $numstring does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
147 1
			return 1;
148 50
		} elseif ( is_infinite( $number ) ) { // number is too large for this platform
149
			return 2;
150
		} else {
151 50
			return 0;
152
		}
153
	}
154
155
	/**
156
	 * @see DataValue::parseUserValue
157
	 */
158 51
	protected function parseUserValue( $value ) {
159
		// Set caption
160 51
		if ( $this->m_caption === false ) {
161 51
			$this->m_caption = $value;
162
		}
163
164 51
		if ( $value !== '' && $value{0} === ':' ) {
165 1
			$this->addErrorMsg( array( 'smw-datavalue-invalid-number', $value ) );
166 1
			return;
167
		}
168
169 50
		$this->m_unitin = false;
170 50
		$this->m_unitvalues = false;
171 50
		$number = $unit = '';
172 50
		$error = $this->parseNumberValue( $value, $number, $unit );
173
174 50
		if ( $error == 1 ) { // no number found
175 1
			$this->addErrorMsg( array( 'smw_nofloat', $value ) );
176 50
		} elseif ( $error == 2 ) { // number is too large for this platform
177
			$this->addErrorMsg( array( 'smw_infinite', $value ) );
178 50
		} elseif ( $this->getTypeID() === '_num' && $unit !== '' ) {
179 1
			$this->addErrorMsg( array( 'smw-datavalue-number-textnotallowed', $unit, $number ) );
180 50
		} elseif ( $number === null ) {
181
			$this->addErrorMsg( array( 'smw-datavalue-number-nullnotallowed', $value ) ); // #1628
182 50
		} elseif ( $this->convertToMainUnit( $number, $unit ) === false ) { // so far so good: now convert unit and check if it is allowed
183 9
			$this->addErrorMsg( array( 'smw_unitnotallowed', $unit ) );
184
		} // note that convertToMainUnit() also sets m_dataitem if valid
185 50
	}
186
187
	/**
188
	 * @see SMWDataValue::loadDataItem()
189
	 * @param $dataitem SMWDataItem
190
	 * @return boolean
191
	 */
192 31
	protected function loadDataItem( SMWDataItem $dataItem ) {
193
194 31
		if ( $dataItem->getDIType() !== SMWDataItem::TYPE_NUMBER ) {
195
			return false;
196
		}
197
198 31
		$this->m_dataitem = $dataItem;
199 31
		$this->m_caption = false;
200 31
		$this->m_unitin = false;
201 31
		$this->makeUserValue();
202 31
		$this->m_unitvalues = false;
203
204 31
		return true;
205
	}
206
207
	/**
208
	 * @see DataValue::setOutputFormat
209
	 *
210
	 * @param $string $formatstring
211
	 */
212 8
	public function setOutputFormat( $formatstring ) {
213
214 8
		if ( $formatstring == $this->m_outformat ) {
215
			return null;
216
		}
217
218
		// #1591
219 8
		$this->findPreferredLanguageFrom( $formatstring );
220
221
		// #1335
222 8
		$this->m_outformat = $this->findPrecisionFrom( $formatstring );
223
224 8
		if ( $this->isValid() ) { // update caption/unitin for this format
225 8
			$this->m_caption = false;
226 8
			$this->m_unitin = false;
227 8
			$this->makeUserValue();
228
		}
229 8
	}
230
231
	/**
232
	 * @since 2.4
233
	 *
234
	 * @return float
235
	 */
236 38
	public function getLocalizedFormattedNumber( $value ) {
237 38
		return $this->getNumberFormatter()->format( $value, $this->getPreferredDisplayPrecision() );
238
	}
239
240
	/**
241
	 * @since 2.4
242
	 *
243
	 * @return float
244
	 */
245 18
	public function getNormalizedFormattedNumber( $value ) {
246 18
		return $this->getNumberFormatter()->format( $value, $this->getPreferredDisplayPrecision(), IntlNumberFormatter::VALUE_FORMAT );
247
	}
248
249
	/**
250
	 * @see DataValue::getShortWikiText
251
	 *
252
	 * @return string
253
	 */
254 36
	public function getShortWikiText( $linker = null ) {
255 36
		return $this->getDataValueFormatter()->format( DataValueFormatter::WIKI_SHORT, $linker );
256
	}
257
258
	/**
259
	 * @see DataValue::getShortHTMLText
260
	 *
261
	 * @return string
262
	 */
263 1
	public function getShortHTMLText( $linker = null ) {
264 1
		return $this->getDataValueFormatter()->format( DataValueFormatter::HTML_SHORT, $linker );
265
	}
266
267
	/**
268
	 * @see DataValue::getLongWikiText
269
	 *
270
	 * @return string
271
	 */
272
	public function getLongWikiText( $linker = null ) {
273
		return $this->getDataValueFormatter()->format( DataValueFormatter::WIKI_LONG, $linker );
274
	}
275
276
	/**
277
	 * @see DataValue::getLongHTMLText
278
	 *
279
	 * @return string
280
	 */
281 2
	public function getLongHTMLText( $linker = null ) {
282 2
		return $this->getDataValueFormatter()->format( DataValueFormatter::HTML_LONG, $linker );
283
	}
284
285 22
	public function getNumber() {
286 22
		return $this->isValid() ? $this->m_dataitem->getNumber() : 32202;
287
	}
288
289 15
	public function getWikiValue() {
290 15
		return $this->getDataValueFormatter()->format( DataValueFormatter::VALUE );
291
	}
292
293
	/**
294
	 * @see DataVelue::getInfolinks
295
	 *
296
	 * @return array
297
	 */
298 1
	public function getInfolinks() {
299
300
		// When generating an infoLink, use the normalized value without any
301
		// precision limitation
302 1
		$this->setOption( 'no.displayprecision', true );
303 1
		$this->setOption( 'content.language', Message::CONTENT_LANGUAGE );
304 1
		$infoLinks = parent::getInfolinks();
305 1
		$this->setOption( 'no.displayprecision', false );
306
307 1
		return $infoLinks;
308
	}
309
310
	/**
311
	 * @since 2.4
312
	 *
313
	 * @return string
314
	 */
315 34
	public function getCanonicalMainUnit() {
316 34
		return $this->m_unitin;
317
	}
318
319
	/**
320
	 * Returns array of converted unit-value-pairs that can be
321
	 * printed.
322
	 *
323
	 * @since 2.4
324
	 *
325
	 * @return array
326
	 */
327 34
	public function getConvertedUnitValues() {
328 34
		$this->makeConversionValues();
329 34
		return $this->m_unitvalues;
330
	}
331
332
	/**
333
	 * Return the unit in which the returned value is to be interpreted.
334
	 * This string is a plain UTF-8 string without wiki or html markup.
335
	 * The returned value is a canonical ID for the main unit.
336
	 * Returns the empty string if no unit is given for the value.
337
	 * Overwritten by subclasses that support units.
338
	 */
339 11
	public function getUnit() {
340 11
		return '';
341
	}
342
343
	/**
344
	 * @since 2.4
345
	 *
346
	 * @param string $unit
347
	 *
348
	 * @return boolean
349
	 */
350 17
	public function hasPrefixalUnitPreference( $unit ) {
351 17
		return isset( $this->prefixalUnitPreference[$unit] ) && $this->prefixalUnitPreference[$unit];
352
	}
353
354
	/**
355
	 * Create links to mapping services based on a wiki-editable message.
356
	 * The parameters available to the message are:
357
	 * $1: string of numerical value in English punctuation
358
	 * $2: string of integer version of value, in English punctuation
359
	 *
360
	 * @return array
361
	 */
362 1
	protected function getServiceLinkParams() {
363 1
		if ( $this->isValid() ) {
364 1
			return array( strval( $this->m_dataitem->getNumber() ), strval( round( $this->m_dataitem->getNumber() ) ) );
365
		} else {
366
			return array();
367
		}
368
	}
369
370
	/**
371
	 * Transform a (typically unit-) string into a normalised form,
372
	 * so that, e.g., "km²" and "km<sup>2</sup>" do not need to be
373
	 * distinguished.
374
	 */
375 50
	public function normalizeUnit( $unit ) {
376 50
		$unit = str_replace( array( '[[', ']]' ), '', trim( $unit ) ); // allow simple links to be used inside annotations
377 50
		$unit = str_replace( array( '²', '<sup>2</sup>' ), '&sup2;', $unit );
378 50
		$unit = str_replace( array( '³', '<sup>3</sup>' ), '&sup3;', $unit );
379 50
		return smwfXMLContentEncode( $unit );
380
	}
381
382
	/**
383
	 * Compute the value based on the given input number and unit string.
384
	 * If the unit is not supported, return false, otherwise return true.
385
	 * This is called when parsing user input, where the given unit value
386
	 * has already been normalized.
387
	 *
388
	 * This class does not support any (non-empty) units, but subclasses
389
	 * may overwrite this behavior.
390
	 * @param $number float value obtained by parsing user input
391
	 * @param $unit string after the numericla user input
392
	 * @return boolean specifying if the unit string is allowed
393
	 */
394 39
	protected function convertToMainUnit( $number, $unit ) {
395 39
		$this->m_dataitem = new SMWDINumber( $number );
396 39
		$this->m_unitin = '';
397 39
		return ( $unit === '' );
398
	}
399
400
	/**
401
	 * This method creates an array of unit-value-pairs that should be
402
	 * printed. Units are the keys and should be canonical unit IDs.
403
	 * The result is stored in $this->m_unitvalues. Again, any class that
404
	 * requires effort for doing this should first check whether the array
405
	 * is already set (i.e. not false) before doing any work.
406
	 * Note that the values should be plain numbers. Output formatting is done
407
	 * later when needed.  Also, it should be checked if the value is valid
408
	 * before trying to calculate with its contents.
409
	 * This method also must call or implement convertToMainUnit().
410
	 *
411
	 * Overwritten by subclasses that support units.
412
	 */
413 26
	protected function makeConversionValues() {
414 26
		$this->m_unitvalues = array( '' => $this->m_dataitem->getNumber() );
415 26
	}
416
417
	/**
418
	 * This method is used when no user input was given to find the best
419
	 * values for m_unitin and m_caption. After conversion,
420
	 * these fields will look as if they were generated from user input,
421
	 * and convertToMainUnit() will have been called (if not, it would be
422
	 * blocked by the presence of m_unitin).
423
	 *
424
	 * Overwritten by subclasses that support units.
425
	 */
426 24
	protected function makeUserValue() {
427 24
		$this->m_caption = '';
428
429 24
		$number = $this->m_dataitem->getNumber();
430
431
		// -u is the format for displaying the unit only
432 24
		if ( $this->m_outformat == '-u' ) {
433
			$this->m_caption = '';
434 24
		} elseif ( ( $this->m_outformat != '-' ) && ( $this->m_outformat != '-n' ) ) {
435 24
			$this->m_caption = $this->getLocalizedFormattedNumber( $number );
436
		} else {
437 2
			$this->m_caption = $this->getNormalizedFormattedNumber( $number );
438
		}
439
440
		// no unit ever, so nothing to do about this
441 24
		$this->m_unitin = '';
442 24
	}
443
444
	/**
445
	 * Return an array of major unit strings (ids only recommended) supported by
446
	 * this datavalue.
447
	 *
448
	 * Overwritten by subclasses that support units.
449
	 */
450
	public function getUnitList() {
451
		return array( '' );
452
	}
453
454 38
	protected function getPreferredDisplayPrecision() {
455
456
		// In case of a value description, don't restrict the value with a display precision
457 38
		if ( $this->getProperty() === null || $this->getOptionBy( 'value.description' ) || $this->getOptionBy( 'no.displayprecision' ) ) {
458 10
			return false;
459
		}
460
461 38
		if ( $this->precision === null ) {
462 38
			$this->precision = ApplicationFactory::getInstance()->getPropertySpecificationLookup()->getDisplayPrecisionBy(
463 38
				$this->getProperty()
464
			);
465
		}
466
467 38
		return $this->precision;
468
	}
469
470 8
	private function findPrecisionFrom( $formatstring ) {
471
472 8
		if ( strpos( $formatstring, '-' ) === false ) {
473 5
			return $formatstring;
474
		}
475
476 5
		$parts = explode( '-', $formatstring );
477
478
		// Find precision from annotated -p<number of digits> formatstring which
479
		// has priority over a possible _PREC value
480 5
		foreach ( $parts as $key => $value ) {
481 5
			if ( strpos( $value, 'p' ) !== false && is_numeric( substr( $value, 1 ) ) ) {
482 2
				$this->precision = strval( substr( $value, 1 ) );
483 5
				unset( $parts[$key] );
484
			}
485
		}
486
487
		// Rebuild formatstring without a possible p element to ensure other
488
		// options can be used in combination such as -n-p2 etc.
489 5
		return implode( '-', $parts );
490
	}
491
492 51
	private function getNumberFormatter() {
493
494 51
		$this->intlNumberFormatter->setOption(
495 51
			'user.language',
496 51
			$this->getOptionBy( 'user.language' )
497
		);
498
499 51
		$this->intlNumberFormatter->setOption(
500 51
			'content.language',
501 51
			$this->getOptionBy( 'content.language' )
502
		);
503
504 51
		$this->intlNumberFormatter->setOption(
505 51
			'separator.thousands',
506 51
			$this->getOptionBy( 'separator.thousands' )
507
		);
508
509 51
		$this->intlNumberFormatter->setOption(
510 51
			'separator.decimal',
511 51
			$this->getOptionBy( 'separator.decimal' )
512
		);
513
514 51
		return $this->intlNumberFormatter;
515
	}
516
517 8
	private function findPreferredLanguageFrom( &$formatstring ) {
518
		// Localized preferred user language
519 8
		if ( strpos( $formatstring, 'LOCL' ) !== false && ( $languageCode = Localizer::getLanguageCodeFrom( $formatstring ) ) !== false ) {
520 1
			$this->intlNumberFormatter->setOption(
521 1
				'preferred.language',
522
				$languageCode
523
			);
524
		}
525
526
		// Remove any remaining
527 8
		$formatstring = str_replace( array( '#LOCL', 'LOCL' ), '', $formatstring );
528 8
	}
529
530
}
531