Passed
Push — master ( 1ee408...ec3fab )
by ignace nyamagana
02:22
created

Sequence::map()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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