Completed
Push — master ( 13c894...975657 )
by ignace nyamagana
03:21
created

Sequence::intersections()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 27
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 7

Importance

Changes 0
Metric Value
cc 7
eloc 16
nc 7
nop 0
dl 0
loc 27
ccs 17
cts 17
cp 1
crap 7
rs 8.8333
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 90
    public function __construct(Period ...$intervals)
54
    {
55 90
        $this->intervals = $intervals;
56 90
    }
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 15
    public function boundaries(): ?Period
66
    {
67 15
        $period = reset($this->intervals);
68 15
        if (false === $period) {
69 6
            return null;
70
        }
71
72 15
        return $period->merge(...$this->intervals);
73
    }
74
75
    /**
76
     * Returns the gaps inside the instance.
77
     */
78 9
    public function gaps(): 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 7
                $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 intersections(): 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 7
                $current = $period;
134
            }
135
        }
136
137 9
        return $sequence;
138
    }
139
140
    /**
141
     * Returns the unions inside the instance.
142
     */
143 6
    public function unions(): self
144
    {
145
        $sequence = $this
146 6
            ->sorted([$this, 'sortByStartDate'])
147 6
            ->reduce([$this, 'calculateUnion'], new self())
148
        ;
149
150 6
        if ($sequence->intervals === $this->intervals) {
151 3
            return $this;
152
        }
153
154 3
        return $sequence;
155
    }
156
157
    /**
158
     * Iteratively calculate the union sequence.
159
     */
160 6
    private function calculateUnion(Sequence $sequence, Period $period): Sequence
161
    {
162 6
        if ($sequence->isEmpty()) {
163 6
            $sequence->push($period);
164
165 6
            return $sequence;
166
        }
167
168 3
        $index = $sequence->count() - 1;
169 3
        $interval = $sequence->get($index);
170 3
        if ($interval->overlaps($period) || $interval->abuts($period)) {
171 3
            $sequence->set($index, $interval->merge($period));
172
173 3
            return $sequence;
174
        }
175
176 3
        $sequence->push($period);
177
178 3
        return $sequence;
179
    }
180
181
    /**
182
     * Returns the sequence boundaries as a Period instance.
183
     *
184
     * DEPRECATION WARNING! This method will be removed in the next major point release
185
     *
186
     * @deprecated deprecated since version 4.4.0
187
     * @see        ::boundaries
188
     *
189
     * If the sequence contains no interval null is returned.
190
     *
191
     * @return ?Period
192
     */
193 12
    public function getBoundaries(): ?Period
194
    {
195 12
        return $this->boundaries();
196
    }
197
198
    /**
199
     * Returns the intersections inside the instance.
200
     *
201
     * DEPRECATION WARNING! This method will be removed in the next major point release
202
     *
203
     * @deprecated deprecated since version 4.4.0
204
     * @see        ::intersections
205
     */
206 9
    public function getIntersections(): self
207
    {
208 9
        return $this->intersections();
209
    }
210
211
    /**
212
     * Returns the gaps inside the instance.
213
     *
214
     * DEPRECATION WARNING! This method will be removed in the next major point release
215
     *
216
     * @deprecated deprecated since version 4.4.0
217
     * @see        ::gaps
218
     */
219 9
    public function getGaps(): self
220
    {
221 9
        return $this->gaps();
222
    }
223
224
    /**
225
     * Returns the sum of all instances durations as expressed in seconds.
226
     */
227 3
    public function getTotalTimestampInterval(): float
228
    {
229 3
        $retval = 0;
230 3
        foreach ($this->intervals as $interval) {
231 3
            $retval += $interval->getTimestampInterval();
232
        }
233
234 3
        return $retval;
235
    }
236
237
    /**
238
     * Tells whether some intervals in the current instance satisfies the predicate.
239
     */
240 3
    public function some(callable $predicate): bool
241
    {
242 3
        foreach ($this->intervals as $offset => $interval) {
243 3
            if (true === $predicate($interval, $offset)) {
244 3
                return true;
245
            }
246
        }
247
248 3
        return false;
249
    }
250
251
    /**
252
     * Tells whether all intervals in the current instance satisfies the predicate.
253
     */
254 3
    public function every(callable $predicate): bool
255
    {
256 3
        foreach ($this->intervals as $offset => $interval) {
257 3
            if (true !== $predicate($interval, $offset)) {
258 3
                return false;
259
            }
260
        }
261
262 3
        return [] !== $this->intervals;
263
    }
