Completed
Push — optional-bounds ( a6997b )
by Daniel
02:35
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
 * @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|null
38
	 */
39
	private $upperBound;
40
41
	/**
42
	 * The quantity's lower bound
43
	 *
44
	 * @var DecimalValue|null
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|null $upperBound The upper bound of the quantity, inclusive.
56
	 * @param DecimalValue|null $lowerBound The lower bound of the quantity, inclusive.
57
	 *
58
	 * @throws IllegalValueException
59
	 */
60
	public function __construct(
61
		DecimalValue $amount,
62
		$unit,
63
		DecimalValue $upperBound = null,
64
		DecimalValue $lowerBound = null
65
	) {
66
		if ( ( $lowerBound && !$upperBound ) || ( !$lowerBound && $upperBound ) ) {
67
			throw new IllegalValueException( 'Either both or no bounds should be defined.' );
68
		}
69
70
		if ( $lowerBound && $lowerBound->compare( $amount ) > 0 ) {
71
			throw new IllegalValueException( '$lowerBound ' . $lowerBound->getValue() . ' must be <= $amount ' . $amount->getValue() );
72
		}
73
74
		if ( $upperBound && $upperBound->compare( $amount ) < 0 ) {
75
			throw new IllegalValueException( '$upperBound ' . $upperBound->getValue() . ' must be >= $amount ' . $amount->getValue() );
76
		}
77
78
		if ( !is_string( $unit ) ) {
79
			throw new IllegalValueException( '$unit needs to be a string, not ' . gettype( $unit ) );
80
		}
81
82
		if ( $unit === '' ) {
83
			throw new IllegalValueException( '$unit can not be an empty string (use "1" for unit-less quantities)' );
84
		}
85
86
		$this->amount = $amount;
87
		$this->unit = $unit;
88
		$this->upperBound = $upperBound;
89
		$this->lowerBound = $lowerBound;
90
	}
91
92
	/**
93
	 * Returns a QuantityValue representing the given amount.
94
	 * If no upper or lower bound is given, the amount is assumed to be absolutely exact,
95
	 * that is, the amount itself will be used as the upper and lower bound.
96
	 *
97
	 * This is a convenience wrapper around the constructor that accepts native values
98
	 * instead of DecimalValue objects.
99
	 *
100
	 * @note: if the amount or a bound is given as a string, the string must conform
101
	 * to the rules defined by @see DecimalValue.
102
	 *
103
	 * @since 0.1
104
	 *
105
	 * @param string|int|float|DecimalValue $amount
106
	 * @param string $unit A unit identifier. Must not be empty, use "1" for unit-less quantities.
107
	 * @param string|int|float|DecimalValue|null $upperBound
108
	 * @param string|int|float|DecimalValue|null $lowerBound
109
	 *
110
	 * @return QuantityValue
111
	 * @throws IllegalValueException
112
	 */
113
	public static function newFromNumber( $amount, $unit = '1', $upperBound = null, $lowerBound = null ) {
114
		$amount = self::asDecimalValue( 'amount', $amount );
115
		$upperBound = ( $upperBound === null || $upperBound === '' )
116
			? null
117
			: self::asDecimalValue( 'upperBound', $upperBound );
118
119
		$lowerBound = ( $lowerBound === null || $lowerBound === '' )
120
			? null
121
			: self::asDecimalValue( 'lowerBound', $lowerBound );
122
123
		return new self( $amount, $unit, $upperBound, $lowerBound );
124
	}
125
126
	/**
127
	 * @see newFromNumber
128
	 *
129
	 * @deprecated since 0.1, use newFromNumber instead
130
	 *
131
	 * @param string|int|float|DecimalValue $amount
132
	 * @param string $unit
133
	 * @param string|int|float|DecimalValue|null $upperBound
134
	 * @param string|int|float|DecimalValue|null $lowerBound
135
	 *
136
	 * @return QuantityValue
137
	 */
