Passed
Push — newFromDecimal ( bb4d99 )
by no
05:42 queued 02:38
created

QuantityValue::newFromDecimal()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
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 simple numeric amounts use @see NumberValue.
12
 *
13
 * @since 0.1
14
 *
15
 * @license GPL-2.0+
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
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...
62
			throw new IllegalValueException( '$lowerBound ' . $lowerBound->getValue() . ' must be <= $amount ' . $amount->getValue() );
63
		}
64
65
		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...
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
	 * Converts $number to a DecimalValue if possible and necessary.
114
	 *
115
	 * @note: if the $number is given as a string, it must conform to the rules
116
	 *        defined by @see DecimalValue.
117
	 *
118
	 * @param string $name The variable name to use in exception messages
119
	 * @param string|int|float|DecimalValue|null $number
120
	 * @param DecimalValue|null $default
121
	 *
122
	 * @throws IllegalValueException
123
	 * @throws InvalidArgumentException
124
	 * @return DecimalValue
125
	 */
126
	private static function asDecimalValue( $name, $number, DecimalValue $default = null ) {
127
		if ( !is_string( $name ) ) {
128
			throw new InvalidArgumentException( '$name must be a string' );
129
		}
130
131
		if ( $number === null ) {
132
			if ( $default === null ) {
133
				throw new InvalidArgumentException( '$' . $name . ' must not be null' );
134
			}
135
136
			$number = $default;
137
		}
138
139
		if ( $number instanceof DecimalValue ) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
140
			// nothing to do
141
		} elseif ( is_int( $number ) || is_float( $number ) || is_string( $number ) ) {
142
			$number = new DecimalValue( $number );
143
		} else {
144
			throw new IllegalValueException( '$' . $name . '  must be a string, int, or float' );
145
		}
146
147
		return $number;
148
	}
149
150
	/**
151
	 * @see Serializable::serialize
152
	 *
153
	 * @since 0.1
154
	 *
155
	 * @return string
156
	 */
157
	public function serialize() {
158
		return serialize( array(
159
			$this->amount,
160
			$this->unit,
161
			$this->upperBound,
162
			$this->lowerBound,
163
		) );
164
	}
165
166
	/**
167
	 * @see Serializable::unserialize
168
	 *
169
	 * @since 0.1
170
	 *
171
	 * @param string $data
172
	 */
173
	public function unserialize( $data ) {
174
		list( $amount, $unit, $upperBound, $lowerBound ) = unserialize( $data );
175
		$this->__construct( $amount, $unit, $upperBound, $lowerBound );
176
	}
177
178
	/**
179
	 * @see DataValue::getType
180
	 *
181
	 * @since 0.1
182
	 *
183
	 * @return string
184
	 */
185
	public static function getType() {
186
		return 'quantity';
187
	}
188
189
	/**
190
	 * @see DataValue::getSortKey
191
	 *
192
	 * @since 0.1
193
	 *
194
	 * @return float
195
	 */
196
	public function getSortKey() {
197
		return $this->amount->getValueFloat();
198
	}
199
200
	/**
201
	 * Returns the quantity object.
202
	 * @see DataValue::getValue
203
	 *
204
	 * @since 0.1
205
	 *
206
	 * @return self
207
	 */
208
	public function getValue() {
209
		return $this;
210
	}
211
212
	/**
213
	 * Returns the amount represented by this quantity.
214
	 *
215
	 * @since 0.1
216
	 *
217
	 * @return DecimalValue
218
	 */
219
	public function getAmount() {
220
		return $this->amount;
221
	}
222
223
	/**
224
	 * Returns this quantity's upper bound.
225
	 *
226
	 * @since 0.1
227
	 *
228
	 * @return DecimalValue
229
	 */
230
	public function getUpperBound() {
231
		return $this->upperBound;
232
	}
233
234
	/**
235
	 * Returns this quantity's lower bound.
236
	 *
237
	 * @since 0.1
238
	 *
239
	 * @return DecimalValue
240
	 */
241
	public function getLowerBound() {
242
		return $this->lowerBound;
243
	}
244
245
	/**
246
	 * Returns the size of the uncertainty interval.
247
	 * This can roughly be interpreted as "amount +/- uncertainty/2".
248
	 *
249
	 * The exact interpretation of the uncertainty interval is left to the concrete application or
250
	 * data point. For example, the uncertainty interval may be defined to be that part of a
251
	 * normal distribution that is required to cover the 95th percentile.
252
	 *
253
	 * @since 0.1
254
	 *
255
	 * @return float
256
	 */
