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

RoundingProvider::_roundPostFormat()   A

Complexity

Conditions 5
Paths 12

Size

Total Lines 30
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 16
c 1
b 0
f 0
nc 12
nop 5
dl 0
loc 30
ccs 15
cts 15
cp 1
crap 5
rs 9.4222
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 1419
    private static function _roundPreFormat(string $decimal, int $places): array
42
    {
43 1419
        $decimal = trim(rtrim($decimal));
44
45 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...
46
47 1419
        $rawString = str_replace('-', '', $decimal);
48
49 1419
        if (str_contains($rawString, '.')) {
50 938
            [$wholePart, $decimalPart] = explode('.', $rawString);
51
        } else {
52 927
            $wholePart = $rawString;
53 927
            $decimalPart = '';
54
        }
55
56 1419
        $absPlaces = abs($places);
57
58 1419
        $currentPart = $places >= 0;
59 1419
        $roundedPart = $currentPart ? str_split($decimalPart) : str_split($wholePart);
60 1419
        $roundedPartString = $currentPart ? $decimalPart : $wholePart;
61 1419
        $otherPart = $currentPart ? str_split($wholePart) : str_split($decimalPart);
62 1419
        $baseLength = $currentPart ? strlen($decimalPart)-1 : strlen($wholePart);
63 1419
        $pos = $currentPart ? $places : $baseLength + $places;
64
65 1419
        if ($currentPart) {
66 1419
            $pos = ($absPlaces > $baseLength && $places < 0) ? $baseLength : $pos;
67
        } else {
68 6
            $pos = ($absPlaces >= $baseLength) ? 0 : $pos;
69
        }
70
71
        return [
72 1419
            $rawString,
73
            $roundedPart,
74
            $roundedPartString,
75
            $otherPart,
76
            $pos,
77
            $wholePart,
78
            $decimalPart,
79
            $currentPart
80
        ];
81
    }
82
83 938
    private static function _roundPostFormat(
84
        string $currentPart,
85
        string $wholePart,
86
        array $roundedPart,
87
        array $otherPart,
88
        int $places
89
    ): array
90
    {
91 938
        if ($currentPart) {
92 935
            $newDecimalPart = implode('', $roundedPart);
93 935
            $newWholePart = implode('', $otherPart);
94
        } else {
95 309
            $newDecimalPart = implode('', $otherPart);
96 309
            $newWholePart = implode('', $roundedPart);
97
        }
98
99 938
        if ($places > 0) {
100 934
            $newDecimalPart = substr($newDecimalPart, 0, $places);
101 325
        } elseif ($places == 0) {
102 325
            $newDecimalPart = '0';
103
        } else {
104 6
            $newWholePart = substr($newWholePart, 0, strlen($wholePart)+$places).str_repeat('0', $places*-1);
105 6
            $newDecimalPart = '0';
106
        }
107
108 938
        if (!strlen(str_replace('0', '', $newDecimalPart))) {
109 376
            $newDecimalPart = '0';
110
        }
111
112 938
        return [$newWholePart, $newDecimalPart];
113
    }
114
115
    /**
116
     * @param string $decimal
117
     * @param int $places
118
     * @return string
119
     */
120 1419
    public static function round(string $decimal, int $places = 0): string
