Passed
Push — master ( 050c5b...b0f6e4 )
by Marius
02:52
created

DecimalValue   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 358
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 0
Metric Value
wmc 51
lcom 1
cbo 2
dl 0
loc 358
rs 8.3206
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
D __construct() 0 25 10
B convertToDecimal() 0 29 5
C compare() 0 53 14
A serialize() 0 3 1
A unserialize() 0 3 1
A getType() 0 3 1
A getSortKey() 0 3 1
A getValue() 0 3 1
A getSign() 0 3 1
A isZero() 0 3 1
A computeComplement() 0 11 3
A computeAbsolute() 0 7 2
A getIntegerPart() 0 9 2
A getFractionalPart() 0 9 2
A getTrimmed() 0 10 2
A getValueFloat() 0 3 1
A getArrayValue() 0 3 1
A newFromArray() 0 3 1
A __toString() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like DecimalValue 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 DecimalValue, and based on these observations, apply Extract Interface, too.

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. The leading plus sign is optional.
50
	 *
51
	 * @throws InvalidArgumentException
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\" is not a well formed decimal value" );
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
		$decimal = strval( abs( $number ) );
95
		$decimal = preg_replace_callback(
96
			'/(\d*)\.(\d*)E([-+]\d+)/i',
97
			function ( $matches ) {
98
				list( , $before, $after, $exponent ) = $matches;
99
100
				// Fill with as many zeros as necessary, and move the decimal point
101
				if ( $exponent < 0 ) {
102
					$before = str_repeat( '0', -$exponent - strlen( $before ) + 1 ) . $before;
103
					$before = substr_replace( $before, '.', $exponent, 0 );
104
				} else {
105
					$after .= str_repeat( '0', $exponent - strlen( $after ) );
106
					$after = substr_replace( $after, '.', $exponent, 0 );
107
				}
108
109
				// Remove not needed ".0" or just "." from the end
110
				return $before . rtrim( rtrim( $after, '0' ), '.' );
111
			},
112
			$decimal,
113
			1
114
		);
115
116
		return ( $number < 0 ? '-' : '+' ) . $decimal;
117
	}
118
119
	/**
120
	 * Compares this DecimalValue to another DecimalValue.
121
	 *
122
	 * @param self $that
123
	 *
124
	 * @throws LogicException
125
	 * @return int +1 if $this > $that, 0 if $this == $that, -1 if $this < $that
126
	 */
127
	public function compare( self $that ) {
128
		if ( $this === $that ) {
129
			return 0;
130
		}
131
132
		$a = $this->value;
133
		$b = $that->value;
134
135
		if ( $a === $b ) {
136
			return 0;
137
		}
138
139
		if ( $a[0] === '+' && $b[0] === '-' ) {
140
			return 1;
141
		}
142
143
		if ( $a[0] === '-' && $b[0] === '+' ) {
144
			return -1;
145
		}
146
147
		// compare the integer parts
148
		$aInt = ltrim( $this->getIntegerPart(), '0' );
149
		$bInt = ltrim( $that->getIntegerPart(), '0' );
150
151
		$sense = $a[0] === '+' ? 1 : -1;
152
153
		// per precondition, there are no leading zeros, so the longer nummber is greater
154
		if ( strlen( $aInt ) > strlen( $bInt ) ) {
155
			return $sense;
156
		}
157
158
		if ( strlen( $aInt ) < strlen( $bInt ) ) {
159
			return -$sense;
160
		}
161
162
		// if both have equal length, compare alphanumerically
163
		$cmp = strcmp( $aInt, $bInt );
164
		if ( $cmp > 0 ) {
165
			return $sense;
166
		}
167
168
		if ( $cmp < 0 ) {
169
			return -$sense;
170
		}
171
172
		// compare fractional parts
173
		$aFract = rtrim( $this->getFractionalPart(), '0' );
174
		$bFract = rtrim( $that->getFractionalPart(), '0' );
175
176
		// the fractional part is left-aligned, so just check alphanumeric ordering
177
		$cmp = strcmp( $aFract, $bFract );
178
		return $cmp === 0 ? 0 : ( $cmp < 0 ? -$sense : $sense );
179
	}
180
181
	/**
182
	 * @see Serializable::serialize
183
	 *
184
	 * @return string
185
	 */
186
	public function serialize() {
187
		return serialize( $this->value );
188
	}
189
190
	/**
191
	 * @see Serializable::unserialize
192
	 *
193
	 * @param string $data
194
	 */
195
	public function unserialize( $data ) {
196
		$this->__construct( unserialize( $data ) );
197
	}
198
199
	/**
200
	 * @see DataValue::getType
201
	 *
202
	 * @return string
203
	 */
204
	public static function getType() {
205
		return 'decimal';
206
	}
207
208
	/**
209
	 * @see DataValue::getSortKey
210
	 *
211
	 * @return float
212
	 */
213
	public function getSortKey() {
214
		return $this->getValueFloat();
215
	}
216
217
	/**
218
	 * Returns the value as a decimal string, using the format described in the class level
219
	 * documentation of @see DecimalValue and matching @see DecimalValue::QUANTITY_VALUE_PATTERN.
220
	 * In particular, the string always starts with a sign (either '+' or '-')
221
	 * and has no leading zeros (except immediately before the decimal point). The decimal point is
222
	 * optional, but must not be the last character. Trailing zeros are significant.
223
	 *
224
	 * @see DataValue::getValue
225
	 *
226
	 * @return string
227
	 */
