Test Failed
Push — httpUnits ( 76c237...022822 )
by no
02:53 queued 40s
created

DecimalMath   C

Complexity

Total Complexity 58

Size/Duplication

Total Lines 469
Duplicated Lines 0 %

Coupling/Cohesion

Components 3
Dependencies 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 58
c 1
b 0
f 0
lcom 3
cbo 1
dl 0
loc 469
rs 6.3005

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 2
A getUseBC() 0 3 1
A product() 0 10 2
A sum() 0 10 2
A min() 0 12 3
A max() 0 12 3
A roundToDigit() 0 5 1
A roundToExponent() 0 6 1
A getPositionForExponent() 0 21 3
C roundDigits() 0 63 13
A bump() 0 5 1
C bumpDigits() 0 28 7
A slump() 0 5 1
D slumpDigits() 0 40 9
A stripLeadingZeros() 0 4 1
B shift() 0 24 4
A shiftLeft() 0 10 2
A shiftRight() 0 10 2

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
namespace DataValues;
4
5
use InvalidArgumentException;
6
7
/**
8
 * Class for performing basic arithmetic and other transformations
9
 * on DecimalValues.
10
 *
11
 * This uses the bcmath library if available. Otherwise, it falls back on
12
 * using floating point operations.
13
 *
14
 * @note: this is not a genuine decimal arithmetics implementation,
15
 * and should not be used for financial computations, physical simulations, etc.
16
 *
17
 * @see DecimalValue
18
 *
19
 * @since 0.1
20
 *
21
 * @license GPL-2.0+
22
 * @author Daniel Kinzler
23
 */