121
    {
122 1419
        $carry = 0;
123
124
        [
125
            $rawString,
126
            $roundedPart,
127
            $roundedPartString,
128
            $otherPart,
129
            $pos,
130
            $wholePart,
131
            $decimalPart,
132
            $currentPart
133 1419
        ] = self::_roundPreFormat($decimal, $places);
134
135 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...
136 1419
        $imaginary = str_ends_with($decimal, 'i') ? 'i' : '';
137
138 1419
        if (empty($decimalPart) && $places >= 0) {
139 921
            return $sign.$rawString.$imaginary;
140
        }
141
142
        do {
143 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

143
            if (!array_key_exists($pos, /** @scrutinizer ignore-type */ $roundedPart)) {
Loading history...
144 624
                break;
145
            }
146
147 745
            $digit = (int)$roundedPart[$pos] + $carry;
148
149 745
            if ($carry == 0 && $digit == 5) {
150 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...
151
            } else {
152 743
                static::$remainder = null;
153
            }
154
155 745
            if ($pos == 0) {
156 341
                if ($currentPart) {
157 341
                    $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

157
                    $nextDigit = (int)$otherPart[count(/** @scrutinizer ignore-type */ $otherPart)-1];
Loading history...
158
                } else {
159 341
                    $nextDigit = 0;
160
                }
161
            } else {
162 737
                $nextDigit = (int)$roundedPart[$pos-1];
163
            }
164
165 745
            if ($carry == 0) {
166 745
                $carry = match (self::getRoundingMode()) {
167 745
                    RoundingMode::HalfUp => self::roundHalfUp($digit),
168 744
                    RoundingMode::HalfDown => self::roundHalfDown($digit),
169 743
                    RoundingMode::HalfOdd => self::roundHalfOdd($digit, $nextDigit),
170 742
                    RoundingMode::HalfZero => self::roundHalfZero($digit),
171 741
                    RoundingMode::HalfInf => self::roundHalfInf($digit),
172 740
                    RoundingMode::Ceil => self::roundCeil($digit),
173 740
                    RoundingMode::Floor => self::roundFloor(),
174 735
                    RoundingMode::HalfRandom => self::roundRandom($digit),
175 734
                    RoundingMode::HalfAlternating => self::roundAlternating($digit),
176 733
                    RoundingMode::Stochastic => self::roundStochastic($digit),
177 732
                    default => self::roundHalfEven($digit, $nextDigit)
178
                };
179
            } else {
180 624
                if ($digit > 9) {
181 314
                    $carry = 1;
182 314
                    $roundedPart[$pos] = '0';
183
                } else {
184 621
                    $carry = 0;
185 621
                    $roundedPart[$pos] = $digit;
186
                }
187
            }
188
189 745
            if ($pos == 0 && $carry == 1) {
190 309
                if ($currentPart) {
191 309
                    $currentPart = false;
192
193
                    // Do the variable swap dance
194 309
                    $temp = $otherPart;
195 309
                    $otherPart = $roundedPart;
196 309
                    $roundedPart = $temp;
197
198 309
                    $pos = count($roundedPart)-1;
199
                } else {
200 9
                    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

200
                    array_unshift(/** @scrutinizer ignore-type */ $roundedPart, $carry);
Loading history...
201 309
                    $carry = 0;
202
                }
203
            } else {
204 745
                $pos -= 1;
205
            }
206 745
        } while ($carry == 1);
207
208 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

208
        [$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

208
        [$newWholePart, $newDecimalPart] = self::_roundPostFormat(/** @scrutinizer ignore-type */ $currentPart, $wholePart, $roundedPart, $otherPart, $places);
Loading history...
209
210 938
        static::$remainder = null;
211
212 938
        return $sign.$newWholePart.'.'.$newDecimalPart.$imaginary;
213
    }
214
215 735
    #[Pure]
216
    private static function nonHalfEarlyReturn(int $digit): int
217
    {
218 735
        return $digit <=> 5;
219
    }
220
221 2
    private static function negativeReverser(): int
222
    {
223 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...
224 2
            return 1;
225
        } else {
226 2
            return 0;
227
        }
228
    }
229
230 738
    private static function remainderCheck(): bool
231
    {
232 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...
233
234 738
        if (is_null($remainder)) {
235 709
            return false;
236
        }
237
238 423
        $remainder = str_replace('0', '', $remainder);
239
240 423
        return !empty($remainder);
241
    }
242
243 1
    private static function roundHalfUp(int $digit): int