138
	public static function newFromDecimal( $amount, $unit = '1', $upperBound = null, $lowerBound = null ) {
139
		return self::newFromNumber( $amount, $unit, $upperBound, $lowerBound );
140
	}
141
142
	/**
143
	 * Converts $number to a DecimalValue if possible and necessary.
144
	 *
145
	 * @note: if the $number is given as a string, it must conform to the rules
146
	 *        defined by @see DecimalValue.
147
	 *
148
	 * @param string $name The variable name to use in exception messages
149
	 * @param string|int|float|DecimalValue|null $number
150
	 *
151
	 * @throws IllegalValueException
152
	 * @throws InvalidArgumentException
153
	 * @return DecimalValue
154
	 */
155
	private static function asDecimalValue( $name, $number ) {
156
		if ( !is_string( $name ) ) {
157
			throw new InvalidArgumentException( '$name must be a string' );
158
		}
159
160
		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...
161
			// nothing to do
162
		} elseif ( is_int( $number ) || is_float( $number ) || is_string( $number ) ) {
163
			$number = new DecimalValue( $number );
164
		} else {
165
			throw new IllegalValueException( '$' . $name . '  must be a string, int, or float' );
166
		}
167
168
		return $number;
169
	}
170
171
	/**
172
	 * @see Serializable::serialize
173
	 *
174
	 * @since 0.1
175
	 *
176
	 * @return string
177
	 */
178
	public function serialize() {
179
		$fields = array(
180
			$this->amount,
181
			$this->unit
182
		);
183
184
		if ( $this->upperBound ) {
185
			$fields[] = $this->upperBound;
186
		}
187
188
		if ( $this->lowerBound ) {
189
			$fields[] = $this->lowerBound;
190
		}
191
192
		return serialize( $fields );
193
	}
194
195
	/**
196
	 * @see Serializable::unserialize
197
	 *
198
	 * @since 0.1
199
	 *
200
	 * @param string $data
201
	 */
202
	public function unserialize( $data ) {
203
		$fields = unserialize( $data );
204
		$amount = $fields[0];
205
		$unit = $fields[1];
206
207
		$upperBound = isset( $fields[2] ) ? $fields[2] : null;
208
		$lowerBound = isset( $fields[3] ) ? $fields[3] : null;
209
210
		$this->__construct( $amount, $unit, $upperBound, $lowerBound );
211
	}
212
213
	/**
214
	 * @see DataValue::getType
215
	 *
216
	 * @since 0.1
217
	 *
218
	 * @return string
219
	 */
220
	public static function getType() {
221
		return 'quantity';
222
	}
223
224
	/**
225
	 * Whether this Quantity has a known upper and lower bound.
226
	 * If this returns true, getUpperBound() and getLowerBound() will return a DecimalValue.
227
	 * If this returns false, getUpperBound() and getLowerBound() will return null.
228
	 *
229
	 * @return bool
230
	 */
231
	public function hasKnownBounds() {
232
		return $this->upperBound !== null && $this->lowerBound !== null;
233
	}
234
235
	/**
236
	 * @see DataValue::getSortKey
237
	 *
238
	 * @since 0.1
239
	 *
240
	 * @return float
241
	 */
242
	public function getSortKey() {
243
		return $this->getAmount()->getValueFloat();
244
	}
245
246
	/**
247
	 * Returns the quantity object.
248
	 * @see DataValue::getValue
249
	 *
250
	 * @since 0.1
251
	 *
252
	 * @return QuantityValue
253
	 */
254
	public function getValue() {
255
		return $this;
256
	}
257
258
	/**
259
	 * Returns the amount represented by this quantity.
260
	 *
261
	 * @since 0.1
262
	 *
263
	 * @return DecimalValue
264
	 */
265
	public function getAmount() {
266
		return $this->amount;
267
	}