24
class DecimalMath {
25
26
	/**
27
	 * Whether to use the bcmath library.
28
	 *
29
	 * @var bool
30
	 */
31
	private $useBC;
32
33
	/**
34
	 * @param bool|null $useBC Whether to use the bcmath library. If null,
35
	 *        bcmath will automatically be used if available.
36
	 */
37
	public function __construct( $useBC = null ) {
38
		if ( $useBC === null ) {
39
			$useBC = function_exists( 'bcscale' );
40
		}
41
42
		$this->useBC = $useBC;
43
	}
44
45
	/**
46
	 * Whether this is using the bcmath library.
47
	 *
48
	 * @return bool
49
	 */
50
	public function getUseBC() {
51
		return $this->useBC;
52
	}
53
54
	/**
55
	 * @param DecimalValue $a
56
	 * @param DecimalValue $b
57
	 *
58
	 * @return DecimalValue
59
	 */
60
	public function product( DecimalValue $a, DecimalValue $b ) {
61
		if ( $this->useBC ) {
62
			$scale = strlen( $a->getFractionalPart() ) + strlen( $b->getFractionalPart() );
63
			$product = bcmul( $a->getValue(), $b->getValue(), $scale );
64
		} else {
65
			$product = $a->getValueFloat() * $b->getValueFloat();
66
		}
67
68
		return new DecimalValue( $product );
69
	}
70
71
	/**
72
	 * @param DecimalValue $a
73
	 * @param DecimalValue $b
74
	 *
75
	 * @return DecimalValue
76
	 */
77
	public function sum( DecimalValue $a, DecimalValue $b ) {
78
		if ( $this->useBC ) {
79
			$scale = max( strlen( $a->getFractionalPart() ), strlen( $b->getFractionalPart() ) );
80
			$sum = bcadd( $a->getValue(), $b->getValue(), $scale );
81
		} else {
82
			$sum = $a->getValueFloat() + $b->getValueFloat();
83
		}
84
85
		return new DecimalValue( $sum );
86
	}
87
88
	/**
89
	 * @param DecimalValue $a
90
	 * @param DecimalValue $b
91
	 *
92
	 * @return DecimalValue
93
	 */
94
	public function min( DecimalValue $a, DecimalValue $b ) {
95
		if ( $this->useBC ) {
96
			$scale = max( strlen( $a->getFractionalPart() ), strlen( $b->getFractionalPart() ) );
97
			$comp = bccomp( $a->getValue(), $b->getValue(), $scale );
98
			$min = $comp > 0 ? $b : $a;
99
		} else {
100
			$min = min( $a->getValueFloat(), $b->getValueFloat() );
101
			$min = new DecimalValue( $min );
102
		}
103
104
		return $min;
105
	}
106
107
	/**
108
	 * @param DecimalValue $a
109
	 * @param DecimalValue $b
110
	 *
111
	 * @return DecimalValue
112
	 */
113
	public function max( DecimalValue $a, DecimalValue $b ) {
114
		if ( $this->useBC ) {
115
			$scale = max( strlen( $a->getFractionalPart() ), strlen( $b->getFractionalPart() ) );
116
			$comp = bccomp( $a->getValue(), $b->getValue(), $scale );
117
			$max = $comp > 0 ? $a : $b;
118
		} else {
119
			$max = max( $a->getValueFloat(), $b->getValueFloat() );
120
			$max = new DecimalValue( $max );
121
		}
122
123
		return $max;
124
	}
125
126
	/**
127
	 * Returns the given value, with any insignificant digits removed or zeroed.
128
	 *
129
	 * Rounding is applied  using the "round half away from zero" rule (that is, +0.5 is
130
	 * rounded to +1 and -0.5 is rounded to -1).
131
	 *
132
	 * @since 0.1
133
	 *
134
	 * @param DecimalValue $decimal
135
	 * @param int $significantDigits The number of digits to retain, counting the decimal point,
136
	 *        but not counting the leading sign.
137
	 *
138
	 * @throws InvalidArgumentException
139
	 * @return DecimalValue
140
	 */
141
	public function roundToDigit( DecimalValue $decimal, $significantDigits ) {
142
		$value = $decimal->getValue();
143
		$rounded = $this->roundDigits( $value, $significantDigits );
144
		return new DecimalValue( $rounded );
145
	}
146
147
	/**
148
	 * Returns the given value, with any insignificant digits removed or zeroed.
149
	 *
150
	 * Rounding is applied  using the "round half away from zero" rule (that is, +0.5 is
151
	 * rounded to +1 and -0.5 is rounded to -1).
152
	 *
153
	 * @since 0.1
154
	 *
155
	 * @param DecimalValue $decimal
156
	 * @param int $significantExponent 	 The exponent of the last significant digit,
157
	 *        e.g. -1 for "keep the first digit after the decimal point", or 2 for
158
	 *        "zero the last two digits before the decimal point".
159
	 *
160
	 * @throws InvalidArgumentException
161
	 * @return DecimalValue
162
	 */
163
	public function roundToExponent( DecimalValue $decimal, $significantExponent ) {
164
		//NOTE: the number of digits to keep (without the leading sign)
165
		//      is the same as the exponent's offset (with the leaqding sign).
166
		$digits = $this->getPositionForExponent( $significantExponent, $decimal );
167
		return $this->roundToDigit( $decimal, $digits );
168
	}
169
170
	/**
171
	 * Returns the (zero based) position for the given exponent in
172
	 * the given decimal string, counting the decimal point and the leading sign.
173
	 *
174
	 * @example: the position of exponent 0 in "+10.03" is 2.
175
	 * @example: the position of exponent 1 in "+210.03" is 2.
176
	 * @example: the position of exponent -2 in "+1.037" is 4.
177
	 *
178
	 * @param int $exponent
179
	 * @param DecimalValue $decimal
180
	 *
181
	 * @return int
182
	 */
183
	public function getPositionForExponent( $exponent, DecimalValue $decimal ) {
184
		$decimal = $decimal->getValue();
185
186
		$pointPos = strpos( $decimal, '.' );
187
		if ( $pointPos === false ) {
188
			$pointPos = strlen( $decimal );
189
		}
190
191
		// account for leading sign
192
		$pointPos--;
193
194
		if ( $exponent < 0 ) {
195
			// account for decimal point
196
			$position = $pointPos + 1 - $exponent;
197
		} else {
198
			// make sure we don't remove more digits than are there
199
			$position = max( 0, $pointPos - $exponent );
200
		}
201
202
		return $position;
203
	}
204
205
	/**
206
	 * Returns the given value, with any insignificant digits removed or zeroed.
207
	 *
208
	 * Rounding is applied using the "round half away from zero" rule (that is, +0.5 is
209
	 * rounded to +1 and -0.5 is rounded to -1).
210
	 *
211
	 * @see round()
212
	 *
213
	 * @param string $value
214
	 * @param int $significantDigits
215
	 *
216
	 * @throws InvalidArgumentException if $significantDigits is smaller than 0
217
	 * @return string
218
	 */
219
	private function roundDigits( $value, $significantDigits ) {
220
		if ( !is_int( $significantDigits ) || $significantDigits < 0 ) {
221
			throw new InvalidArgumentException( '$significantDigits must be a non-negative integer' );
222
		}
223
224
		// keeping no digits results in zero.
225
		if ( $significantDigits === 0 ) {
226
			return '+0';
227
		}
228
229
		$len = strlen( $value );
230
231
		// keeping all digits means no rounding
232
		if ( $significantDigits >= $len - 1 ) {
233
			return $value;
234
		}
235
236
		// whether the last character is already part of the integer part of the decimal value
237
		$i = min( $significantDigits + 1, $len ); // account for the sign
238
		$ch = $i < $len ? $value[$i] : '0';
239
240
		if ( $ch === '.' ) {
241
			// NOTE: we expect the input to be well formed, so it cannot end with a '.'
242
			$i++;
243
			$ch = $i < $len ? $value[$i] : '0';
244
		}
245
246
		// split in significant and insignificant part
247
		$rounded = substr( $value, 0, $i );
248
249
		if ( strpos( $rounded, '.' ) === false ) {
250
			$suffix = substr( $value, $i );
251
252
			// strip insignificant digits after the decimal point
253
			$ppos = strpos( $suffix, '.' );
254
			if ( $ppos !== false ) {
255
				$suffix = substr( $suffix, 0, $ppos );
256
			}
257
258
			// zero out insignificant digits
259
			$suffix = strtr( $suffix, '123456789', '000000000' );
260
		} else {
261
			// decimal point is in $rounded, so $suffix is insignificant
262
			$suffix = '';
263
		}
264
265
		if ( $ch >= '5' ) {
266
			$rounded = $this->bumpDigits( $rounded );
267
		}
268
269
		$rounded .= $suffix;
270
271
		if ( $significantDigits > strlen( $rounded ) - 1 ) {
272
			if ( strpos( $rounded, '.' ) !== false ) {
273
				$rounded = str_pad( $rounded, $significantDigits + 1, '0', STR_PAD_RIGHT );
274
			}
275
		}
276
277
		// strip trailing decimal point
278
		$rounded = rtrim( $rounded, '.' );
279
280
		return $rounded;
281
	}
282
283
	/**
284
	 * Increment the least significant digit by one if it is less than 9, and
285
	 * set it to zero and continue to the next more significant digit if it is 9.
286
	 * Exception: bump( 0 ) == 1;
287
	 *
288
	 * E.g.: bump( 0.2 ) == 0.3, bump( -0.09 ) == -0.10, bump( 9.99 ) == 10.00
289
	 *
290
	 * This is the inverse of @see slump()
291
	 *
292
	 * @since 0.1
293
	 *
294
	 * @param DecimalValue $decimal
295
	 *
296
	 * @return DecimalValue
297
	 */
298
	public function bump( DecimalValue $decimal ) {
299
		$value = $decimal->getValue();
300
		$bumped = $this->bumpDigits( $value );
301
		return new DecimalValue( $bumped );
302
	}
303
304
	/**
305
	 * Increment the least significant digit by one if it is less than 9, and
306
	 * set it to zero and continue to the next more significant digit if it is 9.
307
	 *
308
	 * @see bump()
309
	 *
310
	 * @param string $value
311
	 * @return string
312
	 */
313
	private function bumpDigits( $value ) {
314
		if ( $value === '+0' ) {
315
			return '+1';
316
		}
317
318
		$bumped = '';
319
320
		for ( $i = strlen( $value ) - 1; $i >= 0; $i-- ) {
321
			$ch = $value[$i];
322
323
			if ( $ch === '.' ) {
324
				$bumped = '.' . $bumped;
325
				continue;
326
			} elseif ( $ch === '9' ) {
327
				$bumped = '0' . $bumped;
328
				continue;
329
			} elseif ( $ch === '+' || $ch === '-' ) {
330
				$bumped = $ch . '1' . $bumped;
331
				break;
332
			} else {
333
				$bumped = chr( ord( $ch ) + 1 ) . $bumped;
334
				break;
335
			}
336
		}
337
338
		$bumped = substr( $value, 0, $i ) . $bumped;
339
		return $bumped;
340
	}
341
342
	/**
343
	 * Decrement the least significant digit by one if it is more than 0, and
344
	 * set it to 9 and continue to the next more significant digit if it is 0.
345
	 * Exception: slump( 0 ) == -1;
346
	 *
347
	 * E.g.: slump( 0.2 ) == 0.1, slump( -0.10 ) == -0.01, slump( 0.0 ) == -1.0
348
	 *
349
	 * This is the inverse of @see bump()
350
	 *
351
	 * @since 0.1
352
	 *
353
	 * @param DecimalValue $decimal
354
	 *
355
	 * @return DecimalValue
356
	 */
357
	public function slump( DecimalValue $decimal ) {
358
		$value = $decimal->getValue();
359
		$slumped = $this->slumpDigits( $value );
360
		return new DecimalValue( $slumped );
361
	}
362
363
	/**
364
	 * Decrement the least significant digit by one if it is more than 0, and
365
	 * set it to 9 and continue to the next more significant digit if it is 0.
366
	 *
367
	 * @see slump()
368
	 *
369
	 * @param string $value
370
	 * @return string
371
	 */
372
	private function slumpDigits( $value ) {
373
		if ( $value === '+0' ) {
374
			return '-1';
375
		}
376
377
		// a "precise zero" will become negative
378
		if ( preg_match( '/^\+0\.(0*)0$/', $value, $m ) ) {
379
			return '-0.' . $m[1] . '1';
380
		}
381
382
		$slumped = '';
383
384
		for ( $i = strlen( $value ) - 1; $i >= 0; $i-- ) {
385
			$ch = substr( $value, $i, 1 );
386
387
			if ( $ch === '.' ) {
388
				$slumped = '.' . $slumped;
389
				continue;
390
			} elseif ( $ch === '0' ) {
391
				$slumped = '9' . $slumped;
392
				continue;
393
			} elseif ( $ch === '+' || $ch === '-' ) {
394
				$slumped = '0';
395
				break;
396
			} else {
397
				$slumped = chr( ord( $ch ) - 1 ) . $slumped;
398
				break;
399
			}
400
		}
401
402
		// preserve prefix
403
		$slumped = substr( $value, 0, $i ) . $slumped;
404
		$slumped = $this->stripLeadingZeros( $slumped );
405
406
		if ( $slumped === '-0' ) {
407
			$slumped = '+0';
408
		}
409
410
		return $slumped;
411
	}
412
413
	/**
414
	 * @param string $digits
415
	 *
416
	 * @return string
417
	 */
418
	private function stripLeadingZeros( $digits ) {
419
		$digits = preg_replace( '/^([-+])0+(?=\d)/', '\1', $digits );
420
		return $digits;
421
	}
422
423
	/**
424
	 * Shift the decimal point according to the given exponent.
425
	 *
426
	 * @param DecimalValue $decimal
427
	 * @param int $exponent The exponent to apply (digits to shift by). A Positive exponent
428
	 * shifts the decimal point to the right, a negative exponent shifts to the left.
429
	 *
430
	 * @throws InvalidArgumentException
431
	 * @return DecimalValue
432
	 */
433
	public function shift( DecimalValue $decimal, $exponent ) {
434
		if ( !is_int( $exponent ) ) {
435
			throw new InvalidArgumentException( '$exponent must be an integer' );
436
		}
437
438
		if ( $exponent == 0 ) {
439
			return $decimal;
440
		}
441
442
		$sign = $decimal->getSign();
443
		$intPart = $decimal->getIntegerPart();
444
		$fractPart = $decimal->getFractionalPart();
445
446
		if ( $exponent < 0 ) {
447
			$intPart = $this->shiftLeft( $intPart, $exponent );
448
		} else {
449
			$fractPart = $this->shiftRight( $fractPart, $exponent );
450
		}
451
452
		$digits = $sign . $intPart . $fractPart;
453
		$digits = $this->stripLeadingZeros( $digits );
454
455
		return new DecimalValue( $digits );
456
	}
457
458
	/**
459
	 * @param string $intPart
460
	 * @param int $exponent must be negative
461
	 *
462
	 * @return string
463
	 */
464
	private function shiftLeft( $intPart, $exponent ) {
465
		//note: $exponent is negative!
466
		if ( -$exponent < strlen( $intPart ) ) {
467
			$intPart = substr( $intPart, 0, $exponent ) . '.' . substr( $intPart, $exponent );
468
		} else {
469
			$intPart = '0.' . str_pad( $intPart, -$exponent, '0', STR_PAD_LEFT );
470
		}
471
472
		return $intPart;
473
	}
474
475
	/**
476
	 * @param string $fractPart
477
	 * @param int $exponent must be positive
478
	 *
479
	 * @return string
480
	 */
481
	private function shiftRight( $fractPart, $exponent ) {
482
		//note: $exponent is positive.
483
		if ( $exponent < strlen( $fractPart ) ) {
484
			$fractPart = substr( $fractPart, 0, $exponent ) . '.' . substr( $fractPart, $exponent );
485
		} else {
486
			$fractPart = str_pad( $fractPart, $exponent, '0', STR_PAD_RIGHT );
487
		}
488
489
		return $fractPart;
490
	}
491
492
}
493