QuantityValue   A
last analyzed

Complexity

Total Complexity 19

Size/Duplication

Total Lines 273
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 97.37%

Importance

Changes 0
Metric Value
wmc 19
lcom 1
cbo 4
dl 0
loc 273
ccs 74
cts 76
cp 0.9737
rs 10
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 3
A newFromNumber() 0 7 1
A serialize() 0 8 1
A unserialize() 0 4 1
A getUpperBound() 0 3 1
A getLowerBound() 0 3 1
A getUncertainty() 0 3 1
A getUncertaintyMargin() 0 9 1
A getOrderOfUncertainty() 0 22 2
A transform() 0 37 4
A __toString() 0 7 2
A getArrayValue() 0 8 1
1
<?php
2
3
namespace DataValues;
4
5
use InvalidArgumentException;
6
7
/**
8
 * Class representing a quantity with associated unit and uncertainty interval.
9
 * The amount is stored as a @see DecimalValue object.
10
 *
11
 * @see UnboundedQuantityValue for quantities with unknown uncertainty interval.
12
 * For simple numeric amounts use @see NumberValue.
13
 *
14
 * @note UnboundedQuantityValue and QuantityValue both use the value type ID "quantity".
15
 * The fact that we use subclassing to model the bounded vs the unbounded case should be
16
 * considered an implementation detail.
17
 *
18
 * @since 0.1
19
 *
20
 * @license GPL-2.0+
21
 * @author Daniel Kinzler
22
 */
