Completed
Push — master ( ee11b3...9e15b9 )
by adam
02:46
created

src/DataValues/QuantityValue.php (2 issues)

Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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 simple numeric amounts use @see NumberValue.
12
 *
13
 * @since 0.1
14
 *
15
 * @licence GNU GPL v2+
16
 * @author Daniel Kinzler
17
 */
18
class QuantityValue extends DataValueObject {
19
20
	/**
21
	 * The quantity's amount
22
	 *
23
	 * @var DecimalValue
24
	 */
25
	private $amount;
26
27
	/**
28
	 * The quantity's unit identifier (use "1" for unitless quantities).
29
	 *
30
	 * @var string
31
	 */
32
	private $unit;
33
34
	/**
35
	 * The quantity's upper bound
36
	 *
37
	 * @var DecimalValue
38
	 */
39
	private $upperBound;
40
41
	/**
42
	 * The quantity's lower bound
43
	 *
44
	 * @var DecimalValue
45
	 */
46
	private $lowerBound;
47
48
	/**
49
	 * Constructs a new QuantityValue object, representing the given value.
50
	 *
51
	 * @since 0.1
52
	 *
53
	 * @param DecimalValue $amount
54
	 * @param string $unit A unit identifier. Must not be empty, use "1" for unit-less quantities.
55
	 * @param DecimalValue $upperBound The upper bound of the quantity, inclusive.
56
	 * @param DecimalValue $lowerBound The lower bound of the quantity, inclusive.
57
	 *
58
	 * @throws IllegalValueException
59
	 */
60
	public function __construct( DecimalValue $amount, $unit, DecimalValue $upperBound, DecimalValue $lowerBound ) {
61
		if ( $lowerBound->compare( $amount ) > 0 ) {
0 ignored issues
show
$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...
62
			throw new IllegalValueException( '$lowerBound ' . $lowerBound->getValue() . ' must be <= $amount ' . $amount->getValue() );
63
		}
64
65
		if ( $upperBound->compare( $amount ) < 0 ) {
0 ignored issues
show
$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...
66
			throw new IllegalValueException( '$upperBound ' . $upperBound->getValue() . ' must be >= $amount ' . $amount->getValue() );
67
		}
68
69
		if ( !is_string( $unit ) ) {
70
			throw new IllegalValueException( '$unit needs to be a string, not ' . gettype( $unit ) );
71
		}
72
73
		if ( $unit === '' ) {
74
			throw new IllegalValueException( '$unit can not be an empty string (use "1" for unit-less quantities)' );
75
		}
76
77
		$this->amount = $amount;
78
		$this->unit = $unit;
79
		$this->upperBound = $upperBound;
80
		$this->lowerBound = $lowerBound;
81
	}
82
83
	/**
84
	 * Returns a QuantityValue representing the given amount.
85
	 * If no upper or lower bound is given, the amount is assumed to be absolutely exact,
86
	 * that is, the amount itself will be used as the upper and lower bound.
87
	 *
88
	 * This is a convenience wrapper around the constructor that accepts native values
89
	 * instead of DecimalValue objects.
90
	 *
91
	 * @note: if the amount or a bound is given as a string, the string must conform
92
	 * to the rules defined by @see DecimalValue.
93
	 *
94
	 * @since 0.1
95
	 *
96
	 * @param string|int|float|DecimalValue $amount
97
	 * @param string $unit A unit identifier. Must not be empty, use "1" for unit-less quantities.
98
	 * @param string|int|float|DecimalValue|null $upperBound
99
	 * @param string|int|float|DecimalValue|null $lowerBound
100
	 *
101
	 * @return self
102
	 * @throws IllegalValueException
103
	 */
104
	public static function newFromNumber( $amount, $unit = '1', $upperBound = null, $lowerBound = null ) {
105
		$amount = self::asDecimalValue( 'amount', $amount );
106
		$upperBound = self::asDecimalValue( 'upperBound', $upperBound, $amount );
107
		$lowerBound = self::asDecimalValue( 'lowerBound', $lowerBound, $amount );
108
109
		return new self( $amount, $unit, $upperBound, $lowerBound );
110
	}
111
112
	/**
113
	 * @see newFromNumber
114
	 *
115
	 * @deprecated since 0.1, use newFromNumber instead
116
	 *
117
	 * @param string|int|float|DecimalValue $amount
118
	 * @param string $unit
119
	 * @param string|int|float|DecimalValue|null $upperBound
120
	 * @param string|int|float|DecimalValue|null $lowerBound
121
	 *
122
	 * @return self
123
	 */
124
	public static function newFromDecimal( $amount, $unit = '1', $upperBound = null, $lowerBound = null ) {
125
		return self::newFromNumber( $amount, $unit, $upperBound, $lowerBound );
126
	}
127
128
	/**
129
	 * Converts $number to a DecimalValue if possible and necessary.
130
	 *
131
	 * @note: if the $number is given as a string, it must conform to the rules
132
	 *        defined by @see DecimalValue.
133
	 *
134
	 * @param string $name The variable name to use in exception messages
135
	 * @param string|int|float|DecimalValue|null $number
136
	 * @param DecimalValue|null $default
137
	 *
138
	 * @throws IllegalValueException
139
	 * @throws InvalidArgumentException
140
	 * @return DecimalValue
141
	 */
142
	private static function asDecimalValue( $name, $number, DecimalValue $default = null ) {
143
		if ( !is_string( $name ) ) {
144
			throw new InvalidArgumentException( '$name must be a string' );
145
		}
146
147
		if ( $number === null ) {
148
			if ( $default === null ) {
149
				throw new InvalidArgumentException( '$' . $name . ' must not be null' );
150
			}
151
152
			$number = $default;
153
		}
154
155
		if ( $number instanceof DecimalValue ) {
156
			// nothing to do
157
		} elseif ( is_int( $number ) || is_float( $number ) || is_string( $number ) ) {
158
			$number = new DecimalValue( $number );
159
		} else {
160
			throw new IllegalValueException( '$' . $name . '  must be a string, int, or float' );
161
		}
162
163
		return $number;
164
	}
165
166
	/**
167
	 * @see Serializable::serialize
168
	 *
169
	 * @since 0.1
170
	 *
171
	 * @return string
172
	 */
173
	public function serialize() {
174
		return serialize( array(
175
			$this->amount,
176
			$this->unit,
177
			$this->upperBound,
178
			$this->lowerBound,
179
		) );
180
	}
181
182
	/**
183
	 * @see Serializable::unserialize
184
	 *
185
	 * @since 0.1
186
	 *
187
	 * @param string $data
188
	 */
189
	public function unserialize( $data ) {
190
		list( $amount, $unit, $upperBound, $lowerBound ) = unserialize( $data );
191
		$this->__construct( $amount, $unit, $upperBound, $lowerBound );
192
	}
193
194
	/**
195
	 * @see DataValue::getType
196
	 *
197
	 * @since 0.1
198
	 *
199
	 * @return string
200
	 */
201
	public static function getType() {
202
		return 'quantity';
203
	}
204
205
	/**
206
	 * @see DataValue::getSortKey
207
	 *
208
	 * @since 0.1
209
	 *
210
	 * @return float
211
	 */
212
	public function getSortKey() {
213
		return $this->getAmount()->getValueFloat();
214
	}
215
216
	/**
217
	 * Returns the quantity object.
218
	 * @see DataValue::getValue
219
	 *
220
	 * @since 0.1
221
	 *
222
	 * @return self
223
	 */
224
	public function getValue() {
225
		return $this;
226
	}
227
228
	/**
229
	 * Returns the amount represented by this quantity.
230
	 *
231
	 * @since 0.1
232
	 *
233
	 * @return DecimalValue
234
	 */
235
	public function getAmount() {
236
		return $this->amount;
237
	}
238
239
	/**
240
	 * Returns this quantity's upper bound.
241
	 *
242
	 * @since 0.1
243
	 *
244
	 * @return DecimalValue
245
	 */
246
	public function getUpperBound() {
247
		return $this->upperBound;
248
	}
249
250
	/**
251
	 * Returns this quantity's lower bound.
252
	 *
253
	 * @since 0.1
254
	 *
255
	 * @return DecimalValue
256
	 */
257
	public function getLowerBound() {
258
		return $this->lowerBound;
259
	}
260
261
	/**
262
	 * Returns the size of the uncertainty interval.
263
	 * This can roughly be interpreted as "amount +/- uncertainty/2".
264
	 *
265
	 * The exact interpretation of the uncertainty interval is left to the concrete application or
266
	 * data point. For example, the uncertainty interval may be defined to be that part of a
267
	 * normal distribution that is required to cover the 95th percentile.
268
	 *
269
	 * @since 0.1
270
	 *
271
	 * @return float
272
	 */
273
	public function getUncertainty() {
274
		return $this->getUpperBound()->getValueFloat() - $this->getLowerBound()->getValueFloat();
275
	}
276
277
	/**
278
	 * Returns a DecimalValue representing the symmetrical offset to be applied
279
	 * to the raw amount for a rough representation of the uncertainty interval,
280
	 * as in "amount +/- offset".
281
	 *
282
	 * The offset is calculated as max( amount - lowerBound, upperBound - amount ).
283
	 *
284
	 * @since 0.1
285
	 *
286
	 * @return DecimalValue
287
	 */
288
	public function getUncertaintyMargin() {
289
		$math = new DecimalMath();
290
291
		$lowerMargin = $math->sum( $this->getAmount(), $this->getLowerBound()->computeComplement() );
292
		$upperMargin = $math->sum( $this->getUpperBound(), $this->getAmount()->computeComplement() );
293
294
		$margin = $math->max( $lowerMargin, $upperMargin );
295
		return $margin;
296
	}
297
298
	/**
299
	 * Returns the order of magnitude of the uncertainty as the exponent of
300
	 * last significant digit in the amount-string. The value returned by this
301
	 * is suitable for use with @see DecimalMath::roundToExponent().
302
	 *
303
	 * @example: if two digits after the decimal point are significant, this
304
	 * returns -2.
305
	 *
306
	 * @example: if the last two digits before the decimal point are insignificant,
307
	 * this returns 2.
308
	 *
309
	 * Note that this calculation assumes a symmetric uncertainty interval,
310
	 * and can be misleading.
311
	 *
312
	 * @since 0.1
313
	 *
314
	 * @return int
315
	 */
316
	public function getOrderOfUncertainty() {
317
		// the desired precision is given by the distance between the amount and
318
		// whatever is closer, the upper or lower bound.
319
		//TODO: use DecimalMath to avoid floating point errors!
320
		$amount = $this->getAmount()->getValueFloat();
321
		$upperBound = $this->getUpperBound()->getValueFloat();
322
		$lowerBound = $this->getLowerBound()->getValueFloat();
323
		$precision = min( $amount - $lowerBound, $upperBound - $amount );
324
325
		if ( $precision === 0.0 ) {
326
			// If there is no uncertainty, the order of uncertainty is a bit more than what we have digits for.
327
			return -strlen( $this->amount->getFractionalPart() );
328
		}
329
330
		// e.g. +/- 200 -> 2; +/- 0.02 -> -2
331
		// note: we really want floor( log10( $precision ) ), but have to account for
332
		// small errors made in the floating point operations above.
333
		// @todo: use bcmath (via DecimalMath) to avoid this if possible
334
		$orderOfUncertainty = floor( log10( $precision + 0.0000000005 ) );
335
336
		return (int)$orderOfUncertainty;
337
	}
338
339
	/**
340
	 * Returns the number of significant figures in the amount-string,
341
	 * counting the decimal point, but not counting the leading sign.
342
	 *
343
	 * Note that this calculation assumes a symmetric uncertainty interval, and can be misleading
344
	 *
345
	 * @since 0.1
346
	 *
347
	 * @return int
348
	 */
349
	public function getSignificantFigures() {
350
		$math = new DecimalMath();
351
352
		// $orderOfUncertainty is +/- 200 -> 2; +/- 0.02 -> -2
353
		$orderOfUncertainty = $this->getOrderOfUncertainty();
354
355
		// the number of digits (without the sign) is the same as the position (with the sign).
356
		$significantDigits = $math->getPositionForExponent( $orderOfUncertainty, $this->amount );
357
358
		return $significantDigits;
359
	}
360
361
	/**
362
	 * Returns the unit held by this quantity.
363
	 * Unit-less quantities should use "1" as their unit.
364
	 *
365
	 * @since 0.1
366
	 *
367
	 * @return string
368
	 */
369
	public function getUnit() {
370
		return $this->unit;
371
	}
372
373
	/**
374
	 * Returns a transformed value derived from this QuantityValue by applying
375
	 * the given transformation to the amount and the upper and lower bounds.
376
	 * The resulting amount and bounds are rounded to the significant number of
377
	 * digits. Note that for exact quantities (with at least one bound equal to
378
	 * the amount), no rounding is applied (since they are considered to have
379
	 * infinite precision).
380
	 *
381
	 * The transformation is provided as a callback, which must implement a
382
	 * monotonously increasing, fully differentiable function mapping a DecimalValue
383
	 * to a DecimalValue. Typically, it will be a linear transformation applying a
384
	 * factor and an offset.
385
	 *
386
	 * @param string $newUnit The unit of the transformed quantity.
387
	 *
388
	 * @param callable $transformation A callback that implements the desired transformation.
389
	 *        The transformation will be called three times, once for the amount, once
390
	 *        for the lower bound, and once for the upper bound. It must return a DecimalValue.
391
	 *        The first parameter passed to $transformation is the DecimalValue to transform
392
	 *        In addition, any extra parameters passed to transform() will be passed through
393
	 *        to the transformation callback.
394
	 *
395
	 * @param mixed ... Any extra parameters will be passed to the $transformation function.
396
	 *
397
	 * @throws InvalidArgumentException
398
	 * @return self
399
	 */
400
	public function transform( $newUnit, $transformation ) {
401
		if ( !is_callable( $transformation ) ) {
402
			throw new InvalidArgumentException( '$transformation must be callable.' );
403
		}
404
405
		if ( !is_string( $newUnit ) ) {
406
			throw new InvalidArgumentException( '$newUnit must be a string. Use "1" as the unit for unit-less quantities.' );
407
		}
408
409
		if ( $newUnit === '' ) {
410
			throw new InvalidArgumentException( '$newUnit must not be empty. Use "1" as the unit for unit-less quantities.' );
411
		}
412
413
		$oldUnit = $this->getUnit();
414
415
		if ( $newUnit === null ) {
416
			$newUnit = $oldUnit;
417
		}
418
419
		// Apply transformation by calling the $transform callback.
420
		// The first argument for the callback is the DataValue to transform. In addition,
421
		// any extra arguments given for transform() are passed through.
422
		$args = func_get_args();
423
		array_shift( $args );
424
425
		$args[0] = $this->getAmount();
426
		$amount = call_user_func_array( $transformation, $args );
427
428
		$args[0] = $this->getUpperBound();
429
		$upperBound = call_user_func_array( $transformation, $args );
430
431
		$args[0] = $this->getLowerBound();
432
		$lowerBound = call_user_func_array( $transformation, $args );
433
434
		// use a preliminary QuantityValue to determine the significant number of digits
435
		$transformed = new self( $amount, $newUnit, $upperBound, $lowerBound );
436
		$roundingExponent = $transformed->getOrderOfUncertainty();
437
438
		// apply rounding to the significant digits
439
		$math = new DecimalMath();
440
441
		$amount = $math->roundToExponent( $amount, $roundingExponent );
442
		$upperBound = $math->roundToExponent( $upperBound, $roundingExponent );
443
		$lowerBound = $math->roundToExponent( $lowerBound, $roundingExponent );
444
445
		return new self( $amount, $newUnit, $upperBound, $lowerBound );
446
	}
447
448
	public function __toString() {
449
		$unit = $this->getUnit();
450
		return $this->amount->getValue()
451
			. '[' . $this->lowerBound->getValue()
452
			. '..' . $this->upperBound->getValue()
453
			. ']'
454
			. ( $unit === '1' ? '' : $unit );
455
	}
456
457
	/**
458
	 * @see DataValue::getArrayValue
459
	 *
460
	 * @since 0.1
461
	 *
462
	 * @return array
463
	 */
464
	public function getArrayValue() {
465
		return array(
466
			'amount' => $this->amount->getArrayValue(),
467
			'unit' => $this->unit,
468
			'upperBound' => $this->upperBound->getArrayValue(),
469
			'lowerBound' => $this->lowerBound->getArrayValue(),
470
		);
471
	}
472
473
	/**
474
	 * Constructs a new instance of the DataValue from the provided data.
475
	 * This can round-trip with @see getArrayValue
476
	 *
477
	 * @since 0.1
478
	 *
479
	 * @param mixed $data
480
	 *
481
	 * @return static
482
	 * @throws IllegalValueException
483
	 */
484
	public static function newFromArray( $data ) {
485
		self::requireArrayFields( $data, array( 'amount', 'unit', 'upperBound', 'lowerBound' ) );
486
487
		return new static(
488
			DecimalValue::newFromArray( $data['amount'] ),
489
			$data['unit'],
490
			DecimalValue::newFromArray( $data['upperBound'] ),
491
			DecimalValue::newFromArray( $data['lowerBound'] )
492
		);
493
	}
494
495
	/**
496
	 * @see Comparable::equals
497
	 *
498
	 * @since 0.1
499
	 *
500
	 * @param mixed $target
501
	 *
502
	 * @return bool
503
	 */
504
	public function equals( $target ) {
505
		if ( $this === $target ) {
506
			return true;
507
		}
508
509
		return $target instanceof self
510
			&& $this->toArray() === $target->toArray();
511
	}
512
513
}
514