264
265
    /**
266
     * Returns the array representation of the sequence.
267
     *
268
     * @return Period[]
269
     */
270 6
    public function toArray(): array
271
    {
272 6
        return $this->intervals;
273
    }
274
275
    /**
276
     * {@inheritdoc}
277
     */
278 3
    public function jsonSerialize(): array
279
    {
280 3
        return $this->intervals;
281
    }
282
283
    /**
284
     * {@inheritdoc}
285
     */
286 18
    public function getIterator(): Iterator
287
    {
288 18
        foreach ($this->intervals as $offset => $interval) {
289 18
            yield $offset => $interval;
290
        }
291 18
    }
292
293
    /**
294
     * {@inheritdoc}
295
     */
296 30
    public function count(): int
297
    {
298 30
        return count($this->intervals);
299
    }
300
301
    /**
302
     * @inheritdoc
303
     *
304
     * @param int $offset
305
     */
306 3
    public function offsetExists($offset): bool
307
    {
308 3
        return isset($this->intervals[$offset]);
309
    }
310
311
    /**
312
     * @inheritdoc
313
     *
314
     * @param int $offset
315
     */
316 3
    public function offsetGet($offset): Period
317
    {
318 3
        return $this->get($offset);
319
    }
320
321
    /**
322
     * @inheritdoc
323
     *
324
     * @param int $offset
325
     */
326 6
    public function offsetUnset($offset): void
327
    {
328 6
        $this->remove($offset);
329 3
    }
330
331
    /**
332
     * @inheritdoc
333
     *
334
     * @param mixed|int $offset
335
     * @param Period    $interval
336
     */
337 9
    public function offsetSet($offset, $interval): void
338
    {
339 9
        if (null !== $offset) {
340 9
            $this->set($offset, $interval);
341 3
            return;
342
        }
343
344 3
        $this->push($interval);
345 3
    }
346
347
    /**
348
     * Tells whether the sequence is empty.
349
     */
350 15
    public function isEmpty(): bool
351
    {
352 15
        return [] === $this->intervals;
353
    }
354
355
    /**
356
     * Tells whether the given interval is present in the sequence.
357
     *
358
     * @param Period ...$intervals
359
     */
360 6
    public function contains(Period $interval, Period ...$intervals): bool
361
    {
362 6
        $intervals[] = $interval;
363 6
        foreach ($intervals as $period) {
364 6
            if (false === $this->indexOf($period)) {
365 4
                return false;
366
            }
367
        }
368
369 6
        return true;
370
    }
371
372
    /**
373
     * Attempts to find the first offset attached to the submitted interval.
374
     *
375
     * If no offset is found the method returns boolean false.
376
     *
377
     * @return int|bool
378
     */
379 6
    public function indexOf(Period $interval)
380
    {
381 6
        foreach ($this->intervals as $offset => $period) {
382 6
            if ($period->equals($interval)) {
383 6
                return $offset;
384
            }
385
        }
386
387 6
        return false;
388
    }
389
390
    /**
391
     * Returns the interval specified at a given offset.
392
     *
393
     * @throws InvalidIndex If the offset is illegal for the current sequence
394
     */
395 39
    public function get(int $offset): Period
396
    {
397 39
        $period = $this->intervals[$offset] ?? null;
398 39
        if (null !== $period) {
399 30
            return $period;
400
        }
401
402 15
        throw new InvalidIndex(sprintf('%s is an invalid offset in the current sequence', $offset));
403
    }
404
405
    /**
406
     * Sort the current instance according to the given comparison callable
407
     * and maintain index association.
408
     *
409
     * Returns true on success or false on failure
410
     */
411 6
    public function sort(callable $compare): bool
412
    {
413 6
        return uasort($this->intervals, $compare);
414
    }
415
416
    /**
417
     * Adds new intervals at the front of the sequence.
418
     *
419
     * The sequence is re-indexed after addition
420
     *
421
     * @param Period ...$intervals
422
     */
423 3
    public function unshift(Period $interval, Period ...$intervals): void
424
    {
425 3
        $this->intervals = array_merge([$interval], $intervals, $this->intervals);
426 3
    }
427
428
    /**
429
     * Adds new intervals at the end of the sequence.
430
     *
431
     * @param Period ...$intervals
432
     */
433 24
    public function push(Period $interval, Period ...$intervals): void