23
class QuantityValue extends UnboundedQuantityValue {
24
25
	/**
26
	 * The quantity's upper bound
27
	 *
28
	 * @var DecimalValue
29
	 */
30
	private $upperBound;
31
32
	/**
33
	 * The quantity's lower bound
34
	 *
35
	 * @var DecimalValue
36
	 */
37
	private $lowerBound;
38
39
	/**
40
	 * @since 0.1
41
	 *
42
	 * @param DecimalValue $amount
43
	 * @param string $unit A unit identifier. Must not be empty, use "1" for unit-less quantities.
44
	 * @param DecimalValue $upperBound The upper bound of the quantity, inclusive.
45
	 * @param DecimalValue $lowerBound The lower bound of the quantity, inclusive.
46
	 *
47
	 * @throws IllegalValueException
48
	 */
49 34
	public function __construct( DecimalValue $amount, $unit, DecimalValue $upperBound, DecimalValue $lowerBound ) {
50 34
		parent::__construct( $amount, $unit );
51
52 32
		if ( $lowerBound->compare( $amount ) > 0 ) {
0 ignored issues
show
Documentation introduced by
$amount is of type object<DataValues\DecimalValue>, but the function expects a object<self>.

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...
53 1
			throw new IllegalValueException( '$lowerBound ' . $lowerBound->getValue() . ' must be <= $amount ' . $amount->getValue() );
54
		}
55
56 31
		if ( $upperBound->compare( $amount ) < 0 ) {
0 ignored issues
show
Documentation introduced by
$amount is of type object<DataValues\DecimalValue>, but the function expects a object<self>.

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...
57 1
			throw new IllegalValueException( '$upperBound ' . $upperBound->getValue() . ' must be >= $amount ' . $amount->getValue() );
58
		}
59
60 30
		$this->upperBound = $upperBound;
61 30
		$this->lowerBound = $lowerBound;
62 30
	}
63
64
	/**
65
	 * Returns a QuantityValue representing the given amount.
66
	 * If no upper or lower bound is given, the amount is assumed to be absolutely exact,
67
	 * that is, the amount itself will be used as the upper and lower bound.
68
	 *
69
	 * This is a convenience wrapper around the constructor that accepts native values
70
	 * instead of DecimalValue objects.
71
	 *
72
	 * @note: if the amount or a bound is given as a string, the string must conform
73
	 * to the rules defined by @see DecimalValue.
74
	 *
75
	 * @since 0.1
76
	 *
77
	 * @param string|int|float|DecimalValue $amount
78
	 * @param string $unit A unit identifier. Must not be empty, use "1" for unit-less quantities.
79
	 * @param string|int|float|DecimalValue|null $upperBound
80
	 * @param string|int|float|DecimalValue|null $lowerBound
81
	 *
82
	 * @return self
83
	 * @throws IllegalValueException
84
	 */
85 7
	public static function newFromNumber( $amount, $unit = '1', $upperBound = null, $lowerBound = null ) {
86 7
		$amount = self::asDecimalValue( 'amount', $amount );
87 7
		$upperBound = self::asDecimalValue( 'upperBound', $upperBound, $amount );
88 7
		$lowerBound = self::asDecimalValue( 'lowerBound', $lowerBound, $amount );
89
90 7
		return new self( $amount, $unit, $upperBound, $lowerBound );
91
	}
92
93
	/**
94
	 * @see Serializable::serialize
95
	 *
96
	 * @since 0.1
97
	 *
98
	 * @return string
99
	 */
100 9
	public function serialize() {
101 9
		return serialize( [
102 9
			$this->amount,
103 9
			$this->unit,
104 9
			$this->upperBound,
105 9
			$this->lowerBound,
106
		] );
107
	}
108
109
	/**
110
	 * @see Serializable::unserialize
111
	 *
112
	 * @since 0.1
113
	 *
114
	 * @param string $data
115
	 */
116 9
	public function unserialize( $data ) {
117 9
		list( $amount, $unit, $upperBound, $lowerBound ) = unserialize( $data );
118 9
		$this->__construct( $amount, $unit, $upperBound, $lowerBound );
119 9
	}
120
121
	/**
122
	 * Returns this quantity's upper bound.
123
	 *
124
	 * @since 0.1
125
	 *
126
	 * @return DecimalValue
127
	 */
128 19
	public function getUpperBound() {
129 19
		return $this->upperBound;
130
	}
131
132
	/**
133
	 * Returns this quantity's lower bound.
134
	 *
135
	 * @since 0.1
136
	 *
137
	 * @return DecimalValue
138
	 */
139 19
	public function getLowerBound() {
140 19
		return $this->lowerBound;
141
	}
142
143
	/**
144
	 * Returns the size of the uncertainty interval.
145
	 * This can roughly be interpreted as "amount +/- uncertainty/2".
146
	 *
147
	 * The exact interpretation of the uncertainty interval is left to the concrete application or
148
	 * data point. For example, the uncertainty interval may be defined to be that part of a
149
	 * normal distribution that is required to cover the 95th percentile.
150
	 *
151
	 * @since 0.1
152
	 *
153
	 * @return float
154
	 */
155 8
	public function getUncertainty() {
156 8
		return $this->upperBound->getValueFloat() - $this->lowerBound->getValueFloat();
157
	}
158
159
	/**
160
	 * Returns a DecimalValue representing the symmetrical offset to be applied
161
	 * to the raw amount for a rough representation of the uncertainty interval,
162
	 * as in "amount +/- offset".
163
	 *
164
	 * The offset is calculated as max( amount - lowerBound, upperBound - amount ).
165
	 *
166
	 * @since 0.1
167
	 *
168
	 * @return DecimalValue
169
	 */
170 6
	public function getUncertaintyMargin() {
171 6
		$math = new DecimalMath();
172
173 6
		$lowerMargin = $math->sum( $this->amount, $this->lowerBound->computeComplement() );
174 6
		$upperMargin = $math->sum( $this->upperBound, $this->amount->computeComplement() );
175
176 6
		$margin = $math->max( $lowerMargin, $upperMargin );
177 6
		return $margin;
178
	}
179
180
	/**
181
	 * Returns the order of magnitude of the uncertainty as the exponent of
182
	 * last significant digit in the amount-string. The value returned by this
183
	 * is suitable for use with @see DecimalMath::roundToExponent().
184
	 *
185
	 * @example: if two digits after the decimal point are significant, this
186
	 * returns -2.
187
	 *
188
	 * @example: if the last two digits before the decimal point are insignificant,
189
	 * this returns 2.
190
	 *
191
	 * Note that this calculation assumes a symmetric uncertainty interval,
192
	 * and can be misleading.
193
	 *
194
	 * @since 0.1
195
	 *
196
	 * @return int
197
	 */
198 22
	public function getOrderOfUncertainty() {
199
		// the desired precision is given by the distance between the amount and
200
		// whatever is closer, the upper or lower bound.
201
		//TODO: use DecimalMath to avoid floating point errors!
202 22
		$amount = $this->amount->getValueFloat();
203 22
		$upperBound = $this->upperBound->getValueFloat();
204 22
		$lowerBound = $this->lowerBound->getValueFloat();
205 22
		$precision = min( $amount - $lowerBound, $upperBound - $amount );
206
207 22
		if ( $precision === 0.0 ) {
208
			// If there is no uncertainty, the order of uncertainty is a bit more than what we have digits for.
209 4
			return -strlen( $this->amount->getFractionalPart() );
210
		}
211
212
		// e.g. +/- 200 -> 2; +/- 0.02 -> -2
213
		// note: we really want floor( log10( $precision ) ), but have to account for
214
		// small errors made in the floating point operations above.
215
		// @todo: use bcmath (via DecimalMath) to avoid this if possible
216 18
		$orderOfUncertainty = floor( log10( $precision + 0.0000000005 ) );
217
218 18
		return (int)$orderOfUncertainty;
219
	}
220
221
	/**
222
	 * @see UnboundedQuantityValue::transform
223
	 *
224
	 * @param string $newUnit
225
	 * @param callable $transformation
226
	 * @param mixed [$args,...]
227
	 *
228
	 * @todo Should be factored out into a separate QuantityMath class.
229
	 *
230
	 * @throws InvalidArgumentException
231
	 * @return self
232
	 */
233 9
	public function transform( $newUnit, $transformation ) {
234 9
		if ( !is_callable( $transformation ) ) {
235
			throw new InvalidArgumentException( '$transformation must be callable.' );
236
		}
237
238 9
		if ( !is_string( $newUnit ) || $newUnit === '' ) {
239
			throw new InvalidArgumentException( '$newUnit must be a non-empty string. Use "1" for unit-less quantities.' );
240
		}
241
242
		// Apply transformation by calling the $transform callback.
243
		// The first argument for the callback is the DataValue to transform. In addition,
244
		// any extra arguments given for transform() are passed through.
245 9
		$args = func_get_args();
246 9
		array_shift( $args );
247
248 9
		$args[0] = $this->amount;
249 9
		$amount = call_user_func_array( $transformation, $args );
250
251 9
		$args[0] = $this->upperBound;
252 9
		$upperBound = call_user_func_array( $transformation, $args );
253
254 9
		$args[0] = $this->lowerBound;
255 9
		$lowerBound = call_user_func_array( $transformation, $args );
256
257
		// use a preliminary QuantityValue to determine the significant number of digits
258 9
		$transformed = new self( $amount, $newUnit, $upperBound, $lowerBound );
259 9
		$roundingExponent = $transformed->getOrderOfUncertainty();
260
261
		// apply rounding to the significant digits
262 9
		$math = new DecimalMath();
263
264 9
		$amount = $math->roundToExponent( $amount, $roundingExponent );
265 9
		$upperBound = $math->roundToExponent( $upperBound, $roundingExponent );
266 9
		$lowerBound = $math->roundToExponent( $lowerBound, $roundingExponent );
267
268 9
		return new self( $amount, $newUnit, $upperBound, $lowerBound );
269
	}
270
271 1
	public function __toString() {
272 1
		return $this->amount->getValue()
273 1
			. '[' . $this->lowerBound->getValue()
274 1
			. '..' . $this->upperBound->getValue()
275 1
			. ']'
276 1
			. ( $this->unit === '1' ? '' : $this->unit );
277
	}
278
279
	/**
280
	 * @see DataValue::getArrayValue
281
	 *
282
	 * @since 0.1
283
	 *
284
	 * @return array
285
	 */
286 14
	public function getArrayValue() {
287
		return [
288 14
			'amount' => $this->amount->getArrayValue(),
289 14
			'unit' => $this->unit,
290 14
			'upperBound' => $this->upperBound->getArrayValue(),
291 14
			'lowerBound' => $this->lowerBound->getArrayValue(),
292
		];
293
	}
294
295
}
296