QuantityValue::__toString()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

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