Completed
Push — BoundedQuantityValue ( 1533af )
by Daniel
02:40
created

BoundedQuantityValue::__construct()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 14
rs 9.4285
cc 3
eloc 8
nc 3
nop 4
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
 * For quantities with no known uncertainty interval, see QuantityValue.
12
 * For simple numeric amounts use @see NumberValue.
13
 *
14
 * @since 0.1
15
 *
16
 * @license GPL-2.0+
17
 * @author Daniel Kinzler
18
 */
19
class BoundedQuantityValue extends QuantityValue {
20
21
	/**
22
	 * The quantity's upper bound
23
	 *
24
	 * @var DecimalValue
25
	 */
26
	private $upperBound;
27
28
	/**
29
	 * The quantity's lower bound
30
	 *
31
	 * @var DecimalValue
32
	 */
33
	private $lowerBound;
34
35
	/**
36
	 * Constructs a new QuantityValue object, representing the given value.
37
	 *
38
	 * @since 0.1
39
	 *
40
	 * @param DecimalValue $amount
41
	 * @param string $unit A unit identifier. Must not be empty, use "1" for unit-less quantities.
42
	 * @param DecimalValue $upperBound The upper bound of the quantity, inclusive.
43
	 * @param DecimalValue $lowerBound The lower bound of the quantity, inclusive.
44
	 *
45
	 * @throws IllegalValueException
46
	 */
47
	public function __construct( DecimalValue $amount, $unit, DecimalValue $upperBound, DecimalValue $lowerBound ) {
48
		parent::__construct( $amount, $unit );
49
50
		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...
51
			throw new IllegalValueException( '$lowerBound ' . $lowerBound->getValue() . ' must be <= $amount ' . $amount->getValue() );
52
		}
53
54
		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...
55
			throw new IllegalValueException( '$upperBound ' . $upperBound->getValue() . ' must be >= $amount ' . $amount->getValue() );
56
		}
57
58
		$this->upperBound = $upperBound;
59
		$this->lowerBound = $lowerBound;
60
	}
61
62
	/**
63
	 * Returns a QuantityValue representing the given amount.
64
	 * If no upper or lower bound is given, the amount is assumed to be absolutely exact,
65
	 * that is, the amount itself will be used as the upper and lower bound.
66
	 *
67
	 * This is a convenience wrapper around the constructor that accepts native values
68
	 * instead of DecimalValue objects.
69
	 *
70
	 * @note: if the amount or a bound is given as a string, the string must conform
71
	 * to the rules defined by @see DecimalValue.
72
	 *
73
	 * @since 0.1
74
	 *
75
	 * @param string|int|float|DecimalValue|QuantityValue $number
76
	 * @param string $unit A unit identifier. Must not be empty, use "1" for unit-less quantities.
77
	 * @param string|int|float|DecimalValue|null $upperBound
78
	 * @param string|int|float|DecimalValue|null $lowerBound
79
	 *
80
	 * @return self
81
	 * @throws IllegalValueException
82
	 */
83
	public static function newFromNumber( $number, $unit = '1', $upperBound = null, $lowerBound = null ) {
84
		if ( $number instanceof QuantityValue ) {
85
			$number = $number->getAmount();
86
		}
87
88
		$number = self::asDecimalValue( 'amount', $number );
89
		$upperBound = self::asDecimalValue( 'upperBound', $upperBound, $number );
90
		$lowerBound = self::asDecimalValue( 'lowerBound', $lowerBound, $number );
91
92
		return new self( $number, $unit, $upperBound, $lowerBound );
93
	}
94
95
	/**
96
	 * @see newFromNumber
97
	 *
98
	 * @deprecated since 0.1, use newFromNumber instead
99
	 *
100
	 * @param string|int|float|DecimalValue|QuantityValue $number
101
	 * @param string $unit
102
	 * @param string|int|float|DecimalValue|null $upperBound
103
	 * @param string|int|float|DecimalValue|null $lowerBound
104
	 *
105
	 * @return self
106
	 */
107
	public static function newFromDecimal( $number, $unit = '1', $upperBound = null, $lowerBound = null ) {
108
		if ( $number instanceof QuantityValue ) {
109
			$number = $number->getAmount();
110
		}
111
112
		return self::newFromNumber( $number, $unit, $upperBound, $lowerBound );
113
	}
114
115
	/**
116
	 * @see Serializable::unserialize
117
	 *
118
	 * @since 0.1
119
	 *
120
	 * @param string $data
121
	 */
122
	public function unserialize( $data ) {
123
		list( $amount, $unit, $upperBound, $lowerBound ) = unserialize( $data );
124
		$this->__construct( $amount, $unit, $upperBound, $lowerBound );
125
	}
126
127
	/**
128
	 * @see DataValue::getType
129
	 *
130
	 * @since 0.1
131
	 *
132
	 * @return string
133
	 */