257
	public function getUncertainty() {
258
		return $this->upperBound->getValueFloat() - $this->lowerBound->getValueFloat();
259
	}
260
261
	/**
262
	 * Returns a DecimalValue representing the symmetrical offset to be applied
263
	 * to the raw amount for a rough representation of the uncertainty interval,
264
	 * as in "amount +/- offset".
265
	 *
266
	 * The offset is calculated as max( amount - lowerBound, upperBound - amount ).
267
	 *
268
	 * @since 0.1
269
	 *
270
	 * @return DecimalValue
271
	 */
272
	public function getUncertaintyMargin() {
273
		$math = new DecimalMath();
274
275
		$lowerMargin = $math->sum( $this->amount, $this->lowerBound->computeComplement() );
276
		$upperMargin = $math->sum( $this->upperBound, $this->amount->computeComplement() );
277
278
		$margin = $math->max( $lowerMargin, $upperMargin );
279
		return $margin;
280
	}
281
282
	/**
283
	 * Returns the order of magnitude of the uncertainty as the exponent of
284
	 * last significant digit in the amount-string. The value returned by this
285
	 * is suitable for use with @see DecimalMath::roundToExponent().
286
	 *
287
	 * @example: if two digits after the decimal point are significant, this
288
	 * returns -2.
289
	 *
290
	 * @example: if the last two digits before the decimal point are insignificant,
291
	 * this returns 2.
292
	 *
293
	 * Note that this calculation assumes a symmetric uncertainty interval,
294
	 * and can be misleading.
295
	 *
296
	 * @since 0.1
297
	 *
298
	 * @return int
299
	 */
300
	public function getOrderOfUncertainty() {
301
		// the desired precision is given by the distance between the amount and
302
		// whatever is closer, the upper or lower bound.
303
		//TODO: use DecimalMath to avoid floating point errors!
304
		$amount = $this->amount->getValueFloat();
305
		$upperBound = $this->upperBound->getValueFloat();
306
		$lowerBound = $this->lowerBound->getValueFloat();
307
		$precision = min( $amount - $lowerBound, $upperBound - $amount );
308
309
		if ( $precision === 0.0 ) {
310
			// If there is no uncertainty, the order of uncertainty is a bit more than what we have digits for.
311
			return -strlen( $this->amount->getFractionalPart() );
312
		}
313
314
		// e.g. +/- 200 -> 2; +/- 0.02 -> -2
315
		// note: we really want floor( log10( $precision ) ), but have to account for
316
		// small errors made in the floating point operations above.
317
		// @todo: use bcmath (via DecimalMath) to avoid this if possible
318
		$orderOfUncertainty = floor( log10( $precision + 0.0000000005 ) );
319
320
		return (int)$orderOfUncertainty;
321
	}
322
323
	/**
324
	 * Returns the number of significant figures in the amount-string,
325
	 * counting the decimal point, but not counting the leading sign.
326
	 *
327
	 * Note that this calculation assumes a symmetric uncertainty interval, and can be misleading
328
	 *
329
	 * @since 0.1
330
	 *
331
	 * @return int
332
	 */
333
	public function getSignificantFigures() {
334
		$math = new DecimalMath();
335
336
		// $orderOfUncertainty is +/- 200 -> 2; +/- 0.02 -> -2
337
		$orderOfUncertainty = $this->getOrderOfUncertainty();
338
339
		// the number of digits (without the sign) is the same as the position (with the sign).
340
		$significantDigits = $math->getPositionForExponent( $orderOfUncertainty, $this->amount );
341
342
		return $significantDigits;
343
	}
344
345
	/**
346
	 * Returns the unit held by this quantity.
347
	 * Unit-less quantities should use "1" as their unit.
348
	 *
349
	 * @since 0.1
350
	 *
351
	 * @return string
352
	 */
353
	public function getUnit() {
354
		return $this->unit;
355
	}
356
357
	/**
358
	 * Returns a transformed value derived from this QuantityValue by applying
359
	 * the given transformation to the amount and the upper and lower bounds.
360
	 * The resulting amount and bounds are rounded to the significant number of
361
	 * digits. Note that for exact quantities (with at least one bound equal to
362
	 * the amount), no rounding is applied (since they are considered to have
363
	 * infinite precision).
364
	 *
365
	 * The transformation is provided as a callback, which must implement a
366
	 * monotonously increasing, fully differentiable function mapping a DecimalValue
367
	 * to a DecimalValue. Typically, it will be a linear transformation applying a
368
	 * factor and an offset.
369
	 *
370
	 * @param string $newUnit The unit of the transformed quantity.
371
	 *
372
	 * @param callable $transformation A callback that implements the desired transformation.
373
	 *        The transformation will be called three times, once for the amount, once
374
	 *        for the lower bound, and once for the upper bound. It must return a DecimalValue.
375
	 *        The first parameter passed to $transformation is the DecimalValue to transform
376
	 *        In addition, any extra parameters passed to transform() will be passed through
377
	 *        to the transformation callback.
378
	 *
379
	 * @param mixed ... Any extra parameters will be passed to the $transformation function.
380
	 *
381
	 * @throws InvalidArgumentException
382
	 * @return self
383
	 */