434
    {
435 24
        $this->intervals = array_merge($this->intervals, [$interval], $intervals);
436 24
    }
437
438
    /**
439
     * Inserts new intervals at the specified offset of the sequence.
440
     *
441
     * The sequence is re-indexed after addition
442
     *
443
     * @param Period ...$intervals
444
     *
445
     * @throws InvalidIndex If the offset is illegal for the current sequence.
446
     */
447 3
    public function insert(int $offset, Period $interval, Period ...$intervals): void
448
    {
449 3
        if ($offset < 0 || $offset > count($this->intervals)) {
450 3
            throw new InvalidIndex(sprintf('%s is an invalid offset in the current sequence', $offset));
451
        }
452
453 3
        array_unshift($intervals, $interval);
454 3
        array_splice($this->intervals, $offset, 0, $intervals);
455 3
    }
456
457
    /**
458
     * Updates the interval at the specify offset.
459
     *
460
     * @throws InvalidIndex If the offset is illegal for the current sequence.
461
     */
462 12
    public function set(int $offset, Period $interval): void
463
    {
464 12
        $this->get($offset);
465 9
        $this->intervals[$offset] = $interval;
466 9
    }
467
468
    /**
469
     * Removes an interval from the sequence at the given offset and returns it.
470
     *
471
     * The sequence is re-indexed after removal
472
     *
473
     * @throws InvalidIndex If the offset is illegal for the current sequence.
474
     */
475 9
    public function remove(int $offset): Period
476
    {
477 9
        $interval = $this->get($offset);
478 6
        unset($this->intervals[$offset]);
479
480 6
        $this->intervals = array_values($this->intervals);
481
482 6
        return $interval;
483
    }
484
485
    /**
486
     * Filters the sequence according to the given predicate.
487
     *
488
     * This method MUST retain the state of the current instance, and return
489
     * an instance that contains the interval which validate the predicate.
490
     */
491 6
    public function filter(callable $predicate): self
492
    {
493 6
        $intervals = array_filter($this->intervals, $predicate, ARRAY_FILTER_USE_BOTH);
494 6
        if ($intervals === $this->intervals) {
495 3
            return $this;
496
        }
497
498 3
        return new self(...$intervals);
499
    }
500
501
    /**
502
     * Removes all intervals from the sequence.
503
     */
504 3
    public function clear(): void
505
    {
506 3
        $this->intervals = [];
507 3
    }
508
509
    /**
510
     * Returns an instance sorted according to the given comparison callable
511
     * but does not maintain index association.
512
     *
513
     * This method MUST retain the state of the current instance, and return
514
     * an instance that contains the sorted intervals. The key are re-indexed
515
     */
516 24
    public function sorted(callable $compare): self
517
    {
518 24
        $intervals = $this->intervals;
519 24
        usort($intervals, $compare);
520 24
        if ($intervals === $this->intervals) {
521 12
            return $this;
522
        }
523
524 15
        return new self(...$intervals);
525
    }
526
527
    /**
528
     * Returns an instance where the given function is applied to each element in
529
     * the collection. The callable MUST return a Period object and takes a Period
530
     * and its associated key as argument.
531
     *
532
     * This method MUST retain the state of the current instance, and return
533
     * an instance that contains the returned intervals.
534
     */
535 9
    public function map(callable $func): self
536
    {
537 9
        $intervals = [];
538 9
        foreach ($this->intervals as $offset => $interval) {
539 9
            $intervals[$offset] = $func($interval, $offset);
540
        }
541
542 9
        if ($intervals === $this->intervals) {
543 3
            return $this;
544
        }
545
546 6
        $mapped = new self();
547 6
        $mapped->intervals = $intervals;
548
549 6
        return $mapped;
550
    }
551
552
    /**
553
     * Iteratively reduces the sequence to a single value using a callback.
554
     *
555
     * @param callable $func Accepts the carry, the current value and the current offset, and
556
     *                       returns an updated carry value.
557
     *
558
     * @param mixed|null $carry Optional initial carry value.
559
     *
560
     * @return mixed The carry value of the final iteration, or the initial
561
     *               value if the sequence was empty.
562
     */
563 6
    public function reduce(callable $func, $carry = null)
564
    {
565 6
        foreach ($this->intervals as $offset => $interval) {
566 6
            $carry = $func($carry, $interval, $offset);
567
        }
568
569 6
        return $carry;
570
    }
571
}
572