268
269
	/**
270
	 * Returns this quantity's upper bound, or null if the bounds are not known.
271
	 *
272
	 * @since 0.1
273
	 *
274
	 * @return DecimalValue|null
275
	 */
276
	public function getUpperBound() {
277
		return $this->upperBound;
278
	}
279
280
	/**
281
	 * Returns this quantity's lower bound, or null if the bounds are not known.
282
	 *
283
	 * @since 0.1
284
	 *
285
	 * @return DecimalValue|null
286
	 */
287
	public function getLowerBound() {
288
		return $this->lowerBound;
289
	}
290
291
	/**
292
	 * Returns the size of the uncertainty interval.
293
	 * This can roughly be interpreted as "amount +/- uncertainty/2".
294
	 *
295
	 * The exact interpretation of the uncertainty interval is left to the concrete application or
296
	 * data point. For example, the uncertainty interval may be defined to be that part of a
297
	 * normal distribution that is required to cover the 95th percentile.
298
	 *
299
	 * @since 0.1
300
	 *
301
	 * @return float|bool The uncertainty magnitude, or false if the bounds for this Quantity
302
	 * are not known.
303
	 */
304
	public function getUncertainty() {
305
		if ( !$this->hasKnownBounds() ) {
306
			return false;
307
		}
308
309
		return $this->getUpperBound()->getValueFloat() - $this->getLowerBound()->getValueFloat();
310
	}
311
312
	/**
313
	 * Returns a DecimalValue representing the symmetrical offset to be applied
314
	 * to the raw amount for a rough representation of the uncertainty interval,
315
	 * as in "amount +/- offset".
316
	 *
317
	 * The offset is calculated as max( amount - lowerBound, upperBound - amount ).
318
	 *
319
	 * @since 0.1
320
	 *
321
	 * @return DecimalValue|null The size of the uncertainty margin, or null if the bounds
322
	 * for this Quantity are not known.
323
	 */
324
	public function getUncertaintyMargin() {
325
		if ( !$this->hasKnownBounds() ) {
326
			return null;
327
		}
328
329
		$math = new DecimalMath();
330
331
		$lowerMargin = $math->sum( $this->getAmount(), $this->getLowerBound()->computeComplement() );
332
		$upperMargin = $math->sum( $this->getUpperBound(), $this->getAmount()->computeComplement() );
0 ignored issues
show
Bug introduced by
It seems like $this->getUpperBound() can be null; however, sum() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
333
334
		$margin = $math->max( $lowerMargin, $upperMargin );
335
		return $margin;
336
	}
337
338
	/**
339
	 * Returns the order of magnitude of the uncertainty as the exponent of
340
	 * last significant digit in the amount-string. The value returned by this
341
	 * is suitable for use with @see DecimalMath::roundToExponent().
342
	 *
343
	 * @example: if two digits after the decimal point are significant, this
344
	 * returns -2.
345
	 *
346
	 * @example: if the last two digits before the decimal point are insignificant,
347
	 * this returns 2.
348
	 *
349
	 * Note that this calculation assumes a symmetric uncertainty interval,
350
	 * and can be misleading.
351
	 *
352
	 * A quantity without known bounds is treated as absolutely precise in this context.
353
	 *
354
	 * @since 0.1
355
	 *
356
	 * @return int The order of uncertainty
357
	 */
358
	public function getOrderOfUncertainty() {
359
		if ( !$this->hasKnownBounds() ) {
360
			$precision = 0.0;
361
		} else {
362
			// the desired precision is given by the distance between the amount and
363
			// whatever is closer, the upper or lower bound.
364
			//TODO: use DecimalMath to avoid floating point errors!
365
			$amount = $this->getAmount()->getValueFloat();
366
			$upperBound = $this->getUpperBound()->getValueFloat();
367
			$lowerBound = $this->getLowerBound()->getValueFloat();
368
			$precision = min( $amount - $lowerBound, $upperBound - $amount );
369
		}
370
371
		if ( $precision === 0.0 ) {
372
			// If there is no uncertainty, the order of uncertainty is a bit more than
373
			// what we have digits for.
374
			return -strlen( $this->amount->getFractionalPart() );
375
		}
376
377
		// e.g. +/- 200 -> 2; +/- 0.02 -> -2
378
		// note: we really want floor( log10( $precision ) ), but have to account for
379
		// small errors made in the floating point operations above.
380
		// @todo: use bcmath (via DecimalMath) to avoid this if possible
381
		$orderOfUncertainty = floor( log10( $precision + 0.0000000005 ) );
382
383
		return (int)$orderOfUncertainty;
384
	}
