Passed
Pull Request — master (#134)
by Jordan
06:37 queued 15s
created

RoundingProvider::round()   B

Complexity

Conditions 9
Paths 20

Size

Total Lines 74
Code Lines 54

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 9

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 9
eloc 54
c 4
b 0
f 0
nc 20
nop 2
dl 0
loc 74
ccs 33
cts 33
cp 1
crap 9
rs 7.448

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 331
    public static function setRoundingMode(
27
        RoundingMode $mode
28
    ): void
29
    {
30 331
        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 745
    public static function getRoundingMode(): RoundingMode
37
    {
38 745
        return self::$mode;
39
    }
40
41
    /**
42
     * @param string $decimal
43
     * @param int $places
44
     * @return string
45
     */
46 1419
    public static function round(string $decimal, int $places = 0): string
47
    {
48 1419
        $carry = 0;
49
50
        [
51
            $rawString,
52
            $roundedPart,
53
            $roundedPartString,
54
            $otherPart,
55
            $pos,
56
            $wholePart,
57
            $decimalPart,
58
            $currentPart
59 1419
        ] = self::_roundPreFormat($decimal, $places);
60
61 1419
        $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 1419
        $imaginary = str_ends_with($decimal, 'i') ? 'i' : '';
63
64 1419
        if (empty($decimalPart) && $places >= 0) {
65 921
            return $sign.$rawString.$imaginary;
66
        }
67
68
        do {
69 938
            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 745
            [$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 745
            if ($carry == 0) {
83 745
                $carry = match (self::getRoundingMode()) {
84 745
                    RoundingMode::HalfUp => self::roundHalfUp($digit),
85 744
                    RoundingMode::HalfDown => self::roundHalfDown($digit),
86 743
                    RoundingMode::HalfOdd => self::roundHalfOdd($digit, $nextDigit),
87 742
                    RoundingMode::HalfZero => self::roundHalfZero($digit),
88 741
                    RoundingMode::HalfInf => self::roundHalfInf($digit),
89 740
                    RoundingMode::Ceil => self::roundCeil($digit),
90 740
                    RoundingMode::Floor => self::roundFloor(),
91 735
                    RoundingMode::HalfRandom => self::roundRandom($digit),
92 734
                    RoundingMode::HalfAlternating => self::roundAlternating($digit),
93 733
                    RoundingMode::Stochastic => self::roundStochastic($digit),
94 732
                    default => self::roundHalfEven($digit, $nextDigit)
95
                };
96
            } else {
97 624
                if ($digit > 9) {
98 314
                    $carry = 1;
99 314
                    $roundedPart[$pos] = '0';
100
                } else {
101 621
                    $carry = 0;
102 621
                    $roundedPart[$pos] = $digit;
103
                }
104
            }
105
106 745
            [$roundedPart, $otherPart, $pos, $carry, $currentPart] = self::_roundLoopEnd(
107
                $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

107
                /** @scrutinizer ignore-type */ $roundedPart,
Loading history...
108
                $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

108
                /** @scrutinizer ignore-type */ $otherPart,
Loading history...
109
                $pos,
110
                $carry,
111
                $currentPart
112
            );
113 745
        } while ($carry == 1);
114
115 938
        [$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

115
        [$newWholePart, $newDecimalPart] = self::_roundPostFormat($currentPart, $wholePart, $roundedPart, /** @scrutinizer ignore-type */ $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

115
        [$newWholePart, $newDecimalPart] = self::_roundPostFormat($currentPart, $wholePart, /** @scrutinizer ignore-type */ $roundedPart, $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

115
        [$newWholePart, $newDecimalPart] = self::_roundPostFormat(/** @scrutinizer ignore-type */ $currentPart, $wholePart, $roundedPart, $otherPart, $places);
Loading history...
116
117 938
        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...
118
119 938
        return $sign.$newWholePart.'.'.$newDecimalPart.$imaginary;
120
    }
121
122 735
    #[Pure]
123
    private static function nonHalfEarlyReturn(int $digit): int
124
    {
125 735
        return $digit <=> 5;
126
    }
127
128 2
    private static function negativeReverser(): int
129
    {
130 2
        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...
131 2
            return 1;
132
        } else {
133 2
            return 0;
134
        }
135
    }
136
137 738
    private static function remainderCheck(): bool
138
    {
139 738
        $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...
140
141 738
        if (is_null($remainder)) {
142 709
            return false;
143
        }
144
145 423
        $remainder = str_replace('0', '', $remainder);
146
147 423
        return !empty($remainder);
148
    }
149
150 1
    private static function roundHalfUp(int $digit): int
151
    {
152 1
        $negative = self::negativeReverser();
153 1
        $remainder = self::remainderCheck();
154
155 1
        if ($negative) {
156 1
            return $digit > 5 || ($digit == 5 && $remainder) ? 1 : 0;
157
        } else {
158 1
            return $digit > 4 ? 1 : 0;
159
        }
160
    }
161
162 1
    private static function roundHalfDown(int $digit): int
163
    {
164 1
        $negative = self::negativeReverser();
165 1
        $remainder = self::remainderCheck();
166
167 1
        if ($negative) {
168 1
            return $digit > 4 ? 1 : 0;
169
        } else {
170 1
            return $digit > 5 || ($digit == 5 && $remainder) ? 1 : 0;
171
        }
172
    }
173
174 732
    private static function roundHalfEven(int $digit, int $nextDigit): int
175
    {
176 732
        $early = static::nonHalfEarlyReturn($digit);
177 732
        $remainder = self::remainderCheck();
178
179 732
        if ($early == 0) {
180 417
            return ($nextDigit % 2 == 0 && !$remainder) ? 0 : 1;
181
        } else {
182 705
            return $early == 1 ? 1 : 0;
183
        }
184
    }
185
186 1
    private static function roundHalfOdd(int $digit, int $nextDigit): int
187
    {
188 1
        $early = static::nonHalfEarlyReturn($digit);
189 1
        $remainder = self::remainderCheck();
190
191 1
        if ($early == 0) {
192 1
            return ($nextDigit % 2 == 1 && !$remainder) ? 0 : 1;
193
        } else {
194 1
            return $early == 1 ? 1 : 0;
195
        }
196
    }
197
198 1
    private static function roundHalfZero(int $digit): int
199
    {
200 1
        $remainder = self::remainderCheck();
201
202 1
        return $digit > 5 || ($digit == 5 && $remainder) ? 1 : 0;
203
    }
204
205 1
    #[Pure]
206
    private static function roundHalfInf(int $digit): int
207
    {
208 1
        return $digit > 4 ? 1 : 0;
209
    }
210
211 310
    #[Pure]
212
    private static function roundCeil(int $digit): int
213
    {
214 310
        return $digit == 0 ? 0 : 1;
215
    }
216
217 283
    #[Pure]
218
    private static function roundFloor(): int
219
    {
220 283
        return 0;
221
    }
222
223 1
    private static function roundRandom(int $digit): int
224
    {
225 1
        $early = static::nonHalfEarlyReturn($digit);
226 1
        $remainder = self::remainderCheck();
227
228 1
        if ($early == 0 && !$remainder) {
229 1
            return RandomProvider::randomInt(0, 1, RandomMode::Speed)->asInt();
230
        } else {
231
            return (($early == 1 || $remainder) ? 1 : 0);
232
        }
233
    }
234
235 1
    private static function roundAlternating(int $digit): int
236
    {
237 1
        $early = static::nonHalfEarlyReturn($digit);
238 1
        $remainder = self::remainderCheck();
239
240 1
        if ($early == 0 && !$remainder) {
241 1
            $val = self::$alt;
242 1
            self::$alt = (int)!$val;
243
244 1
            return $val;
245
        } else {
246
            return (($early == 1 || $remainder) ? 1 : 0);
247
        }
248
    }
249
250 1
    private static function roundStochastic(int $digit): int
251
    {
252 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...
253
254 1
        if (is_null($remainder)) {
255
            $target = $digit;
256
            $rangeMin = 0;
257
            $rangeMax = 9;
258
        } else {
259 1
            $remainder = substr($remainder, 0, 3);
260 1
            $target = (int)($digit.$remainder);
261 1
            $rangeMin = 0;
262 1
            $rangeMax = (int)str_repeat('9', strlen($remainder) + 1);
263
        }
264
265 1
        $random = RandomProvider::randomInt($rangeMin, $rangeMax, RandomMode::Speed)->asInt();
266
267 1
        if ($random < $target) {
268 1
            return 1;
269
        } else {
270 1
            return 0;
271
        }
272
    }
273
274 1419
    private static function _roundPreFormat(string $decimal, int $places): array
275
    {
276 1419
        $decimal = trim(rtrim($decimal));
277
278 1419
        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...
279
280 1419
        $rawString = str_replace('-', '', $decimal);
281
282 1419
        if (str_contains($rawString, '.')) {
283 938
            [$wholePart, $decimalPart] = explode('.', $rawString);
284
        } else {
285 927
            $wholePart = $rawString;
286 927
            $decimalPart = '';
287
        }
288
289 1419
        $absPlaces = abs($places);
290
291 1419
        $currentPart = $places >= 0;
292 1419
        $roundedPart = $currentPart ? str_split($decimalPart) : str_split($wholePart);
293 1419
        $roundedPartString = $currentPart ? $decimalPart : $wholePart;
294 1419
        $otherPart = $currentPart ? str_split($wholePart) : str_split($decimalPart);
295 1419
        $baseLength = $currentPart ? strlen($decimalPart)-1 : strlen($wholePart);
296 1419
        $pos = $currentPart ? $places : $baseLength + $places;
297
298 1419
        if ($currentPart) {
299 1419
            $pos = ($absPlaces > $baseLength && $places < 0) ? $baseLength : $pos;
300
        } else {
301 6
            $pos = ($absPlaces >= $baseLength) ? 0 : $pos;
302
        }
303
304
        return [
305 1419
            $rawString,
306
            $roundedPart,
307
            $roundedPartString,
308
            $otherPart,
309
            $pos,
310
            $wholePart,
311
            $decimalPart,
312
            $currentPart
313
        ];
314
    }
315
316 938
    private static function _roundPostFormat(
317
        string $currentPart,
318
        string $wholePart,
319
        array $roundedPart,
320
        array $otherPart,
321
        int $places
322
    ): array
323
    {
324 938
        if ($currentPart) {
325 935
            $newDecimalPart = implode('', $roundedPart);
326 935
            $newWholePart = implode('', $otherPart);
327
        } else {
328 309
            $newDecimalPart = implode('', $otherPart);
329 309
            $newWholePart = implode('', $roundedPart);
330
        }
331
332 938
        if ($places > 0) {
333 934
            $newDecimalPart = substr($newDecimalPart, 0, $places);
334 325
        } elseif ($places == 0) {
335 325
            $newDecimalPart = '0';
336
        } else {
337 6
            $newWholePart = substr($newWholePart, 0, strlen($wholePart)+$places).str_repeat('0', $places*-1);
338 6
            $newDecimalPart = '0';
339
        }
340
341 938
        if (!strlen(str_replace('0', '', $newDecimalPart))) {
342 376
            $newDecimalPart = '0';
343
        }
344
345 938
        return [$newWholePart, $newDecimalPart];
346
    }
347
348 745
    private static function _roundLoopStart(
349
        array $roundedPart,
350
        array $otherPart,
351
        string $roundedPartString,
352
        int $pos,
353
        int $carry,
354
        bool $currentPart
355
    ): array
356
    {
357 745
        $digit = (int)$roundedPart[$pos] + $carry;
358
359 745
        if ($carry == 0 && $digit == 5) {
360 428
            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...
361
        } else {
362 743
            static::$remainder = null;
363
        }
364
365 745
        if ($pos == 0) {
366 341
            if ($currentPart) {
367 341
                $nextDigit = (int)$otherPart[count($otherPart)-1];
368
            } else {
369 341
                $nextDigit = 0;
370
            }
371
        } else {
372 737
            $nextDigit = (int)$roundedPart[$pos-1];
373
        }
374
375 745
        return [$digit, $nextDigit];
376
    }
377
378 745
    private static function _roundLoopEnd(
379
        array $roundedPart,
380
        array $otherPart,
381
        int $pos,
382
        int $carry,
383
        bool $currentPart
384
    ): array
385
    {
386 745
        if ($pos == 0 && $carry == 1) {
387 309
            if ($currentPart) {
388 309
                $currentPart = false;
389
390
                // Do the variable swap dance
391 309
                $temp = $otherPart;
392 309
                $otherPart = $roundedPart;
393 309
                $roundedPart = $temp;
394
395 309
                $pos = count($roundedPart)-1;
396
            } else {
397 9
                array_unshift($roundedPart, $carry);
398 309
                $carry = 0;
399
            }
400
        } else {
401 745
            $pos -= 1;
402
        }
403
404 745
        return [$roundedPart, $otherPart, $pos, $carry, $currentPart];
405
    }
406
407
}