134
	public static function getType() {
135
		return 'quantity'; // FIXME: should this have a different type id? wehat will that break?
136
	}
137
138
	/**
139
	 * Returns this quantity's upper bound.
140
	 *
141
	 * @since 0.1
142
	 *
143
	 * @return DecimalValue
144
	 */
145
	public function getUpperBound() {
146
		return $this->upperBound;
147
	}
148
149
	/**
150
	 * Returns this quantity's lower bound.
151
	 *
152
	 * @since 0.1
153
	 *
154
	 * @return DecimalValue
155
	 */
156
	public function getLowerBound() {
157
		return $this->lowerBound;
158
	}
159
160
	/**
161
	 * Returns the size of the uncertainty interval.
162
	 * This can roughly be interpreted as "amount +/- uncertainty/2".
163
	 *
164
	 * The exact interpretation of the uncertainty interval is left to the concrete application or
165
	 * data point. For example, the uncertainty interval may be defined to be that part of a
166
	 * normal distribution that is required to cover the 95th percentile.
167
	 *
168
	 * @since 0.1
169
	 *
170
	 * @return float
171
	 */
172
	public function getUncertainty() {
173
		return $this->upperBound->getValueFloat() - $this->lowerBound->getValueFloat();
174
	}
175
176
	/**
177
	 * Returns a DecimalValue representing the symmetrical offset to be applied
178
	 * to the raw amount for a rough representation of the uncertainty interval,
179
	 * as in "amount +/- offset".
180
	 *
181
	 * The offset is calculated as max( amount - lowerBound, upperBound - amount ).
182
	 *
183
	 * @since 0.1
184
	 *
185
	 * @return DecimalValue
186
	 */
187
	public function getUncertaintyMargin() {
188
		$math = new DecimalMath();
189
190
		$lowerMargin = $math->sum( $this->amount, $this->lowerBound->computeComplement() );
191
		$upperMargin = $math->sum( $this->upperBound, $this->amount->computeComplement() );
192
193
		$margin = $math->max( $lowerMargin, $upperMargin );
194
		return $margin;
195
	}
196
197
	/**
198
	 * Returns the order of magnitude of the uncertainty as the exponent of
199
	 * last significant digit in the amount-string. The value returned by this
200
	 * is suitable for use with @see DecimalMath::roundToExponent().
201
	 *
202
	 * @example: if two digits after the decimal point are significant, this
203
	 * returns -2.
204
	 *
205
	 * @example: if the last two digits before the decimal point are insignificant,
206
	 * this returns 2.
207
	 *
208
	 * Note that this calculation assumes a symmetric uncertainty interval,
209
	 * and can be misleading.
210
	 *
211
	 * @since 0.1
212
	 *
213
	 * @return int
214
	 */
215
	public function getOrderOfUncertainty() {
216
		// the desired precision is given by the distance between the amount and
217
		// whatever is closer, the upper or lower bound.
218
		//TODO: use DecimalMath to avoid floating point errors!
219
		$amount = $this->amount->getValueFloat();
220
		$upperBound = $this->upperBound->getValueFloat();
221
		$lowerBound = $this->lowerBound->getValueFloat();
222
		$precision = min( $amount - $lowerBound, $upperBound - $amount );
223
224
		if ( $precision === 0.0 ) {
225
			// If there is no uncertainty, the order of uncertainty is a bit more than what we have digits for.
226
			return -strlen( $this->amount->getFractionalPart() );
227
		}
228
229
		// e.g. +/- 200 -> 2; +/- 0.02 -> -2
230
		// note: we really want floor( log10( $precision ) ), but have to account for
231
		// small errors made in the floating point operations above.
232
		// @todo: use bcmath (via DecimalMath) to avoid this if possible
233
		$orderOfUncertainty = floor( log10( $precision + 0.0000000005 ) );
234
235
		return (int)$orderOfUncertainty;
236
	}
237
238
	/**
239
	 * Returns the number of significant figures in the amount-string,
240
	 * counting the decimal point, but not counting the leading sign.
241
	 *
242
	 * Note that this calculation assumes a symmetric uncertainty interval, and can be misleading
243
	 *
244
	 * @since 0.1
245
	 *
246
	 * @return int
247
	 */
248
	public function getSignificantFigures() {
249
		$math = new DecimalMath();
250
251
		// $orderOfUncertainty is +/- 200 -> 2; +/- 0.02 -> -2
252
		$orderOfUncertainty = $this->getOrderOfUncertainty();
253
254
		// the number of digits (without the sign) is the same as the position (with the sign).
255
		$significantDigits = $math->getPositionForExponent( $orderOfUncertainty, $this->amount );
256
257
		return $significantDigits;
258
	}