385
386
	/**
387
	 * Returns the number of significant figures in the amount-string,
388
	 * counting the decimal point, but not counting the leading sign.
389
	 *
390
	 * Note that this calculation assumes a symmetric uncertainty interval, and can be misleading
391
	 *
392
	 * @since 0.1
393
	 *
394
	 * @return int
395
	 */
396
	public function getSignificantFigures() {
397
		$math = new DecimalMath();
398
399
		// $orderOfUncertainty is +/- 200 -> 2; +/- 0.02 -> -2
400
		$orderOfUncertainty = $this->getOrderOfUncertainty();
401
402
		// the number of digits (without the sign) is the same as the position (with the sign).
403
		$significantDigits = $math->getPositionForExponent( $orderOfUncertainty, $this->amount );
404
405
		return $significantDigits;
406
	}
407
408
	/**
409
	 * Returns the unit held by this quantity.
410
	 * Unit-less quantities should use "1" as their unit.
411
	 *
412
	 * @since 0.1
413
	 *
414
	 * @return string
415
	 */
416
	public function getUnit() {
417
		return $this->unit;
418
	}
419
420
	/**
421
	 * Returns a transformed value derived from this QuantityValue by applying
422
	 * the given transformation to the amount and the upper and lower bounds.
423
	 * The resulting amount and bounds are rounded to the significant number of
424
	 * digits. Note that for exact quantities (with at least one bound equal to
425
	 * the amount), no rounding is applied (since they are considered to have
426
	 * infinite precision).
427
	 *
428
	 * The transformation is provided as a callback, which must implement a
429
	 * monotonously increasing, fully differentiable function mapping a DecimalValue
430
	 * to a DecimalValue. Typically, it will be a linear transformation applying a
431
	 * factor and an offset.
432
	 *
433
	 * @param string $newUnit The unit of the transformed quantity.
434
	 *
435
	 * @param callable $transformation A callback that implements the desired transformation.
436
	 *        The transformation will be called three times, once for the amount, once
437
	 *        for the lower bound, and once for the upper bound. It must return a DecimalValue.
438
	 *        The first parameter passed to $transformation is the DecimalValue to transform
439
	 *        In addition, any extra parameters passed to transform() will be passed through
440
	 *        to the transformation callback.
441
	 *
442
	 * @param mixed ... Any extra parameters will be passed to the $transformation function.
443
	 *
444
	 * @throws InvalidArgumentException
445
	 * @return QuantityValue
446
	 */
