Passed
Push — master ( b61fa8...5a05c2 )
by Edward
02:32
created

RangeSet::createIntersection()   A

Complexity

Conditions 6
Paths 7

Size

Total Lines 26
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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