Passed
Push — master ( 5d0e72...3ef39b )
by Jordan
03:58 queued 12s
created

RoundingProvider::roundAlternating()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 5.0488

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 8
c 1
b 0
f 0
nc 5
nop 1
dl 0
loc 12
ccs 7
cts 8
cp 0.875
crap 5.0488
rs 9.6111
1
<?php
2
3
namespace Samsara\Fermat\Provider;
4
5
use JetBrains\PhpStorm\ExpectedValues;
6
use JetBrains\PhpStorm\Pure;
7
use Samsara\Fermat\Types\Base\Interfaces\Numbers\DecimalInterface;
8
9
class RoundingProvider
10
{
11
12
    const MODE_HALF_UP = 1;
13
    const MODE_HALF_DOWN = 2;
14
    const MODE_HALF_EVEN = 3;
15
    const MODE_HALF_ODD = 4;
16
    const MODE_HALF_ZERO = 5;
17
    const MODE_HALF_INF = 6;
18
    const MODE_CEIL = 7;
19
    const MODE_FLOOR = 8;
20
    const MODE_RANDOM = 9;
21
    const MODE_ALTERNATING = 10;
22
    const MODE_STOCHASTIC = 11;
23
24
    private static int $mode = self::MODE_HALF_EVEN;
25
    private static ?DecimalInterface $decimal;
26
    private static int $alt = 1;
27
    private static ?string $remainder;
28
29 32
    public static function setRoundingMode(
30
        #[ExpectedValues([
31
            self::MODE_HALF_UP,
32
            self::MODE_HALF_DOWN,
33
            self::MODE_HALF_EVEN,
34
            self::MODE_HALF_ODD,
35
            self::MODE_HALF_ZERO,
36
            self::MODE_HALF_INF,
37
            self::MODE_CEIL,
38
            self::MODE_FLOOR,
39
            self::MODE_RANDOM,
40
            self::MODE_ALTERNATING,
41
            self::MODE_STOCHASTIC
42
        ])]
43
        int $mode
44
    ): void
45
    {
46 32
        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...
47 32
    }
48
49 48
    #[Pure]
50
    public static function getRoundingMode(): int
51
    {
52 48
        return static::$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...
53
    }
54
55 48
    public static function round(DecimalInterface $decimal, int $places = 0): string
