Passed
Push — master ( 0fb430...371745 )
by Smoren
02:22 queued 15s
created

Slice::isSliceArray()   B

Complexity

Conditions 8
Paths 6

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 10
c 0
b 0
f 0
dl 0
loc 20
rs 8.4444
cc 8
nc 6
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 $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 (!self::isSliceString($s)) {
42
            $str = \is_scalar($s) ? "{$s}" : gettype($s);
43
            throw new ValueError("Invalid slice: \"{$str}\".");
44
        }
45
46
        /** @var string $s */
47
        $slice = self::parseSliceString($s);
48
49
        return new Slice(...$slice);
0 ignored issues
show
Bug introduced by
$slice 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

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