Passed
Push — testTrailingNewlineRobustness ( 711559 )
by no
02:29
created

DecimalValue::convertToDecimal()   C

Complexity

Conditions 8
Paths 11

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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