384
	public function transform( $newUnit, $transformation ) {
385
		if ( !is_callable( $transformation ) ) {
386
			throw new InvalidArgumentException( '$transformation must be callable.' );
387
		}
388
389
		if ( !is_string( $newUnit ) ) {
390
			throw new InvalidArgumentException( '$newUnit must be a string. Use "1" as the unit for unit-less quantities.' );
391
		}
392
393
		if ( $newUnit === '' ) {
394
			throw new InvalidArgumentException( '$newUnit must not be empty. Use "1" as the unit for unit-less quantities.' );
395
		}
396
397
		$oldUnit = $this->unit;
398
399
		if ( $newUnit === null ) {
400
			$newUnit = $oldUnit;
401
		}
402
403
		// Apply transformation by calling the $transform callback.
404
		// The first argument for the callback is the DataValue to transform. In addition,
405
		// any extra arguments given for transform() are passed through.
406
		$args = func_get_args();
407
		array_shift( $args );
408
409
		$args[0] = $this->amount;
410
		$amount = call_user_func_array( $transformation, $args );
411
412
		$args[0] = $this->upperBound;
413
		$upperBound = call_user_func_array( $transformation, $args );
414
415
		$args[0] = $this->lowerBound;
416
		$lowerBound = call_user_func_array( $transformation, $args );
417
418
		// use a preliminary QuantityValue to determine the significant number of digits
419
		$transformed = new self( $amount, $newUnit, $upperBound, $lowerBound );
420
		$roundingExponent = $transformed->getOrderOfUncertainty();
421
422
		// apply rounding to the significant digits
423
		$math = new DecimalMath();
424
425
		$amount = $math->roundToExponent( $amount, $roundingExponent );
426
		$upperBound = $math->roundToExponent( $upperBound, $roundingExponent );
427
		$lowerBound = $math->roundToExponent( $lowerBound, $roundingExponent );
428
429
		return new self( $amount, $newUnit, $upperBound, $lowerBound );
430
	}
431
432
	public function __toString() {
433
		return $this->amount->getValue()
434
			. '[' . $this->lowerBound->getValue()
435
			. '..' . $this->upperBound->getValue()
436
			. ']'
437
			. ( $this->unit === '1' ? '' : $this->unit );
438
	}
439
440
	/**
441
	 * @see DataValue::getArrayValue
442
	 *
443
	 * @since 0.1
444
	 *
445
	 * @return array
446
	 */
447
	public function getArrayValue() {
448
		return array(
449
			'amount' => $this->amount->getArrayValue(),
450
			'unit' => $this->unit,
451
			'upperBound' => $this->upperBound->getArrayValue(),
452
			'lowerBound' => $this->lowerBound->getArrayValue(),
453
		);
454
	}
455
456
	/**
457
	 * Constructs a new instance of the DataValue from the provided data.
458
	 * This can round-trip with @see getArrayValue
459
	 *
460
	 * @since 0.1
461
	 *
462
	 * @param mixed $data
463
	 *
464
	 * @return self
465
	 * @throws IllegalValueException
466
	 */
467
	public static function newFromArray( $data ) {
468
		self::requireArrayFields( $data, array( 'amount', 'unit', 'upperBound', 'lowerBound' ) );
469
470
		return new static(
471
			DecimalValue::newFromArray( $data['amount'] ),
472
			$data['unit'],
473
			DecimalValue::newFromArray( $data['upperBound'] ),
474
			DecimalValue::newFromArray( $data['lowerBound'] )
475
		);
476
	}
477
478
	/**
479
	 * @see Comparable::equals
480
	 *
481
	 * @since 0.1
482
	 *
483
	 * @param mixed $target
484
	 *
485
	 * @return bool
486
	 */
487
	public function equals( $target ) {
488
		if ( $this === $target ) {
489
			return true;
490
		}
491
492
		return $target instanceof self
493
			&& $this->toArray() === $target->toArray();
494
	}
495
496
}
497