Passed
Push — master ( 83af19...b3521e )
by Smoren
02:05
created

Slice::normalize()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 6
c 1
b 0
f 0
dl 0
loc 11
rs 10
cc 3
nc 3
nop 1
1
<?php
2
3
namespace Smoren\ArrayView\Structs;
4
5
use Smoren\ArrayView\Exceptions\IndexError;
6
use Smoren\ArrayView\Exceptions\ValueError;
7
use Smoren\ArrayView\Util;
8
9
/**
10
 * @property-read int|null $start
11
 * @property-read int|null $end
12
 * @property-read int|null $step
13
 */
14
class Slice
15
{
16
    /**
17
     * @var int|null
18
     */
19
    public ?int $start;
20
    /**
21
     * @var int|null
22
     */
23
    public ?int $end;
24
    /**
25
     * @var int|null
26
     */
27
    public ?int $step;
28
29
    /**
30
     * @param string|Slice|array<int> $s
31
     *
32
     * @return Slice
33
     */
34
    public static function toSlice($s): Slice
35
    {
36
        /** @var mixed $s */
37
        if ($s instanceof Slice) {
38
            return $s;
39
        }
40
41
        if (\is_array($s) && self::isSliceArray($s)) {
42
            return new Slice(...$s);
0 ignored issues
show
Bug introduced by
$s is expanded, but the parameter $start of Smoren\ArrayView\Structs\Slice::__construct() does not expect variable arguments. ( Ignorable by Annotation )

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

42
            return new Slice(/** @scrutinizer ignore-type */ ...$s);
Loading history...
43
        }
44
45
        if (!self::isSliceString($s)) {
46
            $str = \is_scalar($s) ? "{$s}" : gettype($s);
47
            throw new ValueError("Invalid slice: \"{$str}\".");
48
        }
49
50
        /** @var string $s */
51
        $slice = self::parseSliceString($s);
52
53
        return new Slice(...$slice);
54
    }
55
56
    /**
57
     * @param mixed $s
58
     *
59
     * @return bool
60
     */
61
    public static function isSlice($s): bool
62
    {
63
        return ($s instanceof Slice) || static::isSliceString($s) || static::isSliceArray($s);
64
    }
65
66
    /**
67
     * @param mixed $s
68
     *
69
     * @return bool
70
     */
71
    public static function isSliceString($s): bool
72
    {
73
        if (!\is_string($s)) {
74
            return false;
75
        }
76
77
        if (\is_numeric($s)) {
78
            return false;
79
        }
80
81
        if (!\preg_match('/^-?[0-9]*:?-?[0-9]*:?-?[0-9]*$/', $s)) {
82
            return false;
83
        }
84
85
        $slice = self::parseSliceString($s);
86
87
        return !(\count($slice) < 1 || \count($slice) > 3);
88
    }
89
90
    /**
91
     * @param mixed $s
92
     *
93
     * @return bool
94
     */
95
    public static function isSliceArray($s): bool
96
    {
97
        if (!\is_array($s)) {
98
            return false;
99
        }
100
101
        if (\count($s) > 3) {
102
            return false;
103
        }
104
105
        foreach ($s as $key => $item) {
106
            if (\is_string($key)) {
107
                return false;
108
            }
109
            if ($item !== null && (!\is_numeric($item) || \is_float($item + 0))) {
110
                return false;
111
            }
112
        }
113
114
        return true;
115
    }
116
117
    /**
118
     * @param int|null $start
119
     * @param int|null $end
120
     * @param int|null $step
121
     */
122
    public function __construct(?int $start = null, ?int $end = null, ?int $step = null)
123
    {
124
        $this->start = $start;
0 ignored issues
show
Bug introduced by
The property start is declared read-only in Smoren\ArrayView\Structs\Slice.
Loading history...
125
        $this->end = $end;
0 ignored issues
show
Bug introduced by
The property end is declared read-only in Smoren\ArrayView\Structs\Slice.
Loading history...
126
        $this->step = $step;
0 ignored issues
show
Bug introduced by
The property step is declared read-only in Smoren\ArrayView\Structs\Slice.
Loading history...
127
    }
128
129
    /**
130
     * @param int $containerSize
131
     *
132
     * @return NormalizedSlice
133
     */
134
    public function normalize(int $containerSize): NormalizedSlice
135
    {
136
        $step = $this->step ?? 1;
137
138
        if ($step > 0) {
139
            return $this->normalizeWithPositiveStep($containerSize, $step);
140
        } elseif ($step < 0) {
141
            return $this->normalizeWithNegativeStep($containerSize, $step);
142
        }
143
144
        throw new IndexError("Step cannot be 0.");
145
    }
146
147
    /**
148
     * @return string
149
     */
150
    public function toString(): string
151
    {
152
        [$start, $end, $step] = [$this->start ?? '', $this->end ?? '', $this->step ?? ''];
153
        return "{$start}:{$end}:{$step}";
154
    }
155
156
    /**
157
     * @param string $s
158
     * @return array<int|null>
159
     */
160
    private static function parseSliceString(string $s): array
161
    {
162
        if ($s === '') {
163
            return [];
164
        }
165
        return array_map(fn($x) => trim($x) === '' ? null : \intval(trim($x)), \explode(':', $s));
166
    }
167
168
    /**
169
     * @param int $x
170
     * @param int $min
171
     * @param int $max
172
     * @return int
173
     */
174
    private function squeezeInBounds(int $x, int $min, int $max): int
175
    {
176
        return max($min, min($max, $x));
177
    }
178
179
    private function normalizeWithPositiveStep(int $containerSize, int $step): NormalizedSlice
180
    {
181
        $start = $this->start ?? 0;
182
        $end = $this->end ?? $containerSize;
183
184
        [$start, $end, $step] = [(int)\round($start), (int)\round($end), (int)\round($step)];
185
186
        $start = Util::normalizeIndex($start, $containerSize, false);
187
        $end = Util::normalizeIndex($end, $containerSize, false);
188
189
        if ($start >= $containerSize) {
190
            $start = $end = $containerSize - 1;
191
        }
192
193
        $start = $this->squeezeInBounds($start, 0, $containerSize - 1);
194
        $end = $this->squeezeInBounds($end, 0, $containerSize);
195
196
        if ($end < $start) {
197
            $end = $start;
198
        }
199
200
        return new NormalizedSlice($start, $end, $step);
201
    }
202
203
    private function normalizeWithNegativeStep(int $containerSize, int $step): NormalizedSlice
204
    {
205
        $start = $this->start ?? $containerSize - 1;
206
        $end = $this->end ?? -1;
207
208
        [$start, $end, $step] = [(int)\round($start), (int)\round($end), (int)\round($step)];
209
210
        $start = Util::normalizeIndex($start, $containerSize, false);
211
212
        if (!($this->end === null)) {
213
            $end = Util::normalizeIndex($end, $containerSize, false);
214
        }
215
216
        if ($start < 0) {
217
            $start = $end = 0;
218
        }
219
220
        $start = $this->squeezeInBounds($start, 0, $containerSize - 1);
221
        $end = $this->squeezeInBounds($end, -1, $containerSize);
222
223
        if ($end > $start) {
224
            $end = $start;
225
        }
226
227
        return new NormalizedSlice($start, $end, $step);
228
    }
229
}
230