Completed
Pull Request — master (#73)
by ignace nyamagana
03:39
created

Sequence::getUnions()   A

Complexity

Conditions 6
Paths 8

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 14
nc 8
nop 0
dl 0
loc 24
ccs 15
cts 15
cp 1
crap 6
rs 9.2222
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * League.Period (https://period.thephpleague.com).
5
 *
6
 * (c) Ignace Nyamagana Butera <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace League\Period;
15
16
use ArrayAccess;
17
use Countable;
18
use Iterator;
19
use IteratorAggregate;
20
use JsonSerializable;
21
use function array_filter;
22
use function array_merge;
23
use function array_splice;
24
use function array_unshift;
25
use function array_values;
26
use function count;
27
use function reset;
28
use function sort;
29
use function sprintf;
30
use function uasort;
31
use function usort;
32
use const ARRAY_FILTER_USE_BOTH;
33
34
/**
35
 * A class to manipulate interval collection.
36
 *
37
 * @package League.period
38
 * @author  Ignace Nyamagana Butera <[email protected]>
39
 * @since   4.1.0
40
 */
41
final class Sequence implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable
42
{
43
    /**
44
     * @var Period[]
45
     */
46
    private $intervals = [];
47
48
    /**
49
     * new instance.
50
     *
51
     * @param Period ...$intervals
52
     */
53 84
    public function __construct(Period ...$intervals)
54
    {
55 84
        $this->intervals = $intervals;
56 84
    }
57
58
    /**
59
     * Returns the sequence boundaries as a Period instance.
60
     *
61
     * If the sequence contains no interval null is returned.
62
     *
63
     * @return ?Period
64
     */
65 9
    public function getBoundaries(): ?Period
66
    {
67 9
        $period = reset($this->intervals);
68 9
        if (false === $period) {
69 6
            return null;
70
        }
71
72 9
        return $period->merge(...$this->intervals);
73
    }
74
75
    /**
76
     * Returns the gaps inside the instance.
77
     */
78 9
    public function getGaps(): self
79
    {
80 9
        $sequence = new self();
81 9
        $interval = null;
82 9
        foreach ($this->sorted([$this, 'sortByStartDate']) as $period) {
83 9
            if (null === $interval) {
84 9
                $interval = $period;
85 9
                continue;
86
            }
87
88 9
            if (!$interval->overlaps($period) && !$interval->abuts($period)) {
89 6
                $sequence->push($interval->gap($period));
90
            }
91
92 9
            if (!$interval->contains($period)) {
93 9
                $interval = $period;
94
            }
95
        }
96
97 9
        return $sequence;
98
    }
99
100
    /**
101
     * Sorts two Interval instance using their start datepoint.
102
     */
103 15
    private function sortByStartDate(Period $interval1, Period $interval2): int
104
    {
105 15
        return $interval1->getStartDate() <=> $interval2->getStartDate();
106
    }
107
108
    /**
109
     * Returns the intersections inside the instance.
110
     */
111 9
    public function getIntersections(): self
112
    {
113 9
        $sequence = new self();
114 9
        $current = null;
115 9
        $isPreviouslyContained = false;
116 9
        foreach ($this->sorted([$this, 'sortByStartDate']) as $period) {
117 9
            if (null === $current) {
118 9
                $current = $period;
119 9
                continue;
120
            }
121
122 9
            $isContained = $current->contains($period);
123 9
            if ($isContained && $isPreviouslyContained) {
124 3
                continue;
125
            }
126
127 9
            if ($current->overlaps($period)) {
128 6
                $sequence->push($current->intersect($period));
129
            }
130
131 9
            $isPreviouslyContained = $isContained;
132 9
            if (!$isContained) {
133 9
                $current = $period;
134
            }
135
        }
136
137 9
        return $sequence;
138
    }
139
140
    /**
141
     * Returns the uninon inside the instance.
142
     */
143 6
    public function getUnions(): self
144
    {
145 6
        $sequence = new self();
146 6
        foreach ($this->sorted([$this, 'sortByStartDate']) as $period) {
147 6
            if ($sequence->isEmpty()) {
148 6
                $sequence->push($period);
149 6
                continue;
150
            }
151
152 3
            $index = $sequence->count() - 1;
153 3
            $interval = $sequence->get($index);
154 3
            if ($interval->overlaps($period) || $interval->abuts($period)) {
155 3
                $sequence->set($index, $interval->merge($period));
156 3
                continue;
157
            }
158
159 3
            $sequence->push($period);
160
        }
161
162 6
        if ($sequence->intervals === $this->intervals) {
163 3
            return $this;
164
        }
165
166 3
        return $sequence;
167
    }
168
169
    /**
170
     * Tells whether some intervals in the current instance satisfies the predicate.
171
     */
172 3
    public function some(callable $predicate): bool
173
    {
174 3
        foreach ($this->intervals as $offset => $interval) {
175 3
            if (true === $predicate($interval, $offset)) {
176 3
                return true;
177
            }
178
        }
179
180 3
        return false;
181
    }
182
183
    /**
184
     * Tells whether all intervals in the current instance satisfies the predicate.
185
     */
186 3
    public function every(callable $predicate): bool
187
    {
188 3
        foreach ($this->intervals as $offset => $interval) {
189 3
            if (true !== $predicate($interval, $offset)) {
190 3
                return false;
191
            }
192
        }
193
194 3
        return [] !== $this->intervals;
195
    }
196
197
    /**
198
     * Returns the array representation of the sequence.
199
     *
200
     * @return Period[]
201
     */
202 6
    public function toArray(): array
203
    {
204 6
        return $this->intervals;
205
    }
206
207
    /**
208
     * {@inheritdoc}
209
     */
210 3
    public function jsonSerialize(): array
211
    {
212 3
        return $this->intervals;
213
    }
214
215
    /**
216
     * {@inheritdoc}
217
     */
218 21
    public function getIterator(): Iterator
219
    {
220 21
        foreach ($this->intervals as $offset => $interval) {
221 21
            yield $offset => $interval;
222
        }
223 21
    }
224
225
    /**
226
     * {@inheritdoc}
227
     */
228 30
    public function count(): int
229
    {
230 30
        return count($this->intervals);
231
    }
232
233
    /**
234
     * @inheritdoc
235
     *
236
     * @param int $offset
237
     */
238 3
    public function offsetExists($offset): bool
239
    {
240 3
        return isset($this->intervals[$offset]);
241
    }
242
243
    /**
244
     * @inheritdoc
245
     *
246
     * @param int $offset
247
     */
248 3
    public function offsetGet($offset): Period
249
    {
250 3
        return $this->get($offset);
251
    }
252
253
    /**
254
     * @inheritdoc
255
     *
256
     * @param int $offset
257
     */
258 6
    public function offsetUnset($offset): void
259
    {
260 6
        $this->remove($offset);
261 3
    }
262
263
    /**
264
     * @inheritdoc
265
     *
266
     * @param mixed|int $offset
267
     * @param Period    $interval
268
     */
269 9
    public function offsetSet($offset, $interval): void
270
    {
271 9
        if (null !== $offset) {
272 9
            $this->set($offset, $interval);
273 3
            return;
274
        }
275
276 3
        $this->push($interval);
277 3
    }
278
279
    /**
280
     * Tells whether the sequence is empty.
281
     */
282 15
    public function isEmpty(): bool
283
    {
284 15
        return [] === $this->intervals;
285
    }
286
287
    /**
288
     * Tells whether the given interval is present in the sequence.
289
     *
290
     * @param Period ...$intervals
291
     */
292 6
    public function contains(Period $interval, Period ...$intervals): bool
293
    {
294 6
        $intervals[] = $interval;
295 6
        foreach ($intervals as $period) {
296 6
            if (false === $this->indexOf($period)) {
297 6
                return false;
298
            }
299
        }
300
301 6
        return true;
302
    }
303
304
    /**
305
     * Attempts to find the first offset attached to the submitted interval.
306
     *
307
     * If no offset is found the method returns boolean false.
308
     *
309
     * @return int|bool
310
     */
311 6
    public function indexOf(Period $interval)
312
    {
313 6
        foreach ($this->intervals as $offset => $period) {
314 6
            if ($period->equals($interval)) {
315 6
                return $offset;
316
            }
317
        }
318
319 6
        return false;
320
    }
321
322
    /**
323
     * Returns the interval specified at a given offset.
324
     *
325
     * @throws InvalidIndex If the offset is illegal for the current sequence
326
     */
327 39
    public function get(int $offset): Period
328
    {
329 39
        $period = $this->intervals[$offset] ?? null;
330 39
        if (null !== $period) {
331 30
            return $period;
332
        }
333
334 15
        throw new InvalidIndex(sprintf('%s is an invalid offset in the current sequence', $offset));
335
    }
336
337
    /**
338
     * Sort the current instance according to the given comparison callable
339
     * and maintain index association.
340
     *
341
     * Returns true on success or false on failure
342
     */
343 6
    public function sort(callable $compare): bool
344
    {
345 6
        return uasort($this->intervals, $compare);
346
    }
347
348
    /**
349
     * Adds new intervals at the front of the sequence.
350
     *
351
     * The sequence is re-indexed after addition
352
     *
353
     * @param Period ...$intervals
354
     */
355 3
    public function unshift(Period $interval, Period ...$intervals): void
356
    {
357 3
        $this->intervals = array_merge([$interval], $intervals, $this->intervals);
358 3
    }
359
360
    /**
361
     * Adds new intervals at the end of the sequence.
362
     *
363
     * @param Period ...$intervals
364
     */
365 24
    public function push(Period $interval, Period ...$intervals): void
366
    {
367 24
        $this->intervals = array_merge($this->intervals, [$interval], $intervals);
368 24
    }
369
370
    /**
371
     * Inserts new intervals at the specified offset of the sequence.
372
     *
373
     * The sequence is re-indexed after addition
374
     *
375
     * @param Period ...$intervals
376
     *
377
     * @throws InvalidIndex If the offset is illegal for the current sequence.
378
     */
379 3
    public function insert(int $offset, Period $interval, Period ...$intervals): void
380
    {
381 3
        if ($offset < 0 || $offset > count($this->intervals)) {
382 3
            throw new InvalidIndex(sprintf('%s is an invalid offset in the current sequence', $offset));
383
        }
384
385 3
        array_unshift($intervals, $interval);
386 3
        array_splice($this->intervals, $offset, 0, $intervals);
387 3
    }
388
389
    /**
390
     * Updates the interval at the specify offset.
391
     *
392
     * @throws InvalidIndex If the offset is illegal for the current sequence.
393
     */
394 12
    public function set(int $offset, Period $interval): void
395
    {
396 12
        $this->get($offset);
397 9
        $this->intervals[$offset] = $interval;
398 9
    }
399
400
    /**
401
     * Removes an interval from the sequence at the given offset and returns it.
402
     *
403
     * The sequence is re-indexed after removal
404
     *
405
     * @throws InvalidIndex If the offset is illegal for the current sequence.
406
     */
407 9
    public function remove(int $offset): Period
408
    {
409 9
        $interval = $this->get($offset);
410 6
        unset($this->intervals[$offset]);
411
412 6
        $this->intervals = array_values($this->intervals);
413
414 6
        return $interval;
415
    }
416
417
    /**
418
     * Filters the sequence according to the given predicate.
419
     *
420
     * This method MUST retain the state of the current instance, and return
421
     * an instance that contains the interval which validate the predicate.
422
     */
423 6
    public function filter(callable $predicate): self
424
    {
425 6
        $intervals = array_filter($this->intervals, $predicate, ARRAY_FILTER_USE_BOTH);
426 6
        if ($intervals === $this->intervals) {
427 3
            return $this;
428
        }
429
430 3
        return new self(...$intervals);
431
    }
432
433
    /**
434
     * Removes all intervals from the sequence.
435
     */
436 3
    public function clear(): void
437
    {
438 3
        $this->intervals = [];
439 3
    }
440
441
    /**
442
     * Returns an instance sorted according to the given comparison callable
443
     * but does not maintain index association.
444
     *
445
     * This method MUST retain the state of the current instance, and return
446
     * an instance that contains the sorted intervals. The key are re-indexed
447
     */
448 24
    public function sorted(callable $compare): self
449
    {
450 24
        $intervals = $this->intervals;
451 24
        usort($intervals, $compare);
452 24
        if ($intervals === $this->intervals) {
453 12
            return $this;
454
        }
455
456 15
        return new self(...$intervals);
457
    }
458
459
    /**
460
     * Returns an instance where the given function is applied to each element in
461
     * the collection. The callable MUST return a Period object and takes a Period
462
     * and its associated key as argument.
463
     *
464
     * This method MUST retain the state of the current instance, and return
465
     * an instance that contains the returned intervals.
466
     */
467 9
    public function map(callable $func): self
468
    {
469 9
        $intervals = [];
470 9
        foreach ($this->intervals as $offset => $interval) {
471 9
            $intervals[$offset] = $func($interval, $offset);
472
        }
473
474 9
        if ($intervals === $this->intervals) {
475 3
            return $this;
476
        }
477
478 6
        $mapped = new self();
479 6
        $mapped->intervals = $intervals;
480
481 6
        return $mapped;
482
    }
483
}
484