259
260
	/**
261
	 * Returns a transformed value derived from this QuantityValue by applying
262
	 * the given transformation to the amount and the upper and lower bounds.
263
	 * The resulting amount and bounds are rounded to the significant number of
264
	 * digits. Note that for exact quantities (with at least one bound equal to
265
	 * the amount), no rounding is applied (since they are considered to have
266
	 * infinite precision).
267
	 *
268
	 * The transformation is provided as a callback, which must implement a
269
	 * monotonously increasing, fully differentiable function mapping a DecimalValue
270
	 * to a DecimalValue. Typically, it will be a linear transformation applying a
271
	 * factor and an offset.
272
	 *
273
	 * @param string $newUnit The unit of the transformed quantity.
274
	 *
275
	 * @param callable $transformation A callback that implements the desired transformation.
276
	 *        The transformation will be called three times, once for the amount, once
277
	 *        for the lower bound, and once for the upper bound. It must return a DecimalValue.
278
	 *        The first parameter passed to $transformation is the DecimalValue to transform
279
	 *        In addition, any extra parameters passed to transform() will be passed through
280
	 *        to the transformation callback.
281
	 *
282
	 * @param mixed ... Any extra parameters will be passed to the $transformation function.
283
	 *
284
	 * @throws InvalidArgumentException
285
	 * @return self
286
	 */
287
	public function transform( $newUnit, $transformation ) {
288
		if ( !is_callable( $transformation ) ) {
289
			throw new InvalidArgumentException( '$transformation must be callable.' );
290
		}
291
292
		if ( !is_string( $newUnit ) ) {
293
			throw new InvalidArgumentException( '$newUnit must be a string. Use "1" as the unit for unit-less quantities.' );
294
		}
295
296
		if ( $newUnit === '' ) {
297
			throw new InvalidArgumentException( '$newUnit must not be empty. Use "1" as the unit for unit-less quantities.' );
298
		}
299
300
		$oldUnit = $this->unit;
301
302
		if ( $newUnit === null ) {
303
			$newUnit = $oldUnit;
304
		}
305
306
		// Apply transformation by calling the $transform callback.
307
		// The first argument for the callback is the DataValue to transform. In addition,
308
		// any extra arguments given for transform() are passed through.
309
		$args = func_get_args();
310
		array_shift( $args );
311
312
		$args[0] = $this->amount;
313
		$amount = call_user_func_array( $transformation, $args );
314
315
		$args[0] = $this->upperBound;
316
		$upperBound = call_user_func_array( $transformation, $args );
317
318
		$args[0] = $this->lowerBound;
319
		$lowerBound = call_user_func_array( $transformation, $args );
320
321
		// use a preliminary QuantityValue to determine the significant number of digits
322
		$transformed = new self( $amount, $newUnit, $upperBound, $lowerBound );
323
		$roundingExponent = $transformed->getOrderOfUncertainty();
324
325
		// apply rounding to the significant digits
326
		$math = new DecimalMath();
327
328
		$amount = $math->roundToExponent( $amount, $roundingExponent );
329
		$upperBound = $math->roundToExponent( $upperBound, $roundingExponent );
330
		$lowerBound = $math->roundToExponent( $lowerBound, $roundingExponent );
331
332
		return new self( $amount, $newUnit, $upperBound, $lowerBound );
333
	}
334
335
	public function __toString() {
336
		return $this->amount->getValue()
337
			. '[' . $this->lowerBound->getValue()
338
			. '..' . $this->upperBound->getValue()
339
			. ']'
340
			. ( $this->unit === '1' ? '' : $this->unit );
341
	}
342
343
	/**
344
	 * @see DataValue::getArrayValue
345
	 *
346
	 * @since 0.1
347
	 *
348
	 * @return array
349
	 */
350
	public function getArrayValue() {
351
		return array(
352
			'amount' => $this->amount->getArrayValue(),
353
			'unit' => $this->unit,
354
			'upperBound' => $this->upperBound->getArrayValue(),
355
			'lowerBound' => $this->lowerBound->getArrayValue(),
356
		);
357
	}
358
359
	/**
360
	 * Constructs a new instance of the DataValue from the provided data.
361
	 * This can round-trip with @see getArrayValue
362
	 *
363
	 * @since 0.1
364
	 *
365
	 * @param mixed $data
366
	 *
367
	 * @return self
368
	 * @throws IllegalValueException
369
	 */
370
	public static function newFromArray( $data ) {
371
		self::requireArrayFields( $data, array( 'amount', 'unit', 'upperBound', 'lowerBound' ) );
372
373
		return new static(
374
			DecimalValue::newFromArray( $data['amount'] ),
375
			$data['unit'],
376
			DecimalValue::newFromArray( $data['upperBound'] ),
377
			DecimalValue::newFromArray( $data['lowerBound'] )
378
		);
379
	}
380
381
}
382