Passed
Push — master ( 7aa466...8ea880 )
by Smoren
03:23 queued 01:36
created

Slice::getStep()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Smoren\ArrayView\Structs;
6
7
use Smoren\ArrayView\Exceptions\IndexError;
8
use Smoren\ArrayView\Exceptions\ValueError;
9
use Smoren\ArrayView\Util;
10
11
/**
12
 * Represents a slice definition for selecting a range of elements.
13
 */
14
class Slice
15
{
16
    /**
17
     * @var int|null The start index of the slice range.
18
     */
19
    protected ?int $start;
20
    /**
21
     * @var int|null The end index of the slice range.
22
     */
23
    protected ?int $end;
24
    /**
25
     * @var int|null The step size for selecting elements in the slice range.
26
     */
27
    protected ?int $step;
28
29
    /**
30
     * Converts a slice string or Slice object into a Slice instance.
31
     *
32
     * @param string|Slice|array<int> $s The slice string/array or Slice object to convert.
33
     *
34
     * @return Slice The converted Slice instance.
35
     *
36
     * @throws ValueError if the slice representation is invalid.
37
     */
38
    public static function toSlice($s): Slice
39
    {
40
        /** @var mixed $s */
41
        if ($s instanceof Slice) {
42
            return $s;
43
        }
44
45
        if (\is_array($s) && self::isSliceArray($s)) {
46
            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

46
            return new Slice(/** @scrutinizer ignore-type */ ...$s);
Loading history...
47
        }
48
49
        if (!self::isSliceString($s)) {
50
            $str = \is_scalar($s) ? "{$s}" : gettype($s);
51
            throw new ValueError("Invalid slice: \"{$str}\".");
52
        }
53
54
        /** @var string $s */
55
        $slice = self::parseSliceString($s);
56
57
        return new Slice(...$slice);
58
    }
59
60
    /**
61
     * Checks if the provided value is a Slice instance or a valid slice string.
62
     *
63
     * @param mixed $s The value to check.
64
     *
65
     * @return bool True if the value is a Slice instance or a valid slice string, false otherwise.
66
     */
67
    public static function isSlice($s): bool
68
    {
69
        return ($s instanceof Slice) || static::isSliceString($s) || static::isSliceArray($s);
70
    }
71
72
    /**
73
     * Checks if the provided value is a valid slice string.
74
     *
75
     * @param mixed $s The value to check.
76
     *
77
     * @return bool True if the value is a valid slice string, false otherwise.
78
     */
79
    public static function isSliceString($s): bool
80
    {
81
        if (!\is_string($s)) {
82
            return false;
83
        }
84
85
        if (\is_numeric($s)) {
86
            return false;
87
        }
88
89
        if (!\preg_match('/^-?[0-9]*:?-?[0-9]*:?-?[0-9]*$/', $s)) {
90
            return false;
91
        }
92
93
        $slice = self::parseSliceString($s);
94
95
        return !(\count($slice) < 1 || \count($slice) > 3);
96
    }
97
98
    /**
99
     * Checks if the provided value is a valid slice array.
100
     *
101
     * @param mixed $s The value to check.
102
     *
103
     * @return bool True if the value is a valid slice array, false otherwise.
104
     */
105
    public static function isSliceArray($s): bool
106
    {
107
        if (!\is_array($s)) {
108
            return false;
109
        }
110
111
        if (\count($s) > 3) {
112
            return false;
113
        }
114
115
        foreach ($s as $key => $item) {
116
            if (\is_string($key)) {
117
                return false;
118
            }
119
            if ($item !== null && (!\is_numeric($item) || \is_float($item + 0))) {
120
                return false;
121
            }
122
        }
123
124
        return true;
125
    }
126
127
    /**
128
     * Creates a new Slice instance with optional start, end, and step values.
129
     *
130
     * @param int|null $start The start index of the slice range.
131
     * @param int|null $end The end index of the slice range.
132
     * @param int|null $step The step size for selecting elements in the slice range.
133
     */
134
    public function __construct(?int $start = null, ?int $end = null, ?int $step = null)
135
    {
136
        $this->start = $start;
137
        $this->end = $end;
138
        $this->step = $step;
139
    }
140
141
    /**
142
     * Getter for the start index of the normalized slice.
143
     *
144
     * @return int|null
145
     */
146
    public function getStart(): ?int
147
    {
148
        return $this->start;
149
    }
150
151
    /**
152
     * Getter for the stop index of the normalized slice.
153
     *
154
     * @return int|null
155
     */
156
    public function getEnd(): ?int
157
    {
158
        return $this->end;
159
    }
160
161
    /**
162
     * Getter for the step of the normalized slice.
163
     *
164
     * @return int|null
165
     */
166
    public function getStep(): ?int
167
    {
168
        return $this->step;
169
    }
170
171
    /**
172
     * Normalizes the slice parameters based on the container length.
173
     *
174
     * @param int $containerSize The length of the container or array.
175
     *
176
     * @return NormalizedSlice The normalized slice parameters.
177
     *
178
     * @throws IndexError if the step value is 0.
179
     */
180
    public function normalize(int $containerSize): NormalizedSlice
181
    {
182
        $step = $this->step ?? 1;
183
184
        if ($step > 0) {
185
            return $this->normalizeWithPositiveStep($containerSize, $step);
186
        } elseif ($step < 0) {
187
            return $this->normalizeWithNegativeStep($containerSize, $step);
188
        }
189
190
        throw new IndexError("Step cannot be 0.");
191
    }
192
193
    /**
194
     * Returns the string representation of the Slice.
195
     *
196
     * @return string The string representation of the Slice.
197
     */
198
    public function toString(): string
199
    {
200
        [$start, $end, $step] = [$this->start ?? '', $this->end ?? '', $this->step ?? ''];
201
        return "{$start}:{$end}:{$step}";
202
    }
203
204
    /**
205
     * Parses a slice string into an array of start, end, and step values.
206
     *
207
     * @param string $s The slice string to parse.
208
     *
209
     * @return array<int|null> An array of parsed start, end, and step values.
210
     */
211
    private static function parseSliceString(string $s): array
212
    {
213
        if ($s === '') {
214
            return [];
215
        }
216
        return array_map(fn($x) => trim($x) === '' ? null : \intval(trim($x)), \explode(':', $s));
217
    }
218
219
    /**
220
     * Constrains a value within a given range.
221
     *
222
     * @param int $x The value to constrain.
223
     * @param int $min The minimum allowed value.
224
     * @param int $max The maximum allowed value.
225
     *
226
     * @return int The constrained value.
227
     */
228
    private function squeezeInBounds(int $x, int $min, int $max): int
229
    {
230
        return max($min, min($max, $x));
231
    }
232
233
    /**
234
     * Normalizes the slice parameters based on the container length (for positive step only).
235
     *
236
     * @param int $containerSize The length of the container or array.
237
     * @param int $step Step size.
238
     *
239
     * @return NormalizedSlice The normalized slice parameters.
240
     */
241
    private function normalizeWithPositiveStep(int $containerSize, int $step): NormalizedSlice
242
    {
243
        $start = $this->start ?? 0;
244
        $end = $this->end ?? $containerSize;
245
246
        [$start, $end, $step] = [(int)\round($start), (int)\round($end), (int)\round($step)];
247
248
        $start = Util::normalizeIndex($start, $containerSize, false);
249
        $end = Util::normalizeIndex($end, $containerSize, false);
250
251
        if ($start >= $containerSize) {
252
            $start = $end = $containerSize - 1;
253
        }
254
255
        $start = $this->squeezeInBounds($start, 0, $containerSize - 1);
256
        $end = $this->squeezeInBounds($end, 0, $containerSize);
257
258
        if ($end < $start) {
259
            $end = $start;
260
        }
261
262
        return new NormalizedSlice($start, $end, $step);
263
    }
264
265
    /**
266
     * Normalizes the slice parameters based on the container length (for negative step only).
267
     *
268
     * @param int $containerSize The length of the container or array.
269
     * @param int $step Step size.
270
     *
271
     * @return NormalizedSlice The normalized slice parameters.
272
     */
273
    private function normalizeWithNegativeStep(int $containerSize, int $step): NormalizedSlice
274
    {
275
        $start = $this->start ?? $containerSize - 1;
276
        $end = $this->end ?? -1;
277
278
        [$start, $end, $step] = [(int)\round($start), (int)\round($end), (int)\round($step)];
279
280
        $start = Util::normalizeIndex($start, $containerSize, false);
281
282
        if (!($this->end === null)) {
283
            $end = Util::normalizeIndex($end, $containerSize, false);
284
        }
285
286
        if ($start < 0) {
287
            $start = $end = 0;
288
        }
289
290
        $start = $this->squeezeInBounds($start, 0, $containerSize - 1);
291
        $end = $this->squeezeInBounds($end, -1, $containerSize);
292
293
        if ($end > $start) {
294
            $end = $start;
295
        }
296
297
        return new NormalizedSlice($start, $end, $step);
298
    }
299
}
300