Completed
Push — master ( 7205b8...d749c2 )
by mw
39:58 queued 05:05
created

IntlNumberFormatter::doFormatWithPrecision()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 30
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 5
eloc 15
nc 4
nop 4
dl 0
loc 30
ccs 13
cts 13
cp 1
crap 5
rs 8.439
c 2
b 0
f 1
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() {
0 ignored issues
show
Coding Style introduced by
getInstance uses the super-global variable $GLOBALS which is generally not recommended.

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:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
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 ) {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of $type (integer) and self::DECIMAL_SEPARATOR (string) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
121 67
			return $this->getPreferredLocalizedSeparator( 'separator.decimal', 'smw_decseparator', $language );
122
		}
123
124 56
		if ( $type === self::THOUSANDS_SEPARATOR ) {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of $type (integer) and self::THOUSANDS_SEPARATOR (string) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
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 );
0 ignored issues
show
Bug introduced by
It seems like $precision defined by parameter $precision on line 145 can also be of type integer; however, SMW\IntlNumberFormatter:...edNumberWithPrecision() does only seem to accept boolean, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
149
		}
150
151 37
		if ( $precision !== false || $format === self::DEFAULT_FORMAT ) {
152 4
			return $this->getDefaultFormattedNumberWithPrecision( $value, $precision );
0 ignored issues
show
Bug introduced by
It seems like $precision defined by parameter $precision on line 145 can also be of type integer; however, SMW\IntlNumberFormatter:...edNumberWithPrecision() does only seem to accept boolean, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
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 ) {
0 ignored issues
show
Unused Code introduced by
The parameter $precision is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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,
0 ignored issues
show
Documentation introduced by
$precision is of type integer, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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,
0 ignored issues
show
Bug introduced by
It seems like $precision defined by $precision === false ? $...om($value) : $precision on line 253 can also be of type integer; however, SMW\IntlNumberFormatter::doFormatWithPrecision() does only seem to accept boolean, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
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)
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

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