Passed
Push — master ( 5a05c2...5497cd )
by Edward
02:43
created

RangeSet   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 254
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 42
eloc 95
c 2
b 0
f 0
dl 0
loc 254
rs 9.0399

14 Methods

Rating   Name   Duplication   Size   Complexity  
A isEmpty() 0 3 1
A getSortedRanges() 0 5 1
A getRanges() 0 3 1
A compareRanges() 0 3 1
A createUnion() 0 3 1
A createUnsafe() 0 6 1
A withRanges() 0 3 1
A findRangeWithMinimalStart() 0 13 4
A equals() 0 14 4
B createSymmetricDifference() 0 38 9
A selectRanges() 0 3 1
A createRangePicker() 0 21 4
B mergeRanges() 0 23 7
A createIntersection() 0 26 6

How to fix   Complexity   

Complex Class

Complex classes like RangeSet often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use RangeSet, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Remorhaz\IntRangeSets;
6
7
use Generator;
8
9
use function array_search;
10
use function count;
11
use function usort;
12
13
/**
14
 * Immutable set of integer ranges.
15
 */
16
final class RangeSet implements RangeSetInterface
17
{
18
19
    /**
20
     * @var RangeInterface[]
21
     */
22
    private $ranges = [];
23
24
    /**
25
     * Provided ranges must be sorted, must not overlap and must not follow each other without gaps.
26
     * Warning: no checks are made! Use {@see RangeSet::createUnion()} to create set from arbitrary list of ranges.
27
     *
28
     * @param RangeInterface ...$ranges
29
     * @return static
30
     */
31
    public static function createUnsafe(RangeInterface ...$ranges): self
32
    {
33
        $rangeSet = new self();
34
        $rangeSet->ranges = $ranges;
35
36
        return $rangeSet;
37
    }
38
39
    /**
40
     * {@inheritDoc}
41
     *
42
     * @param RangeSetInterface $rangeSet
43
     * @return RangeSetInterface
44
     */
45
    public function createUnion(RangeSetInterface $rangeSet): RangeSetInterface
46
    {
47
        return $this->mergeRanges(...$rangeSet->getRanges());
48
    }
49
50
    /**
51
     * {@inheritDoc}
52
     *
53
     * @param RangeInterface ...$ranges
54
     * @return RangeSetInterface
55
     */
56
    public function withRanges(RangeInterface ...$ranges): RangeSetInterface
57
    {
58
        return $this->mergeRanges(...$this->getSortedRanges(...$ranges));
59
    }
60
61
    /**
62
     * @param RangeInterface ...$ranges
63
     * @return RangeInterface[]
64
     * @psalm-return array<int,RangeInterface>
65
     */
66
    private function getSortedRanges(RangeInterface ...$ranges): array
67
    {
68
        usort($ranges, [$this, 'compareRanges']);
69
70
        return $ranges;
71
    }
72
73
    private function compareRanges(RangeInterface $firstRange, RangeInterface $secondRange): int
74
    {
75
        return $firstRange->getStart() <=> $secondRange->getStart();
76
    }
77
78
    private function mergeRanges(RangeInterface ...$ranges): RangeSetInterface
79
    {
80
        $resultRanges = [];
81
        /** @var RangeInterface|null $rangeBuffer */
82
        $rangeBuffer = null;
83
        foreach ($this->createRangePicker($this->ranges, $ranges) as $pickedRange) {
84
            if (isset($rangeBuffer)) {
85
                if ($rangeBuffer->containsValue($pickedRange->getFinish())) {
86
                    continue;
87
                }
88
                if ($rangeBuffer->containsValue($pickedRange->getStart()) || $pickedRange->follows($rangeBuffer)) {
89
                    $rangeBuffer = $pickedRange->withStart($rangeBuffer->getStart());
90
                    continue;
91
                }
92
                $resultRanges[] = $rangeBuffer;
93
            }
94
            $rangeBuffer = $pickedRange;
95
        }
96
        if (isset($rangeBuffer)) {
97
            $resultRanges[] = $rangeBuffer;
98
        }
99
100
        return self::createUnsafe(...$resultRanges);
101
    }
102
103
    public function createSymmetricDifference(RangeSetInterface $rangeSet): RangeSetInterface
104
    {
105
        $resultRanges = [];
106
        /** @var RangeInterface|null $rangeBuffer */
107
        $rangeBuffer = null;
108
        foreach ($this->createRangePicker($this->ranges, $rangeSet->getRanges()) as $pickedRange) {
109
            if (isset($rangeBuffer)) {
110
                if ($rangeBuffer->intersects($pickedRange)) {
111
                    $pickedRangeStart = $pickedRange->getStart();
112
                    if ($rangeBuffer->getStart() < $pickedRangeStart) {
113
                        $resultRanges[] = $rangeBuffer->withFinish($pickedRangeStart - 1);
114
                        $rangeBuffer = $rangeBuffer->withStart($pickedRangeStart);
115
                    }
116
117
                    $pickedRangeFinish = $pickedRange->getFinish();
118
                    $rangeBufferFinish = $rangeBuffer->getFinish();
119
                    if ($rangeBufferFinish < $pickedRangeFinish) {
120
                        $rangeBuffer = $pickedRange->withStart($rangeBufferFinish + 1);
121
                    } elseif ($pickedRangeFinish < $rangeBufferFinish) {
122
                        $rangeBuffer = $rangeBuffer->withStart($pickedRangeFinish + 1);
123
                    } else {
124
                        $rangeBuffer = null;
125
                    }
126
                    continue;
127
                }
128
                if ($pickedRange->follows($rangeBuffer)) {
129
                    $rangeBuffer = $rangeBuffer->withFinish($pickedRange->getFinish());
130
                    continue;
131
                }
132
                $resultRanges[] = $rangeBuffer;
133
            }
134
            $rangeBuffer = $pickedRange;
135
        }
136
        if (isset($rangeBuffer)) {
137
            $resultRanges[] = $rangeBuffer;
138
        }
139
140
        return self::createUnsafe(...$resultRanges);
141
    }
142
143
    /**
144
     * {@inheritDoc}
145
     *
146
     * @param RangeSetInterface $rangeSet
147
     * @return RangeSetInterface
148
     */
149
    public function createIntersection(RangeSetInterface $rangeSet): RangeSetInterface
150
    {
151
        $resultRanges = [];
152
        /** @var RangeInterface|null $rangeBuffer */
153
        $rangeBuffer = null;
154
        foreach ($this->createRangePicker($this->ranges, $rangeSet->getRanges()) as $pickedRange) {
155
            if (isset($rangeBuffer)) {
156
                if (!$rangeBuffer->intersects($pickedRange)) {
157
                    continue;
158
                }
159
                $pickedRangeStart = $pickedRange->getStart();
160
                if ($rangeBuffer->getStart() < $pickedRangeStart) {
161
                    $rangeBuffer = $rangeBuffer->withStart($pickedRangeStart);
162
                }
163
                $pickedRangeFinish = $pickedRange->getFinish();
164
                if ($rangeBuffer->getFinish() > $pickedRangeFinish) {
165
                    $resultRanges[] = $rangeBuffer->withFinish($pickedRangeFinish);
166
                    $rangeBuffer = $rangeBuffer->withStart($pickedRangeFinish + 1);
167
                    continue;
168
                }
169
                $resultRanges[] = $rangeBuffer;
170
            }
171
            $rangeBuffer = $pickedRange;
172
        }
173
174
        return self::createUnsafe(...$resultRanges);
175
    }
176
177
    public function equals(RangeSetInterface $rangeSet): bool
178
    {
179
        $ranges = $rangeSet->getRanges();
180
        if (count($this->ranges) != count($ranges)) {
181
            return false;
182
        }
183
184
        foreach ($this->ranges as $index => $range) {
185
            if (!$ranges[$index]->equals($range)) {
186
                return false;
187
            }
188
        }
189
190
        return true;
191
    }
192
193
    /**
194
     * @param RangeInterface[] ...$rangeLists
195
     * @return RangeInterface[]|Generator
196
     * @psalm-return Generator<int,RangeInterface>
197
     */
198
    private function createRangePicker(array ...$rangeLists): Generator
199
    {
200
        $rangeListKeys = array_keys($rangeLists);
201
        $indexes = array_fill_keys($rangeListKeys, 0);
202
203
        while (true) {
204
            $selectedRanges = array_map([$this, 'selectRanges'], $rangeLists, $indexes);
205
            $nextRange = array_reduce($selectedRanges, [$this, 'findRangeWithMinimalStart']);
206
            if (!isset($nextRange)) {
207
                break;
208
            }
209
210
            $nextRangeKey = array_search($nextRange, $selectedRanges);
211
            if (false === $nextRangeKey) {
212
                // @codeCoverageIgnoreStart
213
                throw new Exception\RangeNotPickedException();
214
                // @codeCoverageIgnoreEnd
215
            }
216
            $indexes[$nextRangeKey]++;
217
218
            yield $nextRange;
219
        }
220
    }
221
222
    /**
223
     * @param RangeInterface[] $ranges
224
     * @param int              $index
225
     * @return RangeInterface|null
226
     */
227
    private function selectRanges(array $ranges, int $index): ?RangeInterface
228
    {
229
        return $ranges[$index] ?? null;
230
    }
231
232
    /**
233
     * @param RangeInterface|null $previousRange
234
     * @param RangeInterface|null $range
235
     * @return RangeInterface|null
236
     */
237
    private function findRangeWithMinimalStart(?RangeInterface $previousRange, ?RangeInterface $range): ?RangeInterface
238
    {
239
        if (isset($previousRange)) {
240
            if (!isset($range)) {
241
                return $previousRange;
242
            }
243
244
            return $previousRange->getStart() < $range->getStart()
245
                ? $previousRange
246
                : $range;
247
        }
248
249
        return $range;
250
    }
251
252
    /**
253
     * {@inheritDoc}
254
     *
255
     * @return RangeInterface[]
256
     */
257
    public function getRanges(): array
258
    {
259
        return $this->ranges;
260
    }
261
262
    /**
263
     * {@inheritDoc}
264
     *
265
     * @return bool
266
     */
267
    public function isEmpty(): bool
268
    {
269
        return empty($this->ranges);
270
    }
271
}
272