Completed
Push — master ( 49a5fb...37c7f4 )
by Daniel
03:09 queued 02:50
created

DecimalValue::__construct()   D

Complexity

Conditions 10
Paths 17

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
c 4
b 0
f 0
dl 0
loc 25
rs 4.8196
cc 10
eloc 15
nc 17
nop 1

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace DataValues;
4
5
use InvalidArgumentException;
6
use LogicException;
7
8
/**
9
 * Class representing a decimal number with (nearly) arbitrary precision.
10
 *
11
 * For simple numeric values use @see NumberValue.
12
 *
13
 * The decimal notation for the value follows ISO 31-0, with some additional restrictions:
14
 * - the decimal separator is '.' (period). Comma is not used anywhere.
15
 * - no spacing or other separators are included for groups of digits.
16
 * - the first character in the string always gives the sign, either plus (+) or minus (-).
17
 * - scientific (exponential) notation is not used.
18
 * - the decimal point must not be the last character nor the fist character after the sign.
19
 * - no leading zeros, except one directly before the decimal point
20
 * - zero is always positive.
21
 *
22
 * These rules are enforced by @see QUANTITY_VALUE_PATTERN
23
 *
24
 * @since 0.1
25
 *
26
 * @license GPL-2.0+
27
 * @author Daniel Kinzler
28
 */
