Passed
Pull Request — master (#134)
by Jordan
06:42
created

RoundingProvider::round()   B

Complexity

Conditions 9
Paths 20

Size

Total Lines 75
Code Lines 55

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 34
CRAP Score 9

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 9
eloc 55
c 4
b 0
f 0
nc 20
nop 2
dl 0
loc 75
ccs 34
cts 34
cp 1
crap 9
rs 7.4262

How to fix   Long Method   

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 Samsara\Fermat\Provider;
4
5
use JetBrains\PhpStorm\ExpectedValues;
6
use JetBrains\PhpStorm\Pure;
7
use Samsara\Fermat\Enums\RandomMode;
8
use Samsara\Fermat\Enums\RoundingMode;
9
use Samsara\Fermat\Types\Base\Interfaces\Numbers\DecimalInterface;
10
11
/**
12
 *
13
 */
14
class RoundingProvider
15
{
16
17
    private static RoundingMode $mode = RoundingMode::HalfEven;
18
    private static bool $isNegative = false;
19
    private static int $alt = 1;
20
    private static ?string $remainder;
21
22
    /**
23
     * @param RoundingMode $mode
24
     * @return void
25
     */
26 447
    public static function setRoundingMode(
27
        RoundingMode $mode
28
    ): void
29
    {
30 447
        static::$mode = $mode;
0 ignored issues
show
Bug introduced by
Since $mode is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $mode to at least protected.
Loading history...
31
    }
32
33
    /**
34
     * @return RoundingMode
35
     */
36 860
    public static function getRoundingMode(): RoundingMode
37
    {
38 860
        return self::$mode;
39
    }
40
41
    /**
42
     * @param string $decimal
43
     * @param int $places
44
     * @return string
45
     */
46 1534
    public static function round(string $decimal, int $places = 0): string
47
    {
48 1534
        $carry = 0;
49
50
        [
51
            $rawString,
52
            $roundedPart,
53
            $roundedPartString,
54
            $otherPart,
55
            $pos,
56
            $wholePart,
57
            $decimalPart,
58
            $currentPart
59 1534
        ] = self::_roundPreFormat($decimal, $places);
60
61 1534
        $sign = static::$isNegative ? '-' : '';
0 ignored issues
show
Bug introduced by
Since $isNegative is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $isNegative to at least protected.
Loading history...
62 1534
        $imaginary = str_ends_with($decimal, 'i') ? 'i' : '';
63
64 1534
        if (empty($decimalPart) && $places >= 0) {
65 921
            return $sign.$rawString.$imaginary;
66
        }
67
68
        do {
69 1053
            if (!array_key_exists($pos, $roundedPart)) {
0 ignored issues
show
Bug introduced by
It seems like $roundedPart can also be of type true; however, parameter $array of array_key_exists() does only seem to accept ArrayObject|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

69
            if (!array_key_exists($pos, /** @scrutinizer ignore-type */ $roundedPart)) {
Loading history...
70 624
                break;
71
            }
72
73 860
            [$digit, $nextDigit] = self::_roundLoopStart(
74
                $roundedPart,
0 ignored issues
show
Bug introduced by
It seems like $roundedPart can also be of type true; however, parameter $roundedPart of Samsara\Fermat\Provider\...ider::_roundLoopStart() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

74
                /** @scrutinizer ignore-type */ $roundedPart,
Loading history...
75
                $otherPart,
0 ignored issues
show
Bug introduced by
It seems like $otherPart can also be of type true; however, parameter $otherPart of Samsara\Fermat\Provider\...ider::_roundLoopStart() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

75
                /** @scrutinizer ignore-type */ $otherPart,
Loading history...
76
                $roundedPartString,
77
                $pos,
78
                $carry,
79
                $currentPart
80
            );
81
82 860
            if ($carry == 0) {
83 860
                $carry = match (self::getRoundingMode()) {
84 860
                    RoundingMode::HalfUp => self::roundHalfUp($digit),
85 840
                    RoundingMode::HalfDown => self::roundHalfDown($digit),
86 820
                    RoundingMode::HalfOdd => self::roundHalfOdd($digit, $nextDigit),
87 800
                    RoundingMode::HalfZero => self::roundHalfZero($digit),
88 780
                    RoundingMode::HalfInf => self::roundHalfInf($digit),
89 760
                    RoundingMode::HalfRandom => self::roundRandom($digit),
90 759
                    RoundingMode::HalfAlternating => self::roundAlternating($digit),
91 758
                    RoundingMode::Ceil => self::roundCeil($digit),
92 758
                    RoundingMode::Floor => self::roundFloor($digit),
93 753
                    RoundingMode::Truncate => self::roundTruncate(),
94 753
                    RoundingMode::Stochastic => self::roundStochastic($digit),
95 752
                    default => self::roundHalfEven($digit, $nextDigit)
96
                };
97
            } else {
98 697
                if ($digit > 9) {
99 332
                    $carry = 1;
100 332
                    $roundedPart[$pos] = '0';
101
                } else {
102 670
                    $carry = 0;
103 670
                    $roundedPart[$pos] = $digit;
104
                }
105
            }
106
107 860
            [$roundedPart, $otherPart, $pos, $carry, $currentPart] = self::_roundLoopEnd(
108
                $roundedPart,
0 ignored issues
show
Bug introduced by
It seems like $roundedPart can also be of type true; however, parameter $roundedPart of Samsara\Fermat\Provider\...ovider::_roundLoopEnd() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

108
                /** @scrutinizer ignore-type */ $roundedPart,
Loading history...
109
                $otherPart,
0 ignored issues
show
Bug introduced by
It seems like $otherPart can also be of type true; however, parameter $otherPart of Samsara\Fermat\Provider\...ovider::_roundLoopEnd() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

109
                /** @scrutinizer ignore-type */ $otherPart,
Loading history...
110
                $pos,
111
                $carry,
112
                $currentPart
113
            );
114 860
        } while ($carry == 1);
115
116 1053
        [$newWholePart, $newDecimalPart] = self::_roundPostFormat($currentPart, $wholePart, $roundedPart, $otherPart, $places);
0 ignored issues
show
Bug introduced by
It seems like $otherPart can also be of type true; however, parameter $otherPart of Samsara\Fermat\Provider\...der::_roundPostFormat() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

116
        [$newWholePart, $newDecimalPart] = self::_roundPostFormat($currentPart, $wholePart, $roundedPart, /** @scrutinizer ignore-type */ $otherPart, $places);
Loading history...
Bug introduced by
$currentPart of type boolean is incompatible with the type string expected by parameter $currentPart of Samsara\Fermat\Provider\...der::_roundPostFormat(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

116
        [$newWholePart, $newDecimalPart] = self::_roundPostFormat(/** @scrutinizer ignore-type */ $currentPart, $wholePart, $roundedPart, $otherPart, $places);
Loading history...
Bug introduced by
It seems like $roundedPart can also be of type true; however, parameter $roundedPart of Samsara\Fermat\Provider\...der::_roundPostFormat() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

116
        [$newWholePart, $newDecimalPart] = self::_roundPostFormat($currentPart, $wholePart, /** @scrutinizer ignore-type */ $roundedPart, $otherPart, $places);
Loading history...
117
118 1053
        static::$remainder = null;
0 ignored issues
show
Bug introduced by
Since $remainder is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $remainder to at least protected.
Loading history...
119
120 1053
        return $sign.$newWholePart.'.'.$newDecimalPart.$imaginary;
121
    }
122
123 774
    #[Pure]
124
    private static function nonHalfEarlyReturn(int $digit): int
125
    {
126 774
        return $digit <=> 5;
127
    }
128
129 40
    private static function negativeReverser(): int
130
    {
131 40
        if (static::$isNegative) {
0 ignored issues
show
Bug introduced by
Since $isNegative is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $isNegative to at least protected.
Loading history...
132 20
            return 1;
133
        } else {
134 20
            return 0;
135
        }
136
    }
137
138 834
    private static function remainderCheck(): bool
139
    {
140 834
        $remainder = static::$remainder;
0 ignored issues
show
Bug introduced by
Since $remainder is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $remainder to at least protected.
Loading history...
141
142 834
        if (is_null($remainder)) {
143 745
            return false;
144
        }
145
146 478
        $remainder = str_replace('0', '', $remainder);
147
148 478
        return !empty($remainder);
149
    }
150
151 20
    private static function roundHalfUp(int $digit): int
152
    {
153 20
        $negative = self::negativeReverser();
154 20
        $remainder = self::remainderCheck();
155
156 20
        if ($negative) {
157 10
            return $digit > 5 || ($digit == 5 && $remainder) ? 1 : 0;
158
        } else {
159 10
            return $digit > 4 ? 1 : 0;
160
        }
161
    }
162
163 20
    private static function roundHalfDown(int $digit): int
164
    {
165 20
        $negative = self::negativeReverser();
166 20
        $remainder = self::remainderCheck();
167
168 20
        if ($negative) {
169 10
            return $digit > 4 ? 1 : 0;
170
        } else {
171 10
            return $digit > 5 || ($digit == 5 && $remainder) ? 1 : 0;
172
        }
173
    }
174
175 752
    private static function roundHalfEven(int $digit, int $nextDigit): int
176
    {
177 752
        $early = static::nonHalfEarlyReturn($digit);
178 752
        $remainder = self::remainderCheck();
179
180 752
        if ($early == 0) {
181 428
            return ($nextDigit % 2 == 0 && !$remainder) ? 0 : 1;
182
        } else {
183 713
            return $early == 1 ? 1 : 0;
184
        }
185
    }
186
187 20
    private static function roundHalfOdd(int $digit, int $nextDigit): int
188
    {
189 20
        $early = static::nonHalfEarlyReturn($digit);
190 20
        $remainder = self::remainderCheck();
191
192 20
        if ($early == 0) {
193 12
            return ($nextDigit % 2 == 1 && !$remainder) ? 0 : 1;
194
        } else {
195 8
            return $early == 1 ? 1 : 0;
196
        }
197
    }
198
199 20
    private static function roundHalfZero(int $digit): int
200
    {
201 20
        $remainder = self::remainderCheck();
202
203 20
        return $digit > 5 || ($digit == 5 && $remainder) ? 1 : 0;
204
    }
205
206 20
    #[Pure]
207
    private static function roundHalfInf(int $digit): int
208
    {
209 20
        return $digit > 4 ? 1 : 0;
210
    }
211
212 310
    private static function roundCeil(int $digit): int
213
    {
214 310
        if (self::$isNegative) {
215 1
            return 0;
216
        } else {
217 310
            return $digit == 0 ? 0 : 1;
218
        }
219
    }
220
221 283
    private static function roundFloor(int $digit): int
222
    {
223 283
        if (self::$isNegative) {
224 1
            return $digit == 0 ? 0 : 1;
225
        } else {
226 283
            return 0;
227
        }
228
    }
229
230
    private static function roundTruncate(): int
231
    {
232
        return 0;
233
    }
234
235 1
    private static function roundRandom(int $digit): int
236
    {
237 1
        $early = static::nonHalfEarlyReturn($digit);
238 1
        $remainder = self::remainderCheck();
239
240 1
        if ($early == 0 && !$remainder) {
241 1
            return RandomProvider::randomInt(0, 1, RandomMode::Speed)->asInt();
242
        } else {
243
            return (($early == 1 || $remainder) ? 1 : 0);
244
        }
245
    }
246
247 1
    private static function roundAlternating(int $digit): int
248
    {
249 1
        $early = static::nonHalfEarlyReturn($digit);
250 1
        $remainder = self::remainderCheck();
251
252 1
        if ($early == 0 && !$remainder) {
253 1
            $val = self::$alt;
254 1
            self::$alt = (int)!$val;
255
256 1
            return $val;
257
        } else {
258
            return (($early == 1 || $remainder) ? 1 : 0);
259
        }
260
    }
261
262 1
    private static function roundStochastic(int $digit): int
263
    {
264 1
        $remainder = static::$remainder;
0 ignored issues
show
Bug introduced by
Since $remainder is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $remainder to at least protected.
Loading history...
265
266 1
        if (is_null($remainder)) {
267
            $target = $digit;
268
            $rangeMin = 0;
269
            $rangeMax = 9;
270
        } else {
271 1
            $remainder = substr($remainder, 0, 3);
272 1
            $target = (int)($digit.$remainder);
273 1
            $rangeMin = 0;
274 1
            $rangeMax = (int)str_repeat('9', strlen($remainder) + 1);
275
        }
276
277 1
        $random = RandomProvider::randomInt($rangeMin, $rangeMax, RandomMode::Speed)->asInt();
278
279 1
        if ($random < $target) {
280 1
            return 1;
281
        } else {
282 1
            return 0;
283
        }
284
    }
285
286
    /**
287
     * @param string $decimal
288
     * @param int $places
289
     * @return array
290
     */
291 1534
    private static function _roundPreFormat(string $decimal, int $places): array
292
    {
293 1534
        $decimal = trim(rtrim($decimal));
294
295 1534
        static::$isNegative = str_starts_with($decimal, '-');
0 ignored issues
show
Bug introduced by
Since $isNegative is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $isNegative to at least protected.
Loading history...
296
297 1534
        $rawString = str_replace('-', '', $decimal);
298
299 1534
        if (str_contains($rawString, '.')) {
300 1041
            [$wholePart, $decimalPart] = explode('.', $rawString);
301
        } else {
302 933
            $wholePart = $rawString;
303 933
            $decimalPart = '';
304
        }
305
306 1534
        $absPlaces = abs($places);
307
308 1534
        $currentPart = $places >= 0;
309 1534
        $roundedPart = $currentPart ? str_split($decimalPart) : str_split($wholePart);
310 1534
        $roundedPartString = $currentPart ? $decimalPart : $wholePart;
311 1534
        $otherPart = $currentPart ? str_split($wholePart) : str_split($decimalPart);
312 1534
        $baseLength = $currentPart ? strlen($decimalPart)-1 : strlen($wholePart);
313 1534
        $pos = $currentPart ? $places : $baseLength + $places;
314
315 1534
        if ($currentPart) {
316 1522
            $pos = ($absPlaces > $baseLength && $places < 0) ? $baseLength : $pos;
317
        } else {
318 12
            $pos = ($absPlaces >= $baseLength) ? 0 : $pos;
319
        }
320
321
        return [
322 1534
            $rawString,
323
            $roundedPart,
324
            $roundedPartString,
325
            $otherPart,
326
            $pos,
327
            $wholePart,
328
            $decimalPart,
329
            $currentPart
330
        ];
331
    }
332
333
    /**
334
     * @param string $currentPart
335
     * @param string $wholePart
336
     * @param array $roundedPart
337
     * @param array $otherPart
338
     * @param int $places
339
     * @return array
340
     */
341 1053
    private static function _roundPostFormat(
342
        string $currentPart,
343
        string $wholePart,
344
        array $roundedPart,
345
        array $otherPart,
346
        int $places
347
    ): array
348
    {
349 1053
        if ($currentPart) {
350 984
            $newDecimalPart = implode('', $roundedPart);
351 984
            $newWholePart = implode('', $otherPart);
352
        } else {
353 369
            $newDecimalPart = implode('', $otherPart);
354 369
            $newWholePart = implode('', $roundedPart);
355
        }
356
357 1053
        if ($places > 0) {
358 989
            $newDecimalPart = substr($newDecimalPart, 0, $places);
359 379
        } elseif ($places == 0) {
360 367
            $newDecimalPart = '0';
361
        } else {
362 12
            $newWholePart = substr($newWholePart, 0, strlen($wholePart)+$places).str_repeat('0', $places*-1);
363 12
            $newDecimalPart = '0';
364
        }
365
366 1053
        if (!strlen(str_replace('0', '', $newDecimalPart))) {
367 442
            $newDecimalPart = '0';
368
        }
369
370 1053
        return [$newWholePart, $newDecimalPart];
371
    }
372
373
    /**
374
     * @param array $roundedPart
375
     * @param array $otherPart
376
     * @param string $roundedPartString
377
     * @param int $pos
378
     * @param int $carry
379
     * @param bool $currentPart
380
     * @return int[]
381
     */
382 860
    private static function _roundLoopStart(
383
        array $roundedPart,
384
        array $otherPart,
385
        string $roundedPartString,
386
        int $pos,
387
        int $carry,
388
        bool $currentPart
389
    ): array
390
    {
391 860
        $digit = (int)$roundedPart[$pos] + $carry;
392
393 860
        if ($carry == 0 && $digit == 5) {
394 494
            static::$remainder = substr($roundedPartString, $pos+1);
0 ignored issues
show
Bug introduced by
Since $remainder is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $remainder to at least protected.
Loading history...
395
        } else {
396 840
            static::$remainder = null;
397
        }
398
399 860
        if ($pos == 0) {
400 395
            if ($currentPart) {
401 395
                $nextDigit = (int)$otherPart[count($otherPart)-1];
402
            } else {
403 395
                $nextDigit = 0;
404
            }
405
        } else {
406 816
            $nextDigit = (int)$roundedPart[$pos-1];
407
        }
408
409 860
        return [$digit, $nextDigit];
410
    }
411
412
    /**
413
     * @param array $roundedPart
414
     * @param array $otherPart
415
     * @param int $pos
416
     * @param int $carry
417
     * @param bool $currentPart
418
     * @return array
419
     */
420 860
    private static function _roundLoopEnd(
421
        array $roundedPart,
422
        array $otherPart,
423
        int $pos,
424
        int $carry,
425
        bool $currentPart
426
    ): array
427
    {
428 860
        if ($pos == 0 && $carry == 1) {
429 357
            if ($currentPart) {
430 357
                $currentPart = false;
431
432
                // Do the variable swap dance
433 357
                $temp = $otherPart;
434 357
                $otherPart = $roundedPart;
435 357
                $roundedPart = $temp;
436
437 357
                $pos = count($roundedPart)-1;
438
            } else {
439 27
                array_unshift($roundedPart, $carry);
440 357
                $carry = 0;
441
            }
442
        } else {
443 860
            $pos -= 1;
444
        }
445
446 860
        return [$roundedPart, $otherPart, $pos, $carry, $currentPart];
447
    }
448
449
}