244
    {
245 1
        $negative = self::negativeReverser();
246 1
        $remainder = self::remainderCheck();
247
248 1
        if ($negative) {
249 1
            return $digit > 5 || ($digit == 5 && $remainder) ? 1 : 0;
250
        } else {
251 1
            return $digit > 4 ? 1 : 0;
252
        }
253
    }
254
255 1
    private static function roundHalfDown(int $digit): int
256
    {
257 1
        $negative = self::negativeReverser();
258 1
        $remainder = self::remainderCheck();
259
260 1
        if ($negative) {
261 1
            return $digit > 4 ? 1 : 0;
262
        } else {
263 1
            return $digit > 5 || ($digit == 5 && $remainder) ? 1 : 0;
264
        }
265
    }
266
267 732
    private static function roundHalfEven(int $digit, int $nextDigit): int
268
    {
269 732
        $early = static::nonHalfEarlyReturn($digit);
270 732
        $remainder = self::remainderCheck();
271
272 732
        if ($early == 0) {
273 417
            return ($nextDigit % 2 == 0 && !$remainder) ? 0 : 1;
274
        } else {
275 705
            return $early == 1 ? 1 : 0;
276
        }
277
    }
278
279 1
    private static function roundHalfOdd(int $digit, int $nextDigit): int
280
    {
281 1
        $early = static::nonHalfEarlyReturn($digit);
282 1
        $remainder = self::remainderCheck();
283
284 1
        if ($early == 0) {
285 1
            return ($nextDigit % 2 == 1 && !$remainder) ? 0 : 1;
286
        } else {
287 1
            return $early == 1 ? 1 : 0;
288
        }
289
    }
290
291 1
    private static function roundHalfZero(int $digit): int
292
    {
293 1
        $remainder = self::remainderCheck();
294
295 1
        return $digit > 5 || ($digit == 5 && $remainder) ? 1 : 0;
296
    }
297
298 1
    #[Pure]
299
    private static function roundHalfInf(int $digit): int
300
    {
301 1
        return $digit > 4 ? 1 : 0;
302
    }
303
304 310
    #[Pure]
305
    private static function roundCeil(int $digit): int
306
    {
307 310
        return $digit == 0 ? 0 : 1;
308
    }
309
310 283
    #[Pure]
311
    private static function roundFloor(): int
312
    {
313 283
        return 0;
314
    }
315
316 1
    private static function roundRandom(int $digit): int
317
    {
318 1
        $early = static::nonHalfEarlyReturn($digit);
319 1
        $remainder = self::remainderCheck();
320
321 1
        if ($early == 0 && !$remainder) {
322 1
            return RandomProvider::randomInt(0, 1, RandomMode::Speed)->asInt();
323
        } else {
324
            return (($early == 1 || $remainder) ? 1 : 0);
325
        }
326
    }
327
328 1
    private static function roundAlternating(int $digit): int
329
    {
330 1
        $early = static::nonHalfEarlyReturn($digit);
331 1
        $remainder = self::remainderCheck();
332
333 1
        if ($early == 0 && !$remainder) {
334 1
            $val = self::$alt;
335 1
            self::$alt = (int)!$val;
336
337 1
            return $val;
338
        } else {
339
            return (($early == 1 || $remainder) ? 1 : 0);
340
        }
341
    }
342
343 1
    private static function roundStochastic(int $digit): int
344
    {
345 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...
346
347 1
        if (is_null($remainder)) {
348
            $target = $digit;
349
            $rangeMin = 0;
350
            $rangeMax = 9;
351
        } else {
352 1
            $remainder = substr($remainder, 0, 3);
353 1
            $target = (int)($digit.$remainder);
354 1
            $rangeMin = 0;
355 1
            $rangeMax = (int)str_repeat('9', strlen($remainder) + 1);
356
        }
357
358 1
        $random = RandomProvider::randomInt($rangeMin, $rangeMax, RandomMode::Speed)->asInt();
359
360 1
        if ($random < $target) {
361 1
            return 1;
362
        } else {
363 1
            return 0;
364
        }
365
    }
366
367
}