Completed
Pull Request — master (#66)
by Daniel
06:11 queued 03:53
created

UnboundedQuantityValue::getUnit()   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 0
1
<?php
2
3
namespace DataValues;
4
5
use InvalidArgumentException;
6
use LogicException;
7
8
/**
9
 * Class representing a quantity with associated unit.
10
 * The amount is stored as a @see DecimalValue object.
11
 *
12
 * @see QuantityValue for quantities with a known uncertainty interval.
13
 * For simple numeric amounts use @see NumberValue.
14
 *
15
 * @note UnboundedQuantityValue and QuantityValue both use the value type ID "quantity".
16
 * The fact that we use subclassing to model the bounded vs the unbounded case should be
17
 * considered an implementation detail.
18
 *
19
 * @since 0.7
20
 *
21
 * @license GPL-2.0+
22
 * @author Daniel Kinzler
23
 */
24
class UnboundedQuantityValue extends DataValueObject {
25
26
	/**
27
	 * The quantity's amount
28
	 *
29
	 * @var DecimalValue
30
	 */
31
	protected $amount;
32
33
	/**
34
	 * The quantity's unit identifier (use "1" for unitless quantities).
35
	 *
36
	 * @var string
37
	 */
38
	protected $unit;
39
40
	/**
41
	 * @param DecimalValue $amount
42
	 * @param string $unit A unit identifier. Must not be empty, use "1" for unit-less quantities.
43
	 *
44
	 * @throws IllegalValueException
45
	 */
46
	public function __construct( DecimalValue $amount, $unit ) {
47
		if ( !is_string( $unit ) ) {
48
			throw new IllegalValueException( '$unit needs to be a string, not ' . gettype( $unit ) );
49
		}
50
51
		if ( $unit === '' ) {
52
			throw new IllegalValueException( '$unit can not be an empty string (use "1" for unit-less quantities)' );
53
		}
54
55
		$this->amount = $amount;
56
		$this->unit = $unit;
57
	}
58
59
	/**
60
	 * Returns a QuantityValue representing the given amount.
61
	 *
62
	 * This is a convenience wrapper around the constructor that accepts native values
63
	 * instead of DecimalValue objects.
64
	 *
65
	 * @note: if the amount or a bound is given as a string, the string must conform
66
	 * to the rules defined by @see DecimalValue.
67
	 *
68
	 * @since 0.7
69
	 *
70
	 * @param string|int|float|DecimalValue $number
71
	 * @param string $unit A unit identifier. Must not be empty, use "1" for unit-less quantities.
72
	 *
73
	 * @return self
74
	 * @throws IllegalValueException
75
	 */
76
	public static function newFromNumber( $number, $unit = '1' ) {
77
		$number = self::asDecimalValue( 'amount', $number );
78
79
		return new self( $number, $unit );
80
	}
81
82
	/**
83
	 * Converts $number to a DecimalValue if possible and necessary.
84
	 *
85
	 * @note: if the $number is given as a string, it must conform to the rules
86
	 *        defined by @see DecimalValue.
87
	 *
88
	 * @param string $name The variable name to use in exception messages
89
	 * @param string|int|float|DecimalValue|UnboundedQuantityValue|null $number
90
	 * @param DecimalValue|null $default
91
	 *
92
	 * @throws IllegalValueException
93
	 * @throws InvalidArgumentException
94
	 * @return DecimalValue
95
	 */
96
	protected static function asDecimalValue( $name, $number, DecimalValue $default = null ) {
97
		if ( !is_string( $name ) ) {
98
			throw new InvalidArgumentException( '$name must be a string' );
99
		}
100
101
		if ( $number === null ) {
102
			if ( $default === null ) {
103
				throw new InvalidArgumentException( '$' . $name . ' must not be null' );
104
			}
105
106
			$number = $default;
107
		}
108
109
		if ( $number instanceof UnboundedQuantityValue ) {
110
			return $number->getAmount();
111
		} elseif ( $number instanceof DecimalValue ) {
0 ignored issues
show
Unused Code introduced by
This elseif statement is empty, and could be removed.

This check looks for the bodies of elseif 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 elseif bodies can be removed. If you have an empty elseif but statements in the else branch, consider inverting the condition.

Loading history...
112
			// nothing to do
113
		} elseif ( is_int( $number ) || is_float( $number ) || is_string( $number ) ) {
114
			$number = new DecimalValue( $number );
115
		} else {
116
			throw new IllegalValueException( '$' . $name . '  must be a string, int, or float' );
117
		}
118
119
		return $number;
120
	}
121
122
	/**
123
	 * @see Serializable::serialize
124
	 *
125
	 * @since 0.7
126
	 *
127
	 * @return string
128
	 */
129
	public function serialize() {
130
		return serialize( array(
131
			$this->amount,
132
			$this->unit
133
		) );
134
	}
135
136
	/**
137
	 * @see Serializable::unserialize
138
	 *
139
	 * @since 0.7
140
	 *
141
	 * @param string $data
142
	 */
143
	public function unserialize( $data ) {
144
		list( $amount, $unit ) = unserialize( $data );
145
		$this->__construct( $amount, $unit );
146
	}
147
148
	/**
149
	 * @see DataValue::getType
150
	 *
151
	 * @since 0.7
152
	 *
153
	 * @return string
154
	 */
155
	public static function getType() {
156
		return 'quantity';
157
	}
158
159
	/**
160
	 * @see DataValue::getSortKey
161
	 *
162
	 * @since 0.7
163
	 *
164
	 * @return float
165
	 */
166
	public function getSortKey() {
167
		return $this->amount->getValueFloat();
168
	}
169
170
	/**
171
	 * Returns the quantity object.
172
	 * @see DataValue::getValue
173
	 *
174
	 * @since 0.7
175
	 *
176
	 * @return self
177
	 */
178
	public function getValue() {
179
		return $this;
180
	}
181
182
	/**
183
	 * Returns the amount represented by this quantity.
184
	 *
185
	 * @since 0.7
186
	 *
187
	 * @return DecimalValue
188
	 */
189
	public function getAmount() {
190
		return $this->amount;
191
	}
192
193
	/**
194
	 * Returns the unit held by this quantity.
195
	 * Unit-less quantities should use "1" as their unit.
196
	 *
197
	 * @since 0.7
198
	 *
199
	 * @return string
200
	 */
201
	public function getUnit() {
202
		return $this->unit;
203
	}
204
205
	/**
206
	 * Returns a transformed value derived from this QuantityValue by applying
207
	 * the given transformation to the amount and the upper and lower bounds.
208
	 * The resulting amount and bounds are rounded to the significant number of
209
	 * digits. Note that for exact quantities (with at least one bound equal to
210
	 * the amount), no rounding is applied (since they are considered to have
211
	 * infinite precision).
212
	 *
213
	 * The transformation is provided as a callback, which must implement a
214
	 * monotonously increasing, fully differentiable function mapping a DecimalValue
215
	 * to a DecimalValue. Typically, it will be a linear transformation applying a
216
	 * factor and an offset.
217
	 *
218
	 * @param string $newUnit The unit of the transformed quantity.
219
	 *
220
	 * @param callable $transformation A callback that implements the desired transformation.
221
	 *        The transformation will be called three times, once for the amount, once
222
	 *        for the lower bound, and once for the upper bound. It must return a DecimalValue.
223
	 *        The first parameter passed to $transformation is the DecimalValue to transform
224
	 *        In addition, any extra parameters passed to transform() will be passed through
225
	 *        to the transformation callback.
226
	 *
227
	 * @param mixed ... Any extra parameters will be passed to the $transformation function.
228
	 *
229
	 * @todo Should be factored out into a separate QuantityMath class.
230
	 *
231
	 * @throws InvalidArgumentException
232
	 * @return UnboundedQuantityValue
233
	 */
234
	public function transform( $newUnit, $transformation ) {
235
		if ( !is_callable( $transformation ) ) {
236
			throw new InvalidArgumentException( '$transformation must be callable.' );
237
		}
238
239
		if ( !is_string( $newUnit ) ) {
240
			throw new InvalidArgumentException( '$newUnit must be a string. Use "1" as the unit for unit-less quantities.' );
241
		}
242
243
		if ( $newUnit === '' ) {
244
			throw new InvalidArgumentException( '$newUnit must not be empty. Use "1" as the unit for unit-less quantities.' );
245
		}
246
247
		$oldUnit = $this->unit;
248
249
		if ( $newUnit === null ) {
250
			$newUnit = $oldUnit;
251
		}
252
253
		// Apply transformation by calling the $transform callback.
254
		// The first argument for the callback is the DataValue to transform. In addition,
255
		// any extra arguments given for transform() are passed through.
256
		$args = func_get_args();
257
		array_shift( $args );
258
259
		$args[0] = $this->amount;
260
		$amount = call_user_func_array( $transformation, $args );
261
262
		// use a preliminary QuantityValue to determine the significant number of digits
263
		return new self( $amount, $newUnit );
264
	}
265
266
	public function __toString() {
267
		return $this->amount->getValue()
268
			. ( $this->unit === '1' ? '' : $this->unit );
269
	}
270
271
	/**
272
	 * @see DataValue::getArrayValue
273
	 *
274
	 * @since 0.7
275
	 *
276
	 * @return array
277
	 */
278
	public function getArrayValue() {
279
		return array(
280
			'amount' => $this->amount->getArrayValue(),
281
			'unit' => $this->unit,
282
		);
283
	}
284
285
	/**
286
	 * Constructs a new instance of the DataValue from the provided data.
287
	 * This can round-trip with @see getArrayValue
288
	 *
289
	 * @since 0.7
290
	 *
291
	 * @param mixed $data
292
	 *
293
	 * @return UnboundedQuantityValue
294
	 * @throws IllegalValueException
295
	 */
296
	public static function newFromArray( $data ) {
297
		self::requireArrayFields( $data, array( 'amount', 'unit' ) );
298
299
		return new static(
300
			DecimalValue::newFromArray( $data['amount'] ),
301
			$data['unit']
302
		);
303
	}
304
305
	/**
306
	 * @see Comparable::equals
307
	 *
308
	 * @since 0.7
309
	 *
310
	 * @param mixed $target
311
	 *
312
	 * @return bool
313
	 */
314
	public function equals( $target ) {
315
		if ( $this === $target ) {
316
			return true;
317
		}
318
319
		return $target instanceof self
320
			&& $this->toArray() === $target->toArray();
321
	}
322
323
}
324