DecimalParser   A
last analyzed

Complexity

Total Complexity 12

Size/Duplication

Total Lines 161
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 12
lcom 1
cbo 5
dl 0
loc 161
ccs 38
cts 38
cp 1
rs 10
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 2
A getMath() 0 7 2
A splitDecimalExponent() 0 8 2
A applyDecimalExponent() 0 8 2
A stringParse() 0 23 3
A normalizeDecimal() 0 21 1
1
<?php
2
3
namespace ValueParsers;
4
5
use DataValues\DecimalMath;
6
use DataValues\DecimalValue;
7
use DataValues\IllegalValueException;
8
9
/**
10
 * ValueParser that parses the string representation of a decimal number.
11
 *
12
 * @since 0.1
13
 *
14
 * @license GPL-2.0+
15
 * @author Daniel Kinzler
16
 */
17
class DecimalParser extends StringValueParser {
18
19
	const FORMAT_NAME = 'decimal';
20
21
	/**
22
	 * @var DecimalMath
23
	 */
24
	private $math;
25
26
	/**
27
	 * @var null|NumberUnlocalizer
28
	 */
29
	private $unlocalizer;
30
31
	/**
32
	 * @since 0.1
33
	 *
34
	 * @param ParserOptions|null $options
35
	 * @param NumberUnlocalizer|null $unlocalizer
36
	 */
37 62
	public function __construct( ParserOptions $options = null, NumberUnlocalizer $unlocalizer = null ) {
38 62
		parent::__construct( $options );
39
40 62
		$this->unlocalizer = $unlocalizer ?: new BasicNumberUnlocalizer();
41 62
	}
42
43
	/**
44
	 * @return DecimalMath
45
	 */
46 8
	private function getMath() {
47 8
		if ( $this->math === null ) {
48 8
			$this->math = new DecimalMath();
49
		}
50
51 8
		return $this->math;
52
	}
53
54
	/**
55
	 * Splits the exponent from the scientific notation of a decimal number.
56
	 *
57
	 * @since 0.5
58
	 *
59
	 * @example splitDecimalExponent( '1.2' ) is [ '1.2', 0 ]
60
	 * @example splitDecimalExponent( '1.2e3' ) is [ '1.2', 3 ]
61
	 * @example splitDecimalExponent( '1.2e-2' ) is [ '1.2', -2 ]
62
	 *
63
	 * @param string $valueString A decimal string, possibly using scientific notation.
64
	 *
65
	 * @return array list( $decimal, $exponent ) A pair of the decimal value without the
66
	 *         decimal exponent, and the decimal exponent as an integer. If $valueString
67
	 *         does not use scientific notation, $exponent will be 0.
68
	 */
69 52
	public function splitDecimalExponent( $valueString ) {
70 52
		if ( preg_match( '/^(.*)(?:[eE]|x10\^)([-+]?[\d,]+)$/', $valueString, $matches ) ) {
71 17
			$exponent = (int)str_replace( ',', '', $matches[2] );
72 17
			return [ $matches[1], $exponent ];
73
		}
74
75 35
		return [ $valueString, 0 ];
76
	}
77
78
	/**
79
	 * Applies a decimal exponent, by shifting the decimal point in the decimal string
80
	 * representation of the value.
81
	 *
82
	 * @since 0.5
83
	 *
84
	 * @example applyDecimalExponent( new DecimalValue( '1.2' ), 0 )  is  new DecimalValue( '1.2' )
85
	 * @example applyDecimalExponent( new DecimalValue( '1.2' ), 3 )  is  new DecimalValue( '1200' )
86
	 * @example applyDecimalExponent( new DecimalValue( '1.2' ), -2 )  is  new DecimalValue( '0.012' )
87
	 *
88
	 * @param DecimalValue $decimal
89
	 * @param int $exponent
90
	 *
91
	 * @return DecimalValue
92
	 */
93 34
	public function applyDecimalExponent( DecimalValue $decimal, $exponent ) {
94 34
		if ( $exponent !== 0 ) {
95 8
			$math = $this->getMath();
96 8
			$decimal = $math->shift( $decimal, $exponent );
97
		}
98
99 34
		return $decimal;
100
	}
101
102
	/**
103
	 * Creates a DecimalValue from a given string.
104
	 *
105
	 * The decimal notation for the value is based on ISO 31-0, with some modifications:
106
	 * - the decimal separator is '.' (period). Comma is not used anywhere.
107
	 * - leading and trailing as well as any internal whitespace is ignored
108
	 * - the following characters are ignored: comma (","), apostrophe ("'").
109
	 * - scientific (exponential) notation is supported using the pattern /e[-+]\d+/
110
	 * - the number may start (or end) with a decimal point.
111
	 * - leading zeroes are stripped, except directly before the decimal point
112
	 * - trailing zeroes are stripped, except directly after the decimal point
113
	 * - zero is always positive.
114
	 *
115
	 * @see StringValueParser::stringParse
116
	 *
117
	 * @since 0.1
118
	 *
119
	 * @param string $value
120
	 *
121
	 * @return DecimalValue
122
	 * @throws ParseException
123
	 */
124 40
	protected function stringParse( $value ) {
125 40
		$rawValue = $value;
126
127 40
		$value = $this->unlocalizer->unlocalizeNumber( $value );
128
129
		//handle scientific notation
130 40
		list( $value, $exponent ) = $this->splitDecimalExponent( $value );
131
132 40
		$value = $this->normalizeDecimal( $value );
133
134 40
		if ( $value === '' ) {
135 1
			throw new ParseException( 'Decimal value must not be empty', $rawValue, self::FORMAT_NAME );
136
		}
137
138
		try {
139 39
			$decimal = new DecimalValue( $value );
140 31
			$decimal = $this->applyDecimalExponent( $decimal, $exponent );
141
142 31
			return $decimal;
143 8
		} catch ( IllegalValueException $ex ) {
144 8
			throw new ParseException( $ex->getMessage(), $rawValue, self::FORMAT_NAME );
145
		}
146
	}
147
148
	/**
149
	 * Normalize a decimal string.
150
	 *
151
	 * @param string $number
152
	 *
153
	 * @return string
154
	 */
155 40
	private function normalizeDecimal( $number ) {
156
		// strip fluff
157 40
		$number = preg_replace( '/[\s\'_,`]+/u', '', $number );
158
159
		// strip leading zeros
160 40
		$number = preg_replace( '/^([-+]?)0+([^0]|0$)/', '$1$2', $number );
161
162
		// fix leading decimal point
163 40
		$number = preg_replace( '/^([-+]?)\./', '${1}0.', $number );
164
165
		// strip trailing decimal point
166 40
		$number = preg_replace( '/\.$/', '', $number );
167
168
		// add leading sign
169 40
		$number = preg_replace( '/^(?=\d)/', '+', $number );
170
171
		// make "negative" zero positive
172 40
		$number = preg_replace( '/^-(0+(\.0+)?)$/', '+$1', $number );
173
174 40
		return $number;
175
	}
176
177
}
178