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 |
||
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 | * @param int|float|string $number |
||
47 | * |
||
48 | * @return DecimalValue |
||
49 | */ |
||
50 | private function makeDecimalValue( $number ) { |
||
51 | |||
52 | if ( is_string( $number ) && $number !== '' ) { |
||
53 | if ( $number[0] !== '-' && $number[0] !== '+' ) { |
||
54 | $number = '+' . $number; |
||
55 | } |
||
56 | } |
||
57 | |||
58 | return new DecimalValue( $number ); |
||
59 | } |
||
60 | |||
61 | /** |
||
62 | * Whether this is using the bcmath library. |
||
63 | * |
||
64 | * @return bool |
||
65 | */ |
||
66 | public function getUseBC() { |
||
69 | |||
70 | /** |
||
71 | * Returns the product of the two values. |
||
72 | * |
||
73 | * @param DecimalValue $a |
||
74 | * @param DecimalValue $b |
||
75 | * |
||
76 | * @return DecimalValue |
||
77 | */ |
||
78 | public function product( DecimalValue $a, DecimalValue $b ) { |
||
79 | if ( $this->useBC ) { |
||
80 | $scale = strlen( $a->getFractionalPart() ) + strlen( $b->getFractionalPart() ); |
||
81 | $product = bcmul( $a->getValue(), $b->getValue(), $scale ); |
||
82 | } else { |
||
83 | $product = $a->getValueFloat() * $b->getValueFloat(); |
||
84 | } |
||
85 | |||
86 | return $this->makeDecimalValue( $product ); |
||
87 | } |
||
88 | |||
89 | /** |
||
90 | * Returns the sum of the two values. |
||
91 | * |
||
92 | * @param DecimalValue $a |
||
93 | * @param DecimalValue $b |
||
94 | * |
||
95 | * @return DecimalValue |
||
96 | */ |
||
97 | public function sum( DecimalValue $a, DecimalValue $b ) { |
||
98 | if ( $this->useBC ) { |
||
99 | $scale = max( strlen( $a->getFractionalPart() ), strlen( $b->getFractionalPart() ) ); |
||
100 | $sum = bcadd( $a->getValue(), $b->getValue(), $scale ); |
||
101 | } else { |
||
102 | $sum = $a->getValueFloat() + $b->getValueFloat(); |
||
103 | } |
||
104 | |||
105 | return $this->makeDecimalValue( $sum ); |
||
106 | } |
||
107 | |||
108 | /** |
||
109 | * Returns the minimum of the two values |
||
110 | * |
||
111 | * @param DecimalValue $a |
||
112 | * @param DecimalValue $b |
||
113 | * |
||
114 | * @return DecimalValue |
||
115 | */ |
||
116 | public function min( DecimalValue $a, DecimalValue $b ) { |
||
117 | |||
118 | if ( $this->useBC ) { |
||
119 | $scale = max( strlen( $a->getFractionalPart() ), strlen( $b->getFractionalPart() ) ); |
||
120 | $comp = bccomp( $a->getValue(), $b->getValue(), $scale ); |
||
121 | $min = $comp > 0 ? $b : $a; |
||
122 | } else { |
||
123 | $min = min( $a->getValueFloat(), $b->getValueFloat() ); |
||
124 | $min = $this->makeDecimalValue( $min ); |
||
125 | } |
||
126 | |||
127 | return $min; |
||
128 | } |
||
129 | |||
130 | /** |
||
131 | * Returns the maximum of the two values |
||
132 | * |
||
133 | * @param DecimalValue $a |
||
134 | * @param DecimalValue $b |
||
135 | * |
||
136 | * @return DecimalValue |
||
137 | */ |
||
138 | public function max( DecimalValue $a, DecimalValue $b ) { |
||
139 | |||
140 | if ( $this->useBC ) { |
||
141 | $scale = max( strlen( $a->getFractionalPart() ), strlen( $b->getFractionalPart() ) ); |
||
142 | $comp = bccomp( $a->getValue(), $b->getValue(), $scale ); |
||
143 | $max = $comp > 0 ? $a : $b; |
||
144 | } else { |
||
145 | $max = max( $a->getValueFloat(), $b->getValueFloat() ); |
||
146 | $max = $this->makeDecimalValue( $max ); |
||
147 | } |
||
148 | |||
149 | return $max; |
||
150 | } |
||
151 | |||
152 | /** |
||
153 | * Returns the given value, with any insignificant digits removed or zeroed. |
||
154 | * |
||
155 | * Rounding is applied using the "round half away from zero" rule (that is, +0.5 is |
||
156 | * rounded to +1 and -0.5 is rounded to -1). |
||
157 | * |
||
158 | * @since 0.1 |
||
159 | * |
||
160 | * @param DecimalValue $decimal |
||
161 | * @param int $significantDigits The number of digits to retain, counting the decimal point, |
||
162 | * but not counting the leading sign. |
||
163 | * |
||
164 | * @throws InvalidArgumentException |
||
165 | * @return DecimalValue |
||
166 | */ |
||
167 | public function roundToDigit( DecimalValue $decimal, $significantDigits ) { |
||
168 | $value = $decimal->getValue(); |
||
169 | $rounded = $this->roundDigits( $value, $significantDigits ); |
||
170 | return new DecimalValue( $rounded ); |
||
171 | } |
||
172 | |||
173 | /** |
||
174 | * Returns the given value, with any insignificant digits removed or zeroed. |
||
175 | * |
||
176 | * Rounding is applied using the "round half away from zero" rule (that is, +0.5 is |
||
177 | * rounded to +1 and -0.5 is rounded to -1). |
||
178 | * |
||
179 | * @since 0.1 |
||
180 | * |
||
181 | * @param DecimalValue $decimal |
||
182 | * @param int $significantExponent The exponent of the last significant digit, |
||
183 | * e.g. -1 for "keep the first digit after the decimal point", or 2 for |
||
184 | * "zero the last two digits before the decimal point". |
||
185 | * |
||
186 | * @throws InvalidArgumentException |
||
187 | * @return DecimalValue |
||
188 | */ |
||
189 | public function roundToExponent( DecimalValue $decimal, $significantExponent ) { |
||
190 | //NOTE: the number of digits to keep (without the leading sign) |
||
191 | // is the same as the exponent's offset (with the leaqding sign). |
||
192 | $digits = $this->getPositionForExponent( $significantExponent, $decimal ); |
||
193 | return $this->roundToDigit( $decimal, $digits ); |
||
194 | } |
||
195 | |||
196 | /** |
||
197 | * Returns the (zero based) position for the given exponent in |
||
198 | * the given decimal string, counting the decimal point and the leading sign. |
||
199 | * |
||
200 | * @example: the position of exponent 0 in "+10.03" is 2. |
||
201 | * @example: the position of exponent 1 in "+210.03" is 2. |
||
202 | * @example: the position of exponent -2 in "+1.037" is 4. |
||
203 | * |
||
204 | * @param int $exponent |
||
205 | * @param DecimalValue $decimal |
||
206 | * |
||
207 | * @return int |
||
208 | */ |
||
209 | public function getPositionForExponent( $exponent, DecimalValue $decimal ) { |
||
210 | $decimal = $decimal->getValue(); |
||
211 | |||
212 | $pointPos = strpos( $decimal, '.' ); |
||
213 | if ( $pointPos === false ) { |
||
214 | $pointPos = strlen( $decimal ); |
||
215 | } |
||
216 | |||
217 | // account for leading sign |
||
218 | $pointPos--; |
||
219 | |||
220 | if ( $exponent < 0 ) { |
||
221 | // account for decimal point |
||
222 | $position = $pointPos +1 - $exponent; |
||
223 | } else { |
||
224 | // make sure we don't remove more digits than are there |
||
225 | $position = max( 0, $pointPos - $exponent ); |
||
226 | } |
||
227 | |||
228 | return $position; |
||
229 | } |
||
230 | |||
231 | /** |
||
232 | * Returns the given value, with any insignificant digits removed or zeroed. |
||
233 | * |
||
234 | * Rounding is applied using the "round half away from zero" rule (that is, +0.5 is |
||
235 | * rounded to +1 and -0.5 is rounded to -1). |
||
236 | * |
||
237 | * @see round() |
||
238 | * |
||
239 | * @param string $value |
||
240 | * @param int $significantDigits |
||
241 | * |
||
242 | * @throws InvalidArgumentException if $significantDigits is smaller than 0 |
||
243 | * @return string |
||
244 | */ |
||
245 | private function roundDigits( $value, $significantDigits ) { |
||
246 | if ( !is_int( $significantDigits ) ) { |
||
247 | throw new InvalidArgumentException( '$significantDigits must be an integer' ); |
||
248 | } |
||
249 | |||
250 | // keeping no digits results in zero. |
||
251 | if ( $significantDigits === 0 ) { |
||
252 | return '+0'; |
||
253 | } |
||
254 | |||
255 | if ( $significantDigits < 0 ) { |
||
256 | throw new InvalidArgumentException( '$significantDigits must be larger than zero.' ); |
||
257 | } |
||
258 | |||
259 | // whether the last character is already part of the integer part of the decimal value |
||
260 | $inIntPart = ( strpos( $value, '.' ) === false ); |
||
261 | |||
262 | $rounded = ''; |
||
263 | |||
264 | // Iterate over characters from right to left and build the result back to front. |
||
265 | for ( $i = strlen( $value ) -1; $i > 0 && $i > $significantDigits; $i-- ) { |
||
266 | |||
267 | list( $value, $i, $inIntPart, $next ) = $this->roundNextDigit( $value, $i, $inIntPart ); |
||
268 | |||
269 | $rounded = $next . $rounded; |
||
270 | } |
||
271 | |||
272 | // just keep the remainder of the value as is (this includes the sign) |
||
273 | $rounded = substr( $value, 0, $i +1 ) . $rounded; |
||
274 | |||
275 | if ( strlen( $rounded ) < $significantDigits + 1 ) { |
||
276 | if ( $inIntPart ) { |
||
277 | $rounded .= '.'; |
||
278 | } |
||
279 | |||
280 | $rounded = str_pad( $rounded, $significantDigits+1, '0', STR_PAD_RIGHT ); |
||
281 | } |
||
282 | |||
283 | // strip trailing decimal point |
||
284 | $rounded = rtrim( $rounded, '.' ); |
||
285 | |||
286 | return $rounded; |
||
287 | } |
||
288 | |||
289 | /** |
||
290 | * Extracts the next character to add to the result of a rounding run: |
||
291 | * $value[$] will be examined and processed in order to determine the next |
||
292 | * character to prepend to the result (returned in the $nextCharacter field). |
||
293 | * |
||
294 | * Updated values for the parameters are returned as well as the next |
||
295 | * character. |
||
296 | * |
||
297 | * @param string $value |
||
298 | * @param int $i |
||
299 | * @param bool $inIntPart |
||
300 | * |
||
301 | * @return array ( $value, $i, $inIntPart, $nextCharacter ) |
||
302 | */ |
||
303 | private function roundNextDigit( $value, $i, $inIntPart ) { |
||
304 | // next digit |
||
305 | $ch = $value[$i]; |
||
306 | |||
307 | if ( $ch === '.' ) { |
||
308 | // just transition from the fractional to the integer part |
||
309 | $inIntPart = true; |
||
310 | $nextCharacter = '.'; |
||
311 | } else { |
||
312 | if ( $inIntPart ) { |
||
313 | // in the integer part, zero out insignificant digits |
||
314 | $nextCharacter = '0'; |
||
315 | } else { |
||
316 | // in the fractional part, strip insignificant digits |
||
317 | $nextCharacter = ''; |
||
318 | } |
||
319 | |||
320 | if ( ord( $ch ) >= ord( '5' ) ) { |
||
321 | // when stripping a character >= 5, bump up the next digit to the left. |
||
322 | list( $value, $i, $inIntPart ) = $this->bumpDigitsForRounding( $value, $i, $inIntPart ); |
||
323 | } |
||
324 | } |
||
325 | |||
326 | return array( $value, $i, $inIntPart, $nextCharacter ); |
||
327 | } |
||
328 | |||
329 | /** |
||
330 | * Bumps the last digit of a value that is being processed for rounding while taking |
||
331 | * care of edge cases and updating the state of the rounding process. |
||
332 | * |
||
333 | * - $value is truncated to $i digits, so we can safely increment (bump) the last digit. |
||
334 | * - if the last character of $value is '.', it's trimmed (and $inIntPart is set to true) |
||
335 | * to handle the transition from the fractional to the integer part of $value. |
||
336 | * - the last digit of $value is bumped using bumpDigits() - this is where the magic happens. |
||
337 | * - $i is set to strln( $value ) to make the index consistent in case a trailing decimal |
||
338 | * point got removed. |
||
339 | * |
||
340 | * Updated values for the parameters are returned. |
||
341 | * Note: when returning, $i is always one greater than the greatest valid index in $value. |
||
342 | * |
||
343 | * @param string $value |
||
344 | * @param int $i |
||
345 | * @param bool $inIntPart |
||
346 | * |
||
347 | * @return array ( $value, $i, $inIntPart, $next ) |
||
348 | */ |
||
349 | private function bumpDigitsForRounding( $value, $i, $inIntPart ) { |
||
350 | $remaining = substr( $value, 0, $i ); |
||
351 | |||
352 | // If there's a '.' at the end, strip it and note that we are in the |
||
353 | // integer part of $value now. |
||
354 | if ( $remaining[ strlen( $remaining ) -1 ] === '.' ) { |
||
355 | $remaining = rtrim( $remaining, '.' ); |
||
356 | $inIntPart = true; |
||
357 | } |
||
358 | |||
359 | // Rounding may add digits, adjust $i for that. |
||
360 | $value = $this->bumpDigits( $remaining ); |
||
361 | $i = strlen( $value ); |
||
362 | |||
363 | return array( $value, $i, $inIntPart ); |
||
364 | } |
||
365 | |||
366 | /** |
||
367 | * Increment the least significant digit by one if it is less than 9, and |
||
368 | * set it to zero and continue to the next more significant digit if it is 9. |
||
369 | * Exception: bump( 0 ) == 1; |
||
370 | * |
||
371 | * E.g.: bump( 0.2 ) == 0.3, bump( -0.09 ) == -0.10, bump( 9.99 ) == 10.00 |
||
372 | * |
||
373 | * This is the inverse of @see slump() |
||
374 | * |
||
375 | * @since 0.1 |
||
376 | * |
||
377 | * @param DecimalValue $decimal |
||
378 | * |
||
379 | * @return DecimalValue |
||
380 | */ |
||
381 | public function bump( DecimalValue $decimal ) { |
||
382 | $value = $decimal->getValue(); |
||
383 | $bumped = $this->bumpDigits( $value ); |
||
384 | return new DecimalValue( $bumped ); |
||
385 | } |
||
386 | |||
387 | /** |
||
388 | * Increment the least significant digit by one if it is less than 9, and |
||
389 | * set it to zero and continue to the next more significant digit if it is 9. |
||
390 | * |
||
391 | * @see bump() |
||
392 | * |
||
393 | * @param string $value |
||
394 | * @return string |
||
395 | */ |
||
396 | private function bumpDigits( $value ) { |
||
397 | if ( $value === '+0' ) { |
||
398 | return '+1'; |
||
399 | } |
||
400 | |||
401 | $bumped = ''; |
||
402 | |||
403 | for ( $i = strlen( $value ) -1; $i >= 0; $i-- ) { |
||
404 | $ch = $value[$i]; |
||
405 | |||
406 | if ( $ch === '.' ) { |
||
407 | $bumped = '.' . $bumped; |
||
408 | continue; |
||
409 | } elseif ( $ch === '9' ) { |
||
410 | $bumped = '0' . $bumped; |
||
411 | continue; |
||
412 | } elseif ( $ch === '+' || $ch === '-' ) { |
||
413 | $bumped = $ch . '1' . $bumped; |
||
414 | break; |
||
415 | } else { |
||
416 | $bumped = chr( ord( $ch ) + 1 ) . $bumped; |
||
417 | break; |
||
418 | } |
||
419 | } |
||
420 | |||
421 | $bumped = substr( $value, 0, $i ) . $bumped; |
||
422 | return $bumped; |
||
423 | } |
||
424 | |||
425 | /** |
||
426 | * Decrement the least significant digit by one if it is more than 0, and |
||
427 | * set it to 9 and continue to the next more significant digit if it is 0. |
||
428 | * Exception: slump( 0 ) == -1; |
||
429 | * |
||
430 | * E.g.: slump( 0.2 ) == 0.1, slump( -0.10 ) == -0.01, slump( 0.0 ) == -1.0 |
||
431 | * |
||
432 | * This is the inverse of @see bump() |
||
433 | * |
||
434 | * @since 0.1 |
||
435 | * |
||
436 | * @param DecimalValue $decimal |
||
437 | * |
||
438 | * @return DecimalValue |
||
439 | */ |
||
440 | public function slump( DecimalValue $decimal ) { |
||
441 | $value = $decimal->getValue(); |
||
442 | $slumped = $this->slumpDigits( $value ); |
||
443 | return new DecimalValue( $slumped ); |
||
444 | } |
||
445 | |||
446 | /** |
||
447 | * Decrement the least significant digit by one if it is more than 0, and |
||
448 | * set it to 9 and continue to the next more significant digit if it is 0. |
||
449 | * |
||
450 | * @see slump() |
||
451 | * |
||
452 | * @param string $value |
||
453 | * @return string |
||
454 | */ |
||
455 | private function slumpDigits( $value ) { |
||
456 | if ( $value === '+0' ) { |
||
457 | return '-1'; |
||
458 | } |
||
459 | |||
460 | // a "precise zero" will become negative |
||
461 | if ( preg_match( '/^\+0\.(0*)0$/', $value, $m ) ) { |
||
462 | return '-0.' . $m[1] . '1'; |
||
463 | } |
||
464 | |||
465 | $slumped = ''; |
||
466 | |||
467 | for ( $i = strlen( $value ) -1; $i >= 0; $i-- ) { |
||
468 | $ch = substr( $value, $i, 1 ); |
||
469 | |||
470 | if ( $ch === '.' ) { |
||
471 | $slumped = '.' . $slumped; |
||
472 | continue; |
||
473 | } elseif ( $ch === '0' ) { |
||
474 | $slumped = '9' . $slumped; |
||
475 | continue; |
||
476 | } elseif ( $ch === '+' || $ch === '-' ) { |
||
477 | $slumped = '0'; |
||
478 | break; |
||
479 | } else { |
||
480 | $slumped = chr( ord( $ch ) - 1 ) . $slumped; |
||
481 | break; |
||
482 | } |
||
483 | } |
||
484 | |||
485 | // preserve prefix |
||
486 | $slumped = substr( $value, 0, $i ) . $slumped; |
||
487 | |||
488 | $slumped = $this->stripLeadingZeros( $slumped ); |
||
489 | |||
490 | if ( $slumped === '-0' ) { |
||
491 | $slumped = '+0'; |
||
492 | } |
||
493 | |||
494 | return $slumped; |
||
495 | } |
||
496 | |||
497 | /** |
||
498 | * @param string $digits |
||
499 | * |
||
500 | * @return string |
||
501 | */ |
||
502 | private function stripLeadingZeros( $digits ) { |
||
506 | |||
507 | /** |
||
508 | * Shift the decimal point according to the given exponent. |
||
509 | * |
||
510 | * @param DecimalValue $decimal |
||
511 | * @param int $exponent The exponent to apply (digits to shift by). A Positive exponent |
||
512 | * shifts the decimal point to the right, a negative exponent shifts to the left. |
||
513 | * |
||
514 | * @throws InvalidArgumentException |
||
515 | * @return DecimalValue |
||
516 | */ |
||
517 | public function shift( DecimalValue $decimal, $exponent ) { |
||
518 | if ( !is_int( $exponent ) ) { |
||
519 | throw new InvalidArgumentException( '$exponent must be an integer' ); |
||
520 | } |
||
521 | |||
522 | if ( $exponent == 0 ) { |
||
541 | |||
542 | /** |
||
543 | * @param string $intPart |
||
544 | * @param int $exponent must be negative |
||
545 | * |
||
546 | * @return string |
||
547 | */ |
||
548 | private function shiftLeft( $intPart, $exponent ) { |
||
558 | |||
559 | /** |
||
560 | * @param string $fractPart |
||
561 | * @param int $exponent must be positive |
||
562 | * |
||
563 | * @return string |
||
564 | */ |
||
565 | private function shiftRight( $fractPart, $exponent ) { |
||
575 | |||
576 | } |
||
577 |