Passed
Pull Request — master (#11)
by Smoren
02:23
created

Slice::isSliceArray()   B

Complexity

Conditions 9
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.0555
cc 9
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
        if ($s instanceof Slice) {
37
            return $s;
38
        }
39
40
        if (!self::isSliceString($s)) {
41
            $str = \is_scalar($s) ? "{$s}" : gettype($s);
0 ignored issues
show
introduced by
The condition is_scalar($s) is always true.
Loading history...
42
            throw new ValueError("Invalid slice: \"{$str}\".");
43
        }
44
45
        $slice = self::parseSliceString($s);
46
47
        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

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