56
    {
57 48
        static::$decimal = $decimal;
0 ignored issues
show
Bug introduced by
Since $decimal 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 $decimal to at least protected.
Loading history...
58
59 48
        $rawString = str_replace('-', '', $decimal->getAsBaseTenRealNumber());
60
61 48
        if ($decimal->isInt() && $places >= 0) {
62 11
            return $rawString;
63
        }
64
65 47
        $sign = $decimal->isNegative() ? '-' : '';
66 47
        $imaginary = $decimal->isImaginary() ? 'i' : '';
67
68 47
        if (str_contains($rawString, '.')) {
69 47
            [$wholePart, $decimalPart] = explode('.', $rawString);
70
        } else {
71 6
            $wholePart = $rawString;
72 6
            $decimalPart = '';
73
        }
74
75 47
        $absPlaces = abs($places);
76
77 47
        $currentPart = $places >= 0;
78 47
        $roundedPart = $currentPart ? str_split($decimalPart) : str_split($wholePart);
79 47
        $roundedPartString = $currentPart ? $decimalPart : $wholePart;
80 47
        $otherPart = $currentPart ? str_split($wholePart) : str_split($decimalPart);
81 47
        $baseLength = $currentPart ? strlen($decimalPart)-1 : strlen($wholePart);
82 47
        $pos = $currentPart ? $places : $baseLength + $places;
83 47
        $carry = 0;
84
85 47
        if ($currentPart) {
86 47
            $pos = ($absPlaces > $baseLength) ? $baseLength : $pos;
87
        } else {
88 6
            $pos = ($absPlaces >= $baseLength) ? 0 : $pos;
89
        }
90
91
        do {
92 47
            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

92
            if (!array_key_exists($pos, /** @scrutinizer ignore-type */ $roundedPart)) {
Loading history...
93
                break;
94
            }
95
96 47
            $digit = (int)$roundedPart[$pos] + $carry;
97
98 47
            if ($carry == 0 && $digit == 5) {
99 28
                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...
100
            } else {
101 46
                static::$remainder = null;
102
            }
103
104 47
            if ($pos == 0) {
105 36
                if ($currentPart) {
106 36
                    $nextDigit = (int)$otherPart[count($otherPart)-1];
0 ignored issues
show
Bug introduced by
It seems like $otherPart can also be of type true; however, parameter $value of count() does only seem to accept Countable|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

106
                    $nextDigit = (int)$otherPart[count(/** @scrutinizer ignore-type */ $otherPart)-1];
Loading history...
107
                } else {
108 36
                    $nextDigit = 0;
109
                }
110
            } else {
111 36
                $nextDigit = (int)$roundedPart[$pos-1];
112
            }
113
114 47
            if ($carry == 0) {
115 47
                $carry = match (self::getRoundingMode()) {
116 47
                    self::MODE_HALF_UP => self::roundHalfUp($digit),
117 46
                    self::MODE_HALF_DOWN => self::roundHalfDown($digit),
118 45
                    self::MODE_HALF_ODD => self::roundHalfOdd($digit, $nextDigit),
119 44
                    self::MODE_HALF_ZERO => self::roundHalfZero($digit),
120 43
                    self::MODE_HALF_INF => self::roundHalfInf($digit),
121 42
                    self::MODE_CEIL => self::roundCeil($digit),
122 42
                    self::MODE_FLOOR => self::roundFloor(),
123 34
                    self::MODE_RANDOM => self::roundRandom($digit),
124 33
                    self::MODE_ALTERNATING => self::roundAlternating($digit),
125 32
                    self::MODE_STOCHASTIC => self::roundStochastic($digit),
126 31
                    default => self::roundHalfEven($digit, $nextDigit)
127
                };
128
            } else {
129 39
                if ($digit > 9) {
130 25
                    $carry = 1;
131 25
                    $roundedPart[$pos] = '0';
132
                } else {
133 39
                    $carry = 0;
134 39
                    $roundedPart[$pos] = $digit;
135
                }
136
            }
137
138 47
            if ($pos == 0 && $carry == 1) {
139 18
                if ($currentPart) {
140 18
                    $currentPart = false;
141
142
                    // Do the variable swap dance
143 18
                    $temp = $otherPart;
144 18
                    $otherPart = $roundedPart;
145 18
                    $roundedPart = $temp;
146
147 18
                    $pos = count($roundedPart)-1;
148
                } else {
149 6
                    array_unshift($roundedPart, $carry);
0 ignored issues
show
Bug introduced by
It seems like $roundedPart can also be of type true; however, parameter $array of array_unshift() 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

149
                    array_unshift(/** @scrutinizer ignore-type */ $roundedPart, $carry);
Loading history...
150 18
                    $carry = 0;
151
                }
152
            } else {
153 47
                $pos -= 1;
154
            }
155 47
        } while ($carry == 1);
156
157 47
        if ($currentPart) {
158 46
            $newDecimalPart = implode('', $roundedPart);
159 46
            $newWholePart = implode('', $otherPart);
0 ignored issues
show
Bug introduced by
It seems like $otherPart can also be of type true; however, parameter $pieces of implode() 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

159
            $newWholePart = implode('', /** @scrutinizer ignore-type */ $otherPart);
Loading history...
160
        } else {
161 18
            $newDecimalPart = implode('', $otherPart);
162 18
            $newWholePart = implode('', $roundedPart);
163
        }
164
165 47
        if ($places > 0) {
166 36
            $newDecimalPart = substr($newDecimalPart, 0, $places);
167 33
        } elseif ($places == 0) {
168 33
            $newDecimalPart = '0';
169
        } else {
170 6
            $newWholePart = substr($newWholePart, 0, strlen($wholePart)+$places).str_repeat('0', $places*-1);
171 6
            $newDecimalPart = '0';
172
        }
173
174 47
        if (!strlen(str_replace('0', '', $newDecimalPart))) {
175 34
            $newDecimalPart = '0';
176
        }
177
178 47
        static::$remainder = null;
179 47
        static::$decimal = null;
180
181 47
        return $sign.$newWholePart.'.'.$newDecimalPart.$imaginary;
182
    }
183
184 34
    #[Pure]
185
    private static function nonHalfEarlyReturn(int $digit): int
186
    {
187 34
        return $digit <=> 5;
188
    }
189
190 2
    private static function negativeReverser(): int
191
    {
192 2
        if (static::$decimal->isNegative()) {
0 ignored issues
show
Bug introduced by
Since $decimal 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 $decimal to at least protected.
Loading history...
Bug introduced by
The method isNegative() does not exist on null. ( Ignorable by Annotation )

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

192
        if (static::$decimal->/** @scrutinizer ignore-call */ isNegative()) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
193 2
            return 1;
194
        } else {
195 2
            return 0;
196
        }
197
    }