228
	public function getValue() {
229
		return $this->value;
230
	}
231
232
	/**
233
	 * Returns the sign of the amount (+ or -).
234
	 *
235
	 * @return string "+" or "-".
236
	 */
237
	public function getSign() {
238
		return substr( $this->value, 0, 1 );
239
	}
240
241
	/**
242
	 * Determines whether this DecimalValue is zero.
243
	 *
244
	 * @return bool
245
	 */
246
	public function isZero() {
247
		return (bool)preg_match( '/^[-+]0+(\.0+)?$/', $this->value );
248
	}
249
250
	/**
251
	 * Returns a new DecimalValue that represents the complement of this DecimalValue.
252
	 * That is, it constructs a new DecimalValue with the same digits as this,
253
	 * but with the sign inverted.
254
	 *
255
	 * Note that if isZero() returns true, this method returns this
256
	 * DecimalValue itself (because zero is it's own complement).
257
	 *
258
	 * @return self
259
	 */
260
	public function computeComplement() {
261
		if ( $this->isZero() ) {
262
			return $this;
263
		}
264
265
		$sign = $this->getSign();
266
		$invertedSign = ( $sign === '+' ? '-' : '+' );
267
268
		$inverseDigits = $invertedSign . substr( $this->value, 1 );
269
		return new self( $inverseDigits );
270
	}
271
272
	/**
273
	 * Returns a new DecimalValue that represents the absolute (positive) value
274
	 * of this DecimalValue. That is, it constructs a new DecimalValue with the
275
	 * same digits as this, but with the positive sign.
276
	 *
277
	 * Note that if getSign() returns "+", this method returns this
278
	 * DecimalValue itself (because a positive value is its own absolute value).
279
	 *
280
	 * @return self
281
	 */
282
	public function computeAbsolute() {
283
		if ( $this->getSign() === '+' ) {
284
			return $this;
285
		}
286
287
		return $this->computeComplement();
288
	}
289
290
	/**
291
	 * Returns the integer part of the value, that is, the part before the decimal point,
292
	 * without the sign.
293
	 *
294
	 * @return string
295
	 */
296
	public function getIntegerPart() {
297
		$n = strpos( $this->value, '.' );
298
299
		if ( $n === false ) {
300
			$n = strlen( $this->value );
301
		}
302
303
		return substr( $this->value, 1, $n - 1 );
304
	}
305
306
	/**
307
	 * Returns the fractional part of the value, that is, the part after the decimal point,
308
	 * if any.
309
	 *
310
	 * @return string
311
	 */
312
	public function getFractionalPart() {
313
		$n = strpos( $this->value, '.' );
314
315
		if ( $n === false ) {
316
			return '';
317
		}
318
319
		return substr( $this->value, $n + 1 );
320
	}
321
322
	/**
323
	 * Returns a DecimalValue with the same digits as this one, but with any trailing zeros
324
	 * after the decimal point removed. If there are no trailing zeros after the decimal
325
	 * point, this method will return $this.
326
	 *
327
	 * @return self
328
	 */
329
	public function getTrimmed() {
330
		$trimmed = preg_replace( '/(\.\d+?)0+$/', '$1', $this->value );
331
		$trimmed = preg_replace( '/(?<=\d)\.0*$/', '', $trimmed );
332
333
		if ( $trimmed === $this->value ) {
334
			return $this;
335
		}
336
337
		return new self( $trimmed );
338
	}
339
340
	/**
341
	 * Returns the value held by this object, as a float.
342
	 * Equivalent to floatval( $this->getvalue() ).
343
	 *
344
	 * @return float
345
	 */
346
	public function getValueFloat() {
347
		return floatval( $this->value );
348
	}
349
350
	/**
351
	 * @see DataValue::getArrayValue
352
	 *
353
	 * @return string
354
	 */
355
	public function getArrayValue() {
356
		return $this->value;
357
	}
358
359
	/**
360
	 * Constructs a new instance from the provided data. Required for @see DataValueDeserializer.
361
	 * This is expected to round-trip with @see getArrayValue.
362
	 *
363
	 * @deprecated since 0.8.3. Static DataValue::newFromArray constructors like this are
364
	 *  underspecified (not in the DataValue interface), and misleadingly named (should be named
365
	 *  newFromArrayValue). Instead, use DataValue builder callbacks in @see DataValueDeserializer.
366
	 *
367
	 * @param mixed $data Warning! Even if this is expected to be a value as returned by
368
	 *  @see getArrayValue, callers of this specific newFromArray implementation can not guarantee
369
	 *  this. This is not guaranteed to be a string!
370
	 *
371
	 * @throws InvalidArgumentException if $data is not in the expected format. Subclasses of
372
	 *  InvalidArgumentException are expected and properly handled by @see DataValueDeserializer.
373
	 * @return self
374
	 */
375
	public static function newFromArray( $data ) {
376
		return new static( $data );
377
	}
378
379
	/**
380
	 * @return string
381
	 */
382
	public function __toString() {
383
		return $this->value;
384
	}
385
386
}
387