Passed
Push — nullBounds ( b0cbef...64701e )
by no
02:43
created

QuantityValue   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 523
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 8
Bugs 1 Features 0
Metric Value
wmc 57
c 8
b 1
f 0
lcom 1
cbo 4
dl 0
loc 523
rs 6.433

22 Methods

Rating   Name   Duplication   Size   Complexity  
C __construct() 0 31 8
A newFromNumber() 0 13 3
A newFromDecimal() 0 3 1
C asDecimalValue() 0 23 8
A serialize() 0 8 1
A unserialize() 0 4 1
A getType() 0 3 1
A getSortKey() 0 3 1
A getValue() 0 3 1
A getAmount() 0 3 1
A getUpperBound() 0 3 1
A getLowerBound() 0 3 1
A getUncertainty() 0 3 1
A getUncertaintyMargin() 0 13 3
B getOrderOfUncertainty() 0 26 4
A getSignificantFigures() 0 15 2
A getUnit() 0 3 1
B transform() 0 47 5
A __toString() 0 8 4
A getArrayValue() 0 8 3
A newFromArray() 0 10 3
A equals() 0 8 3

How to fix   Complexity   

Complex Class

Complex classes like QuantityValue often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use QuantityValue, and based on these observations, apply Extract Interface, too.

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