198
199 37
    private static function remainderCheck(): bool
200
    {
201 37
        $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...
202
203 37
        if (is_null($remainder)) {
204 35
            return false;
205
        }
206
207 23
        $remainder = str_replace('0', '', $remainder);
208
209 23
        return !empty($remainder);
210
    }
211
212 1
    private static function roundHalfUp(int $digit): int
213
    {
214 1
        $negative = self::negativeReverser();
215 1
        $remainder = self::remainderCheck();
216
217 1
        if ($negative) {
218 1
            return $digit > 5 || ($digit == 5 && $remainder) ? 1 : 0;
219
        } else {
220 1
            return $digit > 4 ? 1 : 0;
221
        }
222
    }
223
224 1
    private static function roundHalfDown(int $digit): int
225
    {
226 1
        $negative = self::negativeReverser();
227 1
        $remainder = self::remainderCheck();
228
229 1
        if ($negative) {
230 1
            return $digit > 4 ? 1 : 0;
231
        } else {
232 1
            return $digit > 5 || ($digit == 5 && $remainder) ? 1 : 0;
233
        }
234
    }
235
236 31
    private static function roundHalfEven(int $digit, int $nextDigit): int
237
    {
238 31
        $early = static::nonHalfEarlyReturn($digit);
239 31
        $remainder = self::remainderCheck();
240
241 31
        if ($early == 0) {
242 17
            return ($nextDigit % 2 == 0 && !$remainder) ? 0 : 1;
243
        } else {
244 31
            return $early == 1 ? 1 : 0;
245
        }
246
    }
247
248 1
    private static function roundHalfOdd(int $digit, int $nextDigit): int
249
    {
250 1
        $early = static::nonHalfEarlyReturn($digit);
251 1
        $remainder = self::remainderCheck();
252
253 1
        if ($early == 0) {
254 1
            return ($nextDigit % 2 == 1 && !$remainder) ? 0 : 1;
255
        } else {
256 1
            return $early == 1 ? 1 : 0;
257
        }
258
    }
259
260 1
    private static function roundHalfZero(int $digit): int
261
    {
262 1
        $remainder = self::remainderCheck();
263
264 1
        return $digit > 5 || ($digit == 5 && $remainder) ? 1 : 0;
265
    }
266
267 1
    #[Pure]
268
    private static function roundHalfInf(int $digit): int
269
    {
270 1
        return $digit > 4 ? 1 : 0;
271
    }
272
273 4
    #[Pure]
274
    private static function roundCeil(int $digit): int
275
    {
276 4
        return $digit == 0 ? 0 : 1;
277
    }
278
279 23
    #[Pure]
280
    private static function roundFloor(): int
281
    {
282 23
        return 0;
283
    }
284
285 1
    private static function roundRandom(int $digit): int
286
    {
287 1
        $early = static::nonHalfEarlyReturn($digit);
288 1
        $remainder = self::remainderCheck();
289
290 1
        if ($early == 0 && !$remainder) {
291 1
            return RandomProvider::randomInt(0, 1, RandomProvider::MODE_SPEED)->asInt();
292
        } else {
293
            return (($early == 1 || $remainder) ? 1 : 0);
294
        }
295
    }
296
297 1
    private static function roundAlternating(int $digit): int
298
    {
299 1
        $early = static::nonHalfEarlyReturn($digit);
300 1
        $remainder = self::remainderCheck();
301
302 1
        if ($early == 0 && !$remainder) {
303 1
            $val = self::$alt;
304 1
            self::$alt = (int)!$val;
305
306 1
            return $val;
307
        } else {
308
            return (($early == 1 || $remainder) ? 1 : 0);
309
        }
310
    }
311
312 1
    private static function roundStochastic(int $digit): int
313
    {
314 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...
315
316 1
        if (is_null($remainder)) {
317
            $target = $digit;
318
            $rangeMin = 0;
319
            $rangeMax = 9;
320
        } else {
321 1
            $remainder = substr($remainder, 0, 3);
322 1
            $target = (int)($digit.$remainder);
323 1
            $rangeMin = 0;
324 1
            $rangeMax = (int)str_repeat('9', strlen($remainder) + 1);
325
        }
326
327 1
        $random = RandomProvider::randomInt($rangeMin, $rangeMax, RandomProvider::MODE_SPEED)->asInt();
328
329 1
        if ($random < $target) {
330 1
            return 1;
331
        } else {
332 1
            return 0;
333
        }
334
    }
335
336
}