Completed
Pull Request — master (#73)
by ignace nyamagana
04:44
created

Sequence::map()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

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