447
	public function transform( $newUnit, $transformation ) {
448
		if ( !is_callable( $transformation ) ) {
449
			throw new InvalidArgumentException( '$transformation must be callable.' );
450
		}
451
452
		if ( !is_string( $newUnit ) ) {
453
			throw new InvalidArgumentException( '$newUnit must be a string. Use "1" as the unit for unit-less quantities.' );
454
		}
455
456
		if ( $newUnit === '' ) {
457
			throw new InvalidArgumentException( '$newUnit must not be empty. Use "1" as the unit for unit-less quantities.' );
458
		}
459
460
		$oldUnit = $this->getUnit();
461
462
		if ( $newUnit === null ) {
463
			$newUnit = $oldUnit;
464
		}
465
466
		// Apply transformation by calling the $transform callback.
467
		// The first argument for the callback is the DataValue to transform. In addition,
468
		// any extra arguments given for transform() are passed through.
469
		$args = func_get_args();
470
		array_shift( $args );
471
472
		$args[0] = $this->getAmount();
473
		$amount = call_user_func_array( $transformation, $args );
474
475
		$args[0] = $this->getUpperBound();
476
		$upperBound = $args[0] === null ? null : call_user_func_array( $transformation, $args );
477
478
		$args[0] = $this->getLowerBound();
479
		$lowerBound = $args[0] === null ? null : call_user_func_array( $transformation, $args );
480
481
		// use a preliminary QuantityValue to determine the significant number of digits
482
		$transformed = new self( $amount, $newUnit, $upperBound, $lowerBound );
483
		$roundingExponent = $transformed->getOrderOfUncertainty();
484
485
		// apply rounding to the significant digits
486
		$math = new DecimalMath();
487
488
		$amount = $math->roundToExponent( $amount, $roundingExponent );
489
490
		if ( $upperBound && $lowerBound ) {
491
			// the constructor makes sure that either both bounds are set, or none.
492
			$upperBound = $math->roundToExponent( $upperBound, $roundingExponent );
493
			$lowerBound = $math->roundToExponent( $lowerBound, $roundingExponent );
494
		}
495
496
		return new self( $amount, $newUnit, $upperBound, $lowerBound );
497
	}
498
499
	public function __toString() {
500
		$unit = $this->getUnit();
0 ignored issues
show
Unused Code introduced by
$unit is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
501
502
		$s = $this->amount->getValue();
503
504
		if ( $this->hasKnownBounds() ) {
505
			$s .= '[' . $this->lowerBound->getValue() . '..' . $this->upperBound->getValue() . ']';
506
		}
507
508
		if ( $this->unit !== '1' ) {
509
			$s .=  $this->unit;
510
		}
511
512
		return $s;
513
	}
514
515
	/**
516
	 * @see DataValue::getArrayValue
517
	 *
518
	 * @since 0.1
519
	 *
520
	 * @return array
521
	 */
522
	public function getArrayValue() {
523
		$fields = array(
524
			'amount' => $this->amount->getArrayValue(),
525
			'unit' => $this->unit,
526
		);
527
528
529
		if ( $this->upperBound ) {
530
			$fields['upperBound'] = $this->upperBound->getArrayValue();
531
		}
532
533
		if ( $this->lowerBound ) {
534
			$fields['lowerBound'] = $this->lowerBound->getArrayValue();
535
		}
536
537
		return $fields;
538
	}
539
540
	/**
541
	 * Constructs a new instance of the DataValue from the provided data.
542
	 * This can round-trip with @see getArrayValue
543
	 *
544
	 * @since 0.1
545
	 *
546
	 * @param mixed $data
547
	 *
548
	 * @return QuantityValue
549
	 * @throws IllegalValueException
550
	 */
551
	public static function newFromArray( $data ) {
552
		self::requireArrayFields( $data, array( 'amount', 'unit' ) );
553
554
		$upper = isset( $data['upperBound'] )
555
			? DecimalValue::newFromArray( $data['upperBound'] )
556
			: null;
557
558
		$lower = isset( $data['lowerBound'] )
559
			? DecimalValue::newFromArray( $data['lowerBound'] )
560
			: null;
561
562
		return new static(
563
			DecimalValue::newFromArray( $data['amount'] ),
564
			$data['unit'],
565
			$upper,
566
			$lower
567
		);
568
	}
569
570
	/**
571
	 * @see Comparable::equals
572
	 *
573
	 * @since 0.1
574
	 *
575
	 * @param mixed $target
576
	 *
577
	 * @return bool
578
	 */
579
	public function equals( $target ) {
580
		if ( $this === $target ) {
581
			return true;
582
		}
583
584
		return $target instanceof self
585
			&& $this->toArray() === $target->toArray();
586
	}
587
588
}
589