29
class DecimalValue extends DataValueObject {
30
31
	/**
32
	 * The $value as a decimal string, in the format described in the class
33
	 * level documentation of @see DecimalValue, matching @see QUANTITY_VALUE_PATTERN.
34
	 *
35
	 * @var string
36
	 */
37
	private $value;
38
39
	/**
40
	 * Regular expression for matching decimal strings that conform to the format
41
	 * described in the class level documentation of @see DecimalValue.
42
	 */
43
	const QUANTITY_VALUE_PATTERN = '/^[-+]([1-9]\d*|\d)(\.\d+)?\z/';
44
45
	/**
46
	 * Constructs a new DecimalValue object, representing the given value.
47
	 *
48
	 * @param string|int|float $value If given as a string, the value must match
49
	 *                         QUANTITY_VALUE_PATTERN.
50
	 *
51
	 * @throws IllegalValueException
52
	 */
53
	public function __construct( $value ) {
54
		if ( is_int( $value ) || is_float( $value ) ) {
55
			$value = $this->convertToDecimal( $value );
56
		} elseif ( !is_string( $value ) ) {
57
			throw new IllegalValueException( '$number must be a numeric string.' );
58
		}
59
60
		$value = trim( $value );
61
		if ( $value !== '' && $value[0] !== '-' && $value[0] !== '+' ) {
62
			$value = '+' . $value;
63
		}
64
		if ( strlen( $value ) > 127 ) {
65
			throw new IllegalValueException( 'Value must be at most 127 characters long.' );
66
		}
67
		if ( !preg_match( self::QUANTITY_VALUE_PATTERN, $value ) ) {
68
			throw new IllegalValueException( 'Value must match the pattern for decimal values.' );
69
		}
70
71
		$this->value = $value;
72
73
		// make "negative" zero positive
74
		if ( $this->isZero() ) {
75
			$this->value = '+' . substr( $this->value, 1 );
76
		}
77
	}
78
79
	/**
80
	 * Converts the given number to decimal notation. The resulting string conforms to the
81
	 * rules described in the class level documentation of @see DecimalValue and matches
82
	 * @see DecimalValue::QUANTITY_VALUE_PATTERN.
83
	 *
84
	 * @param int|float $number
85
	 *
86
	 * @return string
87
	 * @throws InvalidArgumentException
88
	 */
89
	private function convertToDecimal( $number ) {
90
		if ( $number === NAN || abs( $number ) === INF ) {
91
			throw new InvalidArgumentException( '$number must not be NAN or INF.' );
92
		}
93
94
		if ( is_int( $number ) || ( $number === floor( $number ) ) ) {
95
			$decimal = strval( abs( (int)$number ) );
96
		} else {
97
			$decimal = trim( number_format( abs( $number ), 100, '.', '' ), '0' );
98
99
			if ( $decimal[0] === '.' ) {
100
				$decimal = '0' . $decimal;
101
			}
102
103
			if ( substr( $decimal, -1 ) === '.' ) {
104
				$decimal .= '0';
105
			}
106
		}
107
108
		$decimal = ( ( $number >= 0.0 ) ? '+' : '-' ) . $decimal;
109
		return $decimal;
110
	}
111
112
	/**
113
	 * Compares this DecimalValue to another DecimalValue.
114
	 *
115
	 * @since 0.1
116
	 *
117
	 * @param self $that
118
	 *
119
	 * @throws LogicException
120
	 * @return int +1 if $this > $that, 0 if $this == $that, -1 if $this < $that
121
	 */
122
	public function compare( self $that ) {
123
		if ( $this === $that ) {
124
			return 0;
125
		}
126
127
		$a = $this->value;
128
		$b = $that->value;
129
130
		if ( $a === $b ) {
131
			return 0;
132
		}
133
134
		if ( $a[0] === '+' && $b[0] === '-' ) {
135
			return 1;
136
		}
137
138
		if ( $a[0] === '-' && $b[0] === '+' ) {
139
			return -1;
140
		}
141
142
		// compare the integer parts
143
		$aInt = ltrim( $this->getIntegerPart(), '0' );
144
		$bInt = ltrim( $that->getIntegerPart(), '0' );
145
146
		$sense = $a[0] === '+' ? 1 : -1;
147
148
		// per precondition, there are no leading zeros, so the longer nummber is greater
149
		if ( strlen( $aInt ) > strlen( $bInt ) ) {
150
			return $sense;
151
		}
152
153
		if ( strlen( $aInt ) < strlen( $bInt ) ) {
154
			return -$sense;
155
		}
156
157
		// if both have equal length, compare alphanumerically
158
		$cmp = strcmp( $aInt, $bInt );
159
		if ( $cmp > 0 ) {
160
			return $sense;
161
		}
162
163
		if ( $cmp < 0 ) {
164
			return -$sense;
165
		}
166
167
		// compare fractional parts
168
		$aFract = rtrim( $this->getFractionalPart(), '0' );
169
		$bFract = rtrim( $that->getFractionalPart(), '0' );
170
171
		// the fractional part is left-aligned, so just check alphanumeric ordering
172
		$cmp = strcmp( $aFract, $bFract );
173
		return  ( $cmp > 0 ? $sense : ( $cmp < 0 ? -$sense : 0 ) );
174
	}
175
176
	/**
177
	 * @see Serializable::serialize
178
	 *
179
	 * @return string
180
	 */
181
	public function serialize() {
182
		return serialize( $this->value );
183
	}
184
185
	/**
186
	 * @see Serializable::unserialize
187
	 *
188
	 * @param string $data
189
	 */
190
	public function unserialize( $data ) {
191
		$this->__construct( unserialize( $data ) );
192
	}
193
194
	/**
195
	 * @see DataValue::getType
196
	 *
197
	 * @return string
198
	 */
199
	public static function getType() {
200
		return 'decimal';
201
	}
202
203
	/**
204
	 * @see DataValue::getSortKey
205
	 *
206
	 * @return float
207
	 */
208
	public function getSortKey() {
209
		return $this->getValueFloat();
210
	}
211
212
	/**
213
	 * Returns the value as a decimal string, using the format described in the class level
214
	 * documentation of @see DecimalValue and matching @see DecimalValue::QUANTITY_VALUE_PATTERN.
215
	 * In particular, the string always starts with a sign (either '+' or '-')
216
	 * and has no leading zeros (except immediately before the decimal point). The decimal point is
217
	 * optional, but must not be the last character. Trailing zeros are significant.
218
	 *
219
	 * @see DataValue::getValue
220
	 *
221
	 * @return string
222
	 */
223
	public function getValue() {
224
		return $this->value;
225
	}
226
227
	/**
228
	 * Returns the sign of the amount (+ or -).
229
	 *
230
	 * @since 0.1
231
	 *
232
	 * @return string "+" or "-".
233
	 */
234
	public function getSign() {
235
		return substr( $this->value, 0, 1 );
236
	}
237
238
	/**
239
	 * Determines whether this DecimalValue is zero.
240
	 *
241
	 * @return bool
242
	 */
243
	public function isZero() {
244
		return (bool)preg_match( '/^[-+]0+(\.0+)?$/', $this->value );
245
	}
246
247
	/**
248
	 * Returns a new DecimalValue that represents the complement of this DecimalValue.
249
	 * That is, it constructs a new DecimalValue with the same digits as this,
250
	 * but with the sign inverted.
251
	 *
252
	 * Note that if isZero() returns true, this method returns this
253
	 * DecimalValue itself (because zero is it's own complement).
254
	 *
255
	 * @return self
256
	 */
257
	public function computeComplement() {
258
		if ( $this->isZero() ) {
259
			return $this;
260
		}
261
262
		$sign = $this->getSign();
263
		$invertedSign = ( $sign === '+' ? '-' : '+' );
264
265
		$inverseDigits = $invertedSign . substr( $this->value, 1 );
266
		return new self( $inverseDigits );
267
	}
268
269
	/**
270
	 * Returns a new DecimalValue that represents the absolute (positive) value
271
	 * of this DecimalValue. That is, it constructs a new DecimalValue with the
272
	 * same digits as this, but with the positive sign.
273
	 *
274
	 * Note that if getSign() returns "+", this method returns this
275
	 * DecimalValue itself (because a positive value is its own absolute value).
276
	 *
277
	 * @return self
278
	 */
279
	public function computeAbsolute() {
280
		if ( $this->getSign() === '+' ) {
281
			return $this;
282
		} else {
283
			return $this->computeComplement();
284
		}
285
	}
286
287
	/**
288
	 * Returns the integer part of the value, that is, the part before the decimal point,
289
	 * without the sign.
290
	 *
291
	 * @since 0.1
292
	 *
293
	 * @return string
294
	 */
295
	public function getIntegerPart() {
296
		$n = strpos( $this->value, '.' );
297
298
		if ( $n === false ) {
299
			$n = strlen( $this->value );
300
		}
301
302
		return substr( $this->value, 1, $n -1 );
303
	}
304
305
	/**
306
	 * Returns the fractional part of the value, that is, the part after the decimal point,
307
	 * if any.
308
	 *
309
	 * @since 0.1
310
	 *
311
	 * @return string
312
	 */
313
	public function getFractionalPart() {
314
		$n = strpos( $this->value, '.' );
315
316
		if ( $n === false ) {
317
			return '';
318
		}
319
320
		return substr( $this->value, $n + 1 );
321
	}
322
323
	/**
324
	 * Returns the value held by this object, as a float.
325
	 * Equivalent to floatval( $this->getvalue() ).
326
	 *
327
	 * @since 0.1
328
	 *
329
	 * @return float
330
	 */
331
	public function getValueFloat() {
332
		return floatval( $this->value );
333
	}
334
335
	/**
336
	 * @see DataValue::getArrayValue
337
	 *
338
	 * @return string
339
	 */
340
	public function getArrayValue() {
341
		return $this->value;
342
	}
343
344
	/**
345
	 * Constructs a new instance of the DataValue from the provided data.
346
	 * This can round-trip with @see getArrayValue
347
	 *
348
	 * @param string|int|float $data
349
	 *
350
	 * @return self
351
	 * @throws IllegalValueException
352
	 */
353
	public static function newFromArray( $data ) {
354
		return new static( $data );
355
	}
356
357
	/**
358
	 * @return string
359
	 */
360
	public function __toString() {
361
		return $this->value;
362
	}
363
364
}
365