Test Failed
Push — nullBoundsParser ( e27987 )
by no
07:57 queued 04:39
created

QuantityParser::newQuantityFromParts()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 25
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
c 4
b 0
f 0
dl 0
loc 25
rs 8.5806
cc 4
eloc 16
nc 4
nop 4
1
<?php
2
3
namespace ValueParsers;
4
5
use DataValues\DecimalMath;
6
use DataValues\DecimalValue;
7
use DataValues\IllegalValueException;
8
use DataValues\QuantityValue;
9
use InvalidArgumentException;
10
11
/**
12
 * ValueParser that parses the string representation of a quantity.
13
 *
14
 * @since 0.1
15
 *
16
 * @license GPL-2.0+
17
 * @author Daniel Kinzler
18
 */
19
class QuantityParser extends StringValueParser {
20
21
	const FORMAT_NAME = 'quantity';
22
23
	/**
24
	 * The unit of the value to parse. If this option is given, it's illegal to also specify
25
	 * a unit in the input string.
26
	 *
27
	 * @since 0.5
28
	 */
29
	const OPT_UNIT = 'unit';
30
31
	/**
32
	 * @var DecimalParser
33
	 */
34
	private $decimalParser;
35
36
	/**
37
	 * @var NumberUnlocalizer
38
	 */
39
	private $unlocalizer;
40
41
	/**
42
	 * @since 0.1
43
	 *
44
	 * @param ParserOptions|null $options
45
	 * @param NumberUnlocalizer|null $unlocalizer
46
	 */
47
	public function __construct( ParserOptions $options = null, NumberUnlocalizer $unlocalizer = null ) {
48
		parent::__construct( $options );
49
50
		$this->defaultOption( self::OPT_UNIT, null );
51
52
		$this->unlocalizer = $unlocalizer ?: new BasicNumberUnlocalizer();
53
		$this->decimalParser = new DecimalParser( $this->options, $this->unlocalizer );
54
	}
55
56
	/**
57
	 * @see StringValueParser::stringParse
58
	 *
59
	 * @since 0.1
60
	 *
61
	 * @param string $value
62
	 *
63
	 * @return QuantityValue
64
	 * @throws ParseException
65
	 */
66
	protected function stringParse( $value ) {
67
		list( $amount, $exactness, $margin, $unit ) = $this->splitQuantityString( $value );
68
69
		$unitOption = $this->getUnitFromOptions();
70
71
		if ( $unit === null ) {
72
			$unit = $unitOption !== null ? $unitOption : '1';
73
		} elseif ( $unitOption !== null && $unit !== $unitOption ) {
74
			throw new ParseException( 'Cannot specify a unit in input if a unit was fixed via options.' );
75
		}
76
77
		try {
78
			$quantity = $this->newQuantityFromParts( $amount, $exactness, $margin, $unit );
79
			return $quantity;
80
		} catch ( IllegalValueException $ex ) {
81
			throw new ParseException( $ex->getMessage(), $value, self::FORMAT_NAME );
82
		}
83
	}
84
85
	/**
86
	 * @return string|null
87
	 */
88
	private function getUnitFromOptions() {
89
		$unit = $this->getOption( self::OPT_UNIT );
90
		return $unit === null ? null : trim( $unit );
91
	}
92
93
	/**
94
	 * Constructs a QuantityValue from the given parts.
95
	 *
96
	 * @see splitQuantityString
97
	 *
98
	 * @param string $amount decimal representation of the amount
99
	 * @param string|null $exactness either '!' to indicate an exact value,
100
	 *        or '~' for "automatic", or null if $margin should be used.
101
	 * @param string|null $margin decimal representation of the uncertainty margin
102
	 * @param string $unit the unit identifier (use "1" for unitless quantities).
103
	 *
104
	 * @throws ParseException if one of the decimals could not be parsed.
105
	 * @throws IllegalValueException if the QuantityValue could not be constructed
106
	 * @return QuantityValue
107
	 */
108
	private function newQuantityFromParts( $amount, $exactness, $margin, $unit ) {
109
		list( $amount, $exponent ) = $this->decimalParser->splitDecimalExponent( $amount );
110
		$amountValue = $this->decimalParser->parse( $amount );
111
112
		if ( $exactness === '!' ) {
113
			// the amount is an exact number
114
			$amountValue = $this->decimalParser->applyDecimalExponent( $amountValue, $exponent );
115
			$quantity = $this->newExactQuantity( $amountValue, $unit );
116
		} elseif ( $margin !== null ) {
117
			// uncertainty margin given
118
			// NOTE: the pattern for scientific notation is 2e3 +/- 1e2, so the exponents are treated separately.
119
			$marginValue = $this->decimalParser->parse( $margin );
120
			$amountValue = $this->decimalParser->applyDecimalExponent( $amountValue, $exponent );
121
			$quantity = $this->newUncertainQuantityFromMargin( $amountValue, $unit, $marginValue );
122
		} elseif ( $exactness === '~' ) {
123
			// derive uncertainty from given decimals
124
			// NOTE: with scientific notation, the exponent applies to the uncertainty bounds, too
125
			$quantity = $this->newUncertainQuantityFromDigits( $amountValue, $unit, $exponent );
126
		} else {
127
			$amountValue = $this->decimalParser->applyDecimalExponent( $amountValue, $exponent );
128
			return new QuantityValue( $amountValue, $unit, null, null );
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a object<DataValues\DecimalValue>.

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...
129
		}
130
131
		return $quantity;
132
	}
133
134
	/**
135
	 * Splits a quantity string into its syntactic parts.
136
	 *
137
	 * @see newQuantityFromParts
138
	 *
139
	 * @param string $value
140
	 *
141
	 * @throws InvalidArgumentException If $value is not a string
142
	 * @throws ParseException If $value does not match the expected pattern
143
	 * @return array list( $amount, $exactness, $margin, $unit ).
144
	 *         Parts not present in $value will be null
145
	 */
146
	private function splitQuantityString( $value ) {
147
		if ( !is_string( $value ) ) {
148
			throw new InvalidArgumentException( '$value must be a string' );
149
		}
150
151
		//TODO: allow explicitly specifying the number of significant figures
152
		//TODO: allow explicitly specifying the uncertainty interval
153
154
		$numberPattern = $this->unlocalizer->getNumberRegex( '@' );
155
		$unitPattern = $this->unlocalizer->getUnitRegex( '@' );
156
157
		$pattern = '@^'
158
			. '\s*(' . $numberPattern . ')' // $1: amount
159
			. '\s*(?:'
160
				. '([~!])'  // $2: '!' for "exact", '~' for "approx", or nothing
161
				. '|(?:\+/?-|±)\s*(' . $numberPattern . ')' // $3: plus/minus offset (uncertainty margin)
162
				. '|' // or nothing
163
			. ')'
164
			. '\s*(' . $unitPattern . ')?' // $4: unit
165
			. '\s*$@u';
166
167
		if ( !preg_match( $pattern, $value, $groups ) ) {
168
			throw new ParseException( 'Malformed quantity', $value, self::FORMAT_NAME );
169
		}
170
171
		// Remove $0.
172
		array_shift( $groups );
173
174
		array_walk( $groups, function( &$element ) {
175
			if ( $element === '' ) {
176
				$element = null;
177
			}
178
		} );
179
180
		return array_pad( $groups, 4, null );
181
	}
182
183
	/**
184
	 * Returns a QuantityValue representing the given amount.
185
	 * The amount is assumed to be absolutely exact, that is,
186
	 * the upper and lower bound will be the same as the amount.
187
	 *
188
	 * @param DecimalValue $amount
189
	 * @param string $unit The quantity's unit (use "1" for unit-less quantities)
190
	 *
191
	 * @return QuantityValue
192
	 */
193
	private function newExactQuantity( DecimalValue $amount, $unit = '1' ) {
194
		$lowerBound = $amount;
195
		$upperBound = $amount;
196
197
		return new QuantityValue( $amount, $unit, $upperBound, $lowerBound );
198
	}
199
200
	/**
201
	 * Returns a QuantityValue representing the given amount, automatically assuming
202
	 * a level of uncertainty based on the digits given.
203
	 *
204
	 * The upper and lower bounds are determined automatically from the given
205
	 * digits by increasing resp. decreasing the least significant digit.
206
	 * E.g. "+0.01" would have upperBound "+0.02" and lowerBound "+0.01",
207
	 * while "-100" would have upperBound "-99" and lowerBound "-101".
208
	 *
209
	 * @param DecimalValue $amount The quantity
210
	 * @param string $unit The quantity's unit (use "1" for unit-less quantities)
211
	 * @param DecimalValue $margin
212
	 *
213
	 * @return QuantityValue
214
	 */
215
	private function newUncertainQuantityFromMargin( DecimalValue $amount, $unit = '1', DecimalValue $margin ) {
216
		$decimalMath = new DecimalMath();
217
		$margin = $margin->computeAbsolute();
218
219
		$lowerBound = $decimalMath->sum( $amount, $margin->computeComplement() );
220
		$upperBound = $decimalMath->sum( $amount, $margin );
221
222
		return new QuantityValue( $amount, $unit, $upperBound, $lowerBound );
223
	}
224
225
	/**
226
	 * Returns a QuantityValue representing the given amount, automatically assuming
227
	 * a level of uncertainty based on the digits given.
228
	 *
229
	 * The upper and lower bounds are determined automatically from the given
230
	 * digits by increasing resp. decreasing the least significant digit.
231
	 * E.g. "+0.01" would have upperBound "+0.02" and lowerBound "+0.01",
232
	 * while "-100" would have upperBound "-99" and lowerBound "-101".
233
	 *
234
	 * @param DecimalValue $amount The quantity
235
	 * @param string $unit The quantity's unit (use "1" for unit-less quantities)
236
	 * @param int $exponent Decimal exponent to apply
237
	 *
238
	 * @return QuantityValue
239
	 */
240
	private function newUncertainQuantityFromDigits( DecimalValue $amount, $unit = '1', $exponent = 0 ) {
241
		$math = new DecimalMath();
242
243
		if ( $amount->getSign() === '+' ) {
244
			$upperBound = $math->bump( $amount );
245
			$lowerBound = $math->slump( $amount );
246
		} else {
247
			$upperBound = $math->slump( $amount );
248
			$lowerBound = $math->bump( $amount );
249
		}
250
251
		$amount = $this->decimalParser->applyDecimalExponent( $amount, $exponent );
252
		$lowerBound = $this->decimalParser->applyDecimalExponent( $lowerBound, $exponent );
253
		$upperBound = $this->decimalParser->applyDecimalExponent( $upperBound, $exponent );
254
255
		return new QuantityValue( $amount, $unit, $upperBound, $lowerBound );
256
	}
257
258
}
259