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

Period::containsInterval()   C

Complexity

Conditions 12
Paths 10

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 12

Importance

Changes 0
Metric Value
cc 12
eloc 11
nc 10
nop 1
dl 0
loc 21
ccs 12
cts 12
cp 1
crap 12
rs 6.9666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 DateInterval;
17
use DatePeriod;
18
use DateTimeImmutable;
19
use DateTimeInterface;
20
use DateTimeZone;
21
use JsonSerializable;
22
use function array_keys;
23
use function implode;
24
use function sprintf;
25
26
/**
27
 * A immutable value object class to manipulate Time interval.
28
 *
29
 * @package League.period
30
 * @author  Ignace Nyamagana Butera <[email protected]>
31
 * @since   1.0.0
32
 */
33
final class Period implements JsonSerializable
34
{
35
    private const ISO8601_FORMAT = 'Y-m-d\TH:i:s.u\Z';
36
37
    private const BOUNDARY_TYPE = [
38
        self::INCLUDE_START_EXCLUDE_END => 1,
39
        self::INCLUDE_ALL => 1,
40
        self::EXCLUDE_START_INCLUDE_END => 1,
41
        self::EXCLUDE_ALL => 1,
42
    ];
43
44
    public const INCLUDE_START_EXCLUDE_END = '[)';
45
46
    public const EXCLUDE_START_INCLUDE_END = '(]';
47
48
    public const EXCLUDE_ALL = '()';
49
50
    public const INCLUDE_ALL = '[]';
51
52
    /**
53
     * The starting datepoint.
54
     *
55
     * @var DateTimeImmutable
56
     */
57
    private $startDate;
58
59
    /**
60
     * The ending datepoint.
61
     *
62
     * @var DateTimeImmutable
63
     */
64
    private $endDate;
65
66
    /**
67
     * The boundary type.
68
     *
69
     * @var string
70
     */
71
    private $boundaryType;
72
73
    /**
74
     * Returns a DateTimeImmutable instance.
75
     *
76
     * @param mixed $datepoint a Datepoint
77
     */
78 936
    private static function getDatepoint($datepoint): DateTimeImmutable
79
    {
80 936
        if ($datepoint instanceof DateTimeImmutable) {
81 729
            return $datepoint;
82
        }
83
84 678
        return Datepoint::create($datepoint);
85
    }
86
87
    /**
88
     * Returns a DateInterval instance.
89
     *
90
     * @param mixed $duration a Duration
91
     */
92 342
    private static function getDuration($duration): DateInterval
93
    {
94 342
        if ($duration instanceof DateInterval) {
95 150
            return $duration;
96
        }
97
98 192
        return Duration::create($duration);
99
    }
100
101
    /**************************************************
102
     * Named constructors
103
     **************************************************/
104
105
    /**
106
     * @inheritdoc
107
     */
108 6
    public static function __set_state(array $interval)
109
    {
110 6
        return new self($interval['startDate'], $interval['endDate'], $interval['boundaryType'] ?? self::INCLUDE_START_EXCLUDE_END);
111
    }
112
113
    /**
114
     * Creates new instance from a starting datepoint and a duration.
115
     *
116
     * @param mixed $startDate the starting included datepoint
117
     * @param mixed $duration  a Duration
118
     */
119 117
    public static function after($startDate, $duration, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
120
    {
121 117
        $startDate = self::getDatepoint($startDate);
122
123 117
        return new self($startDate, $startDate->add(self::getDuration($duration)), $boundaryType);
124
    }
125
126
    /**
127
     * Creates new instance from a ending datepoint and a duration.
128
     *
129
     * @param mixed $endDate  the ending excluded datepoint
130
     * @param mixed $duration a Duration
131
     */
132 24
    public static function before($endDate, $duration, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
133
    {
134 24
        $endDate = self::getDatepoint($endDate);
135
136 24
        return new self($endDate->sub(self::getDuration($duration)), $endDate, $boundaryType);
137
    }
138
139
    /**
140
     * Creates new instance where the given duration is simultaneously
141
     * substracted from and added to the datepoint.
142
     *
143
     * @param mixed $datepoint a Datepoint
144
     * @param mixed $duration  a Duration
145
     */
146 21
    public static function around($datepoint, $duration, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
147
    {
148 21
        $datepoint = self::getDatepoint($datepoint);
149 21
        $duration = self::getDuration($duration);
150
151 21
        return new self($datepoint->sub($duration), $datepoint->add($duration), $boundaryType);
152
    }
153
154
    /**
155
     * Creates new instance from a DatePeriod.
156
     */
157 12
    public static function fromDatePeriod(DatePeriod $datePeriod, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
158
    {
159 12
        return new self($datePeriod->getStartDate(), $datePeriod->getEndDate(), $boundaryType);
160
    }
161
162
    /**
163
     * Creates new instance for a specific year.
164
     */
165 12
    public static function fromYear(int $year): self
166
    {
167 12
        $startDate = (new DateTimeImmutable())->setDate($year, 1, 1)->setTime(0, 0);
168
169 12
        return new self($startDate, $startDate->add(new DateInterval('P1Y')));
170
    }
171
172
    /**
173
     * Creates new instance for a specific ISO year.
174
     */
175 6
    public static function fromIsoYear(int $year): self
176
    {
177 6
        return new self(
178 6
            (new DateTimeImmutable())->setISODate($year, 1)->setTime(0, 0),
179 6
            (new DateTimeImmutable())->setISODate(++$year, 1)->setTime(0, 0)
180
        );
181
    }
182
183
    /**
184
     * Creates new instance for a specific year and semester.
185
     */
186 18
    public static function fromSemester(int $year, int $semester = 1): self
187
    {
188 18
        $month = (($semester - 1) * 6) + 1;
189 18
        $startDate = (new DateTimeImmutable())->setDate($year, $month, 1)->setTime(0, 0);
190
191 18
        return new self($startDate, $startDate->add(new DateInterval('P6M')));
192
    }
193
194
    /**
195
     * Creates new instance for a specific year and quarter.
196
     */
197 18
    public static function fromQuarter(int $year, int $quarter = 1): self
198
    {
199 18
        $month = (($quarter - 1) * 3) + 1;
200 18
        $startDate = (new DateTimeImmutable())->setDate($year, $month, 1)->setTime(0, 0);
201
202 18
        return new self($startDate, $startDate->add(new DateInterval('P3M')));
203
    }
204
205
    /**
206
     * Creates new instance for a specific year and month.
207
     */
208 72
    public static function fromMonth(int $year, int $month = 1): self
209
    {
210 72
        $startDate = (new DateTimeImmutable())->setDate($year, $month, 1)->setTime(0, 0);
211
212 72
        return new self($startDate, $startDate->add(new DateInterval('P1M')));
213
    }
214
215
    /**
216
     * Creates new instance for a specific ISO8601 week.
217
     */
218 21
    public static function fromIsoWeek(int $year, int $week = 1): self
219
    {
220 21
        $startDate = (new DateTimeImmutable())->setISODate($year, $week, 1)->setTime(0, 0);
221
222 21
        return new self($startDate, $startDate->add(new DateInterval('P7D')));
223
    }
224
225
    /**
226
     * Creates new instance for a specific year, month and day.
227
     */
228 51
    public static function fromDay(int $year, int $month = 1, int $day = 1): self
229
    {
230 51
        $startDate = (new DateTimeImmutable())->setDate($year, $month, $day)->setTime(0, 0);
231
232 51
        return new self($startDate, $startDate->add(new DateInterval('P1D')));
233
    }
234
235
    /**
236
     * Creates a new instance.
237
     *
238
     * @param mixed $startDate the starting datepoint
239
     * @param mixed $endDate   the ending datepoint
240
     *
241
     * @throws Exception If $startDate is greater than $endDate
242
     */
243 852
    public function __construct($startDate, $endDate, string $boundaryType = self::INCLUDE_START_EXCLUDE_END)
244
    {
245 852
        $startDate = self::getDatepoint($startDate);
246 852
        $endDate = self::getDatepoint($endDate);
247 840
        if ($startDate > $endDate) {
248 66
            throw new Exception('The ending datepoint must be greater or equal to the starting datepoint');
249
        }
250
251 816
        if (!isset(self::BOUNDARY_TYPE[$boundaryType])) {
252 6
            throw new Exception(sprintf(
253 6
                'The boundary type `%s` is invalid. The only valid values are %s',
254 6
                $boundaryType,
255 6
                '`'.implode('`, `', array_keys(self::BOUNDARY_TYPE)).'`'
256
            ));
257
        }
258
259 813
        $this->startDate = $startDate;
260 813
        $this->endDate = $endDate;
261 813
        $this->boundaryType = $boundaryType;
262 813
    }
263
264
    /**
265
     * Returns the starting datepoint.
266
     */
267 234
    public function getStartDate(): DateTimeImmutable
268
    {
269 234
        return $this->startDate;
270
    }
271
272
    /**
273
     * Returns the ending datepoint.
274
     */
275 210
    public function getEndDate(): DateTimeImmutable
276
    {
277 210
        return $this->endDate;
278
    }
279
280
    /**
281
     * Returns the instance boundary type.
282
     */
283 159
    public function getBoundaryType(): string
284
    {
285 159
        return $this->boundaryType;
286
    }
287
288
    /**************************************************
289
     * String representation
290
     **************************************************/
291
292
    /**
293
     * Returns the string representation as a ISO8601 interval format.
294
     *
295
     * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
296
     *
297
     * @return string
298
     */
299 6
    public function __toString()
300
    {
301 6
        $interval = $this->jsonSerialize();
302
303 6
        return $interval['startDate'].'/'.$interval['endDate'];
304
    }
305
306
    /**
307
     * Returns the JSON representation of an instance.
308
     *
309
     * Based on the JSON representation of dates as
310
     * returned by Javascript Date.toJSON() method.
311
     *
312
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toJSON
313
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
314
     *
315
     * @return array<string>
316
     */
317 15
    public function jsonSerialize()
318
    {
319 15
        $utc = new DateTimeZone('UTC');
320
321
        return [
322 15
            'startDate' => $this->startDate->setTimezone($utc)->format(self::ISO8601_FORMAT),
323 15
            'endDate' => $this->endDate->setTimezone($utc)->format(self::ISO8601_FORMAT),
324
        ];
325
    }
326
327
    /**
328
     * Returns the mathematical representation of an instance as a left close, right open interval.
329
     *
330
     * @see https://en.wikipedia.org/wiki/Interval_(mathematics)#Notations_for_intervals
331
     * @see https://php.net/manual/en/function.date.php
332
     * @see https://www.postgresql.org/docs/9.3/static/rangetypes.html
333
     *
334
     * @param string $format the format of the outputted date string
335
     */
336 18
    public function format(string $format): string
337
    {
338 18
        return $this->boundaryType[0]
339 18
            .$this->startDate->format($format)
340 18
            .', '
341 18
            .$this->endDate->format($format)
342 18
            .$this->boundaryType[1];
343
    }
344
345
    /**************************************************
346
     * Boundary related methods
347
     **************************************************/
348
349
    /**
350
     * Tells whether the start datepoint is included in the boundary.
351
     */
352 12
    public function isStartDateIncluded(): bool
353
    {
354 12
        return '[' === $this->boundaryType[0];
355
    }
356
357
    /**
358
     * Tells whether the start datepoint is excluded from the boundary.
359
     */
360 204
    public function isStartDateExcluded(): bool
361
    {
362 204
        return '(' === $this->boundaryType[0];
363
    }
364
365
    /**
366
     * Tells whether the end datepoint is included in the boundary.
367
     */
368 12
    public function isEndDateIncluded(): bool
369
    {
370 12
        return ']' === $this->boundaryType[1];
371
    }
372
373
    /**
374
     * Tells whether the end datepoint is excluded from the boundary.
375
     */
376 204
    public function isEndDateExcluded(): bool
377
    {
378 204
        return ')' === $this->boundaryType[1];
379
    }
380
381
    /**************************************************
382
     * duration comparison methods
383
     **************************************************/
384
385
    /**
386
     * Compares two instances according to their duration.
387
     *
388
     * Returns:
389
     * <ul>
390
     * <li> -1 if the current Interval is lesser than the submitted Interval object</li>
391
     * <li>  1 if the current Interval is greater than the submitted Interval object</li>
392
     * <li>  0 if both Interval objects have the same duration</li>
393
     * </ul>
394
     */
395 57
    public function durationCompare(self $interval): int
396
    {
397 57
        return $this->endDate <=> $this->startDate->add($interval->getDateInterval());
398
    }
399
400
    /**
401
     * Tells whether the current instance duration is equal to the submitted one.
402
     */
403 6
    public function durationEquals(self $interval): bool
404
    {
405 6
        return 0 === $this->durationCompare($interval);
406
    }
407
408
    /**
409
     * Tells whether the current instance duration is greater than the submitted one.
410
     */
411 18
    public function durationGreaterThan(self $interval): bool
412
    {
413 18
        return 1 === $this->durationCompare($interval);
414
    }
415
416
    /**
417
     * Tells whether the current instance duration is less than the submitted one.
418
     */
419 12
    public function durationLessThan(self $interval): bool
420
    {
421 12
        return -1 === $this->durationCompare($interval);
422
    }
423
424
    /**************************************************
425
     * relation methods
426
     **************************************************/
427
428
    /**
429
     * Tells whether two intervals share the same datepoints.
430
     *
431
     * [--------------------)
432
     * [--------------------)
433
     */
434 261
    public function equals(self $interval): bool
435
    {
436 261
        return $this->startDate == $interval->startDate
437 261
            && $this->endDate == $interval->endDate
438 261
            && $this->boundaryType === $interval->boundaryType;
439
    }
440
441
    /**
442
     * Tells whether two intervals abuts.
443
     *
444
     * [--------------------)
445
     *                      [--------------------)
446
     * or
447
     *                      [--------------------)
448
     * [--------------------)
449
     */
450 282
    public function abuts(self $interval): bool
451
    {
452 282
        return $this->meets($interval) || $this->metBy($interval);
453
    }
454
455
    /**
456
     * Tells whether the current instance end date meets the interval start date.
457
     *
458
     * [--------------------)
459
     *                      [--------------------)
460
     */
461 282
    public function meets(self $interval): bool
462
    {
463 282
        return $this->endDate == $interval->startDate
464 282
            && '][' !== $this->boundaryType[1].$interval->boundaryType[0];
465
    }
466
467
    /**
468
     * Tells whether the current instance start date meets the interval end date.
469
     *
470
     *                      [--------------------)
471
     * [--------------------)
472
     */
473 264
    public function metBy(self $interval): bool
474
    {
475 264
        return $interval->meets($this);
476
    }
477
478
    /**
479
     * Tells whether two intervals share the same start datepoint.
480
     *
481
     *    [----------)
482
     *    [--------------------)
483
     *
484
     * or
485
     *
486
     *    [--------------------)
487
     *    [---------)
488
     */
489 12
    public function starts(self $interval): bool
490
    {
491 12
        return $this->startDate == $interval->startDate
492 12
            && $this->boundaryType[0] === $interval->boundaryType[0];
493
    }
494
495
    /**
496
     * Tells whether two intervals share the same end datepoint.
497
     *
498
     *              [----------)
499
     *    [--------------------)
500
     *
501
     * or
502
     *
503
     *    [--------------------)
504
     *               [---------)
505
     */
506 12
    public function finishes(self $interval): bool
507
    {
508 12
        return $this->endDate == $interval->endDate
509 12
            && $this->boundaryType[1] === $interval->boundaryType[1];
510
    }
511
512
    /**
513
     * Tells whether two intervals overlaps.
514
     *
515
     * [--------------------)
516
     *          [--------------------)
517
     */
518 264
    public function overlaps(self $interval): bool
519
    {
520 264
        return !$this->abuts($interval)
521 264
            && $this->startDate < $interval->endDate
522 264
            && $this->endDate > $interval->startDate;
523
    }
524
525
    /**
526
     * Tells whether an instance is entirely before the specified index.
527
     *
528
     * The index can be a DateTimeInterface object or another Period object.
529
     *
530
     * [--------------------)
531
     *                          [--------------------)
532
     *
533
     * @param mixed $index a datepoint or a Period object
534
     */
535 72
    public function isBefore($index): bool
536
    {
537 72
        if ($index instanceof self) {
538 48
            return $this->endDate < $index->startDate
539 48
                || ($this->endDate == $index->startDate && $this->boundaryType[1] !== $index->boundaryType[0]);
540
        }
541
542 24
        $datepoint = self::getDatepoint($index);
543 24
        return $this->endDate < $datepoint
544 24
            || ($this->endDate == $datepoint && ')' === $this->boundaryType[1]);
545
    }
546
547
    /**
548
     * Tells whether an interval is entirely after the specified index.
549
     * The index can be a DateTimeInterface object or another Period object.
550
     *
551
     *                          [--------------------)
552
     * [--------------------)
553
     *
554
     * @param mixed $index a datepoint or a Period object
555
     */
556 42
    public function isAfter($index): bool
557
    {
558 42
        if ($index instanceof self) {
559 24
            return $index->isBefore($this);
560
        }
561
562 18
        $datepoint = self::getDatepoint($index);
563 18
        return $this->startDate > $datepoint
564 18
            || ($this->startDate == $datepoint && '(' === $this->boundaryType[0]);
565
    }
566
567
    /**
568
     * Tells whether an instance is fully contained in the specified interval.
569
     *
570
     *     [----------)
571
     * [--------------------)
572
     */
573 39
    public function isDuring(self $interval): bool
574
    {
575 39
        return $interval->containsInterval($this);
576
    }
577
578
    /**
579
     * Tells whether an instance fully contains the specified index.
580
     *
581
     * The index can be a DateTimeInterface object or another Period object.
582
     *
583
     * @param mixed $index a datepoint or a Period object
584
     */
585 117
    public function contains($index): bool
586
    {
587 117
        if ($index instanceof self) {
588 66
            return $this->containsInterval($index);
589
        }
590
591 51
        return $this->containsDatepoint(self::getDatepoint($index), $this->boundaryType);
592
    }
593
594
    /**
595
     * Tells whether an instance fully contains another instance.
596
     *
597
     * [--------------------)
598
     *     [----------)
599
     */
600 66
    private function containsInterval(self $interval): bool
601
    {
602 66
        if ($this->startDate < $interval->startDate && $this->endDate > $interval->endDate) {
603 18
            return true;
604
        }
605
606 60
        if ($this->startDate == $interval->startDate && $this->endDate == $interval->endDate) {
607 21
            return $this->boundaryType === $interval->boundaryType || '[]' === $this->boundaryType;
608
        }
609
610 39
        if ($this->startDate == $interval->startDate) {
611 12
            return ($this->boundaryType[0] === $interval->boundaryType[0] || '[' === $this->boundaryType[0])
612 12
                && $this->containsDatepoint($this->startDate->add($interval->getDateInterval()), $this->boundaryType);
613
        }
614
615 27
        if ($this->endDate == $interval->endDate) {
616 18
            return ($this->boundaryType[1] === $interval->boundaryType[1] || ']' === $this->boundaryType[1])
617 18
                && $this->containsDatepoint($this->endDate->sub($interval->getDateInterval()), $this->boundaryType);
0 ignored issues
show
Bug introduced by
It seems like $this->endDate->sub($interval->getDateInterval()) can also be of type false; however, parameter $datepoint of League\Period\Period::containsDatepoint() does only seem to accept DateTimeInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

617
                && $this->containsDatepoint(/** @scrutinizer ignore-type */ $this->endDate->sub($interval->getDateInterval()), $this->boundaryType);
Loading history...
618
        }
619
620 9
        return false;
621
    }
622
623
    /**
624
     * Tells whether an instance contains a datepoint.
625
     *
626
     * [------|------------)
627
     */
628 81
    private function containsDatepoint(DateTimeInterface $datepoint, string $boundaryType): bool
629
    {
630
        switch ($boundaryType) {
631 81
            case self::EXCLUDE_ALL:
632 6
                return $datepoint > $this->startDate && $datepoint < $this->endDate;
633 75
            case self::INCLUDE_ALL:
634 3
                return $datepoint >= $this->startDate && $datepoint <= $this->endDate;
635 72
            case self::EXCLUDE_START_INCLUDE_END:
636 6
                return $datepoint > $this->startDate && $datepoint <= $this->endDate;
637 66
            case self::INCLUDE_START_EXCLUDE_END:
638
            default:
639 66
                return $datepoint >= $this->startDate && $datepoint < $this->endDate;
640
        }
641
    }
642
643
    /**************************************************
644
     * operation on duration methods
645
     **************************************************/
646
647
    /**
648
     * Returns the instance duration as expressed in seconds.
649
     */
650 33
    public function getTimestampInterval(): float
651
    {
652 33
        return $this->endDate->getTimestamp() - $this->startDate->getTimestamp();
653
    }
654
655
    /**
656
     * Returns the instance duration as a DateInterval object.
657
     */
658 126
    public function getDateInterval(): DateInterval
659
    {
660 126
        return $this->startDate->diff($this->endDate);
661
    }
662
663
    /**
664
     * Returns the difference between two instances expressed in seconds.
665
     */
666 6
    public function timestampIntervalDiff(self $interval): float
667
    {
668 6
        return $this->getTimestampInterval() - $interval->getTimestampInterval();
669
    }
670
671
    /**
672
     * Returns the difference between two instances expressed with a DateInterval object.
673
     */
674 12
    public function dateIntervalDiff(self $interval): DateInterval
675
    {
676 12
        return $this->endDate->diff($this->startDate->add($interval->getDateInterval()));
677
    }
678
679
    /**
680
     * Allows iteration over a set of dates and times,
681
     * recurring at regular intervals, over the instance.
682
     *
683
     * @see http://php.net/manual/en/dateperiod.construct.php
684
     *
685
     * @param mixed $duration a Duration
686
     */
687 54
    public function getDatePeriod($duration, int $option = 0): DatePeriod
688
    {
689 54
        return new DatePeriod($this->startDate, self::getDuration($duration), $this->endDate, $option);
690
    }
691
692
    /**
693
     * Allows iteration over a set of dates and times,
694
     * recurring at regular intervals, over the instance backwards starting from
695
     * the instance ending datepoint.
696
     *
697
     * @param mixed $duration a Duration
698
     */
699 24
    public function getDatePeriodBackwards($duration, int $option = 0): iterable
700
    {
701 24
        $duration = self::getDuration($duration);
702 24
        $date = $this->endDate;
703 24
        if ((bool) ($option & DatePeriod::EXCLUDE_START_DATE)) {
704 12
            $date = $this->endDate->sub($duration);
705
        }
706
707 24
        while ($date > $this->startDate) {
708 24
            yield $date;
709 24
            $date = $date->sub($duration);
710
        }
711 24
    }
712
713
    /**
714
     * Allows splitting an instance in smaller Period objects according to a given interval.
715
     *
716
     * The returned iterable Interval set is ordered so that:
717
     * <ul>
718
     * <li>The first returned object MUST share the starting datepoint of the parent object.</li>
719
     * <li>The last returned object MUST share the ending datepoint of the parent object.</li>
720
     * <li>The last returned object MUST have a duration equal or lesser than the submitted interval.</li>
721
     * <li>All returned objects except for the first one MUST start immediately after the previously returned object</li>
722
     * </ul>
723
     *
724
     * @param mixed $duration a Duration
725
     *
726
     * @return iterable<Period>
727
     */
728 30
    public function split($duration): iterable
729
    {
730 30
        $duration = self::getDuration($duration);
731 30
        foreach ($this->getDatePeriod($duration) as $startDate) {
732 30
            $endDate = $startDate->add($duration);
733 30
            if ($endDate > $this->endDate) {
734 12
                $endDate = $this->endDate;
735
            }
736
737 30
            yield new self($startDate, $endDate, $this->boundaryType);
738
        }
739 30
    }
740
741
    /**
742
     * Allows splitting an instance in smaller Period objects according to a given interval.
743
     *
744
     * The returned iterable Period set is ordered so that:
745
     * <ul>
746
     * <li>The first returned object MUST share the ending datepoint of the parent object.</li>
747
     * <li>The last returned object MUST share the starting datepoint of the parent object.</li>
748
     * <li>The last returned object MUST have a duration equal or lesser than the submitted interval.</li>
749
     * <li>All returned objects except for the first one MUST end immediately before the previously returned object</li>
750
     * </ul>
751
     *
752
     * @param mixed $duration a Duration
753
     *
754
     * @return iterable<Period>
755
     */
756 18
    public function splitBackwards($duration): iterable
757
    {
758 18
        $endDate = $this->endDate;
759 18
        $duration = self::getDuration($duration);
760
        do {
761 18
            $startDate = $endDate->sub($duration);
762 18
            if ($startDate < $this->startDate) {
763 6
                $startDate = $this->startDate;
764
            }
765 18
            yield new self($startDate, $endDate, $this->boundaryType);
766
767 18
            $endDate = $startDate;
768 18
        } while ($endDate > $this->startDate);
769 18
    }
770
771
    /**************************************************
772
     * operation on relation methods
773
     **************************************************/
774
775
    /**
776
     * Returns the computed intersection between two instances as a new instance.
777
     *
778
     * [--------------------)
779
     *          ∩
780
     *                 [----------)
781
     *          =
782
     *                 [----)
783
     *
784
     * @throws Exception If both objects do not overlaps
785
     */
786 138
    public function intersect(self $interval): self
787
    {
788 138
        if (!$this->overlaps($interval)) {
789 12
            throw new Exception('Both '.self::class.' objects should overlaps');
790
        }
791
792 126
        $startDate = $this->startDate;
793 126
        $endDate = $this->endDate;
794 126
        $boundaryType = $this->isEndDateExcluded() ? '[' : '(';
795 126
        $boundaryType .= $interval->isStartDateExcluded() ? ']' : ')';
796 126
        if ($interval->startDate > $this->startDate) {
797 120
            $startDate = $interval->startDate;
798
        }
799
800 126
        if ($interval->endDate < $this->endDate) {
801 24
            $endDate = $interval->endDate;
802
        }
803
804 126
        $intersect = new self($startDate, $endDate, $boundaryType);
805 126
        if ($intersect->equals($this)) {
806 12
            return $this;
807
        }
808
809 126
        return $intersect;
810
    }
811
812
    /**
813
     * Returns the computed difference between two overlapping instances as
814
     * an array containing Period objects or the null value.
815
     *
816
     * The array will always contains 2 elements:
817
     *
818
     * <ul>
819
     * <li>an NULL filled array if both objects have the same datepoints</li>
820
     * <li>one Period object and NULL if both objects share one datepoint</li>
821
     * <li>two Period objects if both objects share no datepoint</li>
822
     * </ul>
823
     *
824
     * [--------------------)
825
     *          \
826
     *                [-----------)
827
     *          =
828
     * [--------------)  +  [-----)
829
     *
830
     * @return array<null|Period>
831
     */
832 78
    public function diff(self $interval): array
833
    {
834 78
        if ($interval->equals($this)) {
835 6
            return [null, null];
836
        }
837
838 72
        $intersect = $this->intersect($interval);
839 66
        $merge = $this->merge($interval);
840 66
        if ($merge->startDate == $intersect->startDate) {
841 6
            $lastBoundary = $merge->boundaryType[0].$intersect->boundaryType[1];
842
843 6
            return [$merge->startingOn($intersect->endDate)->withBoundaryType($lastBoundary), null];
844
        }
845
846 60
        if ($merge->endDate == $intersect->endDate) {
847 6
            $firstBoundary = $intersect->boundaryType[0].$merge->boundaryType[1];
848
849 6
            return [$merge->endingOn($intersect->startDate)->withBoundaryType($firstBoundary), null];
850
        }
851
852 54
        $firstBoundary = $intersect->boundaryType[0].$merge->boundaryType[1];
853 54
        $lastBoundary = $merge->boundaryType[0].$intersect->boundaryType[1];
854
855
        return [
856 54
            $merge->endingOn($intersect->startDate)->withBoundaryType($firstBoundary),
857 54
            $merge->startingOn($intersect->endDate)->withBoundaryType($lastBoundary),
858
        ];
859
    }
860
861
    /**
862
     * Returns the computed gap between two instances as a new instance.
863
     *
864
     * [--------------------)
865
     *          +
866
     *                          [----------)
867
     *          =
868
     *                      [---)
869
     *
870
     * @throws Exception If both instance overlaps
871
     */
872 84
    public function gap(self $interval): self
873
    {
874 84
        if ($this->overlaps($interval)) {
875 18
            throw new Exception('Both '.self::class.' objects must not overlaps');
876
        }
877
878 66
        $boundaryType = $this->isEndDateExcluded() ? '[' : '(';
879 66
        $boundaryType .= $interval->isStartDateExcluded() ? ']' : ')';
880 66
        if ($interval->startDate > $this->startDate) {
881 66
            return new self($this->endDate, $interval->startDate, $boundaryType);
882
        }
883
884 6
        return new self($interval->endDate, $this->startDate, $this->boundaryType);
885
    }
886
887
    /**
888
     * Merges one or more instances to return a new instance.
889
     * The resulting instance represents the largest duration possible.
890
     *
891
     * This method MUST retain the state of the current instance, and return
892
     * an instance that contains the specified new datepoints.
893
     *
894
     * [--------------------)
895
     *          +
896
     *                 [----------)
897
     *          =
898
     * [--------------------------)
899
     *
900
     *
901
     * @param Period ...$intervals
902
     */
903 81
    public function merge(self $interval, self ...$intervals): self
904
    {
905 81
        $intervals[] = $interval;
906 81
        $carry = $this;
907 81
        foreach ($intervals as $interval) {
908 81
            if ($carry->startDate > $interval->startDate) {
909 21
                $carry = new self(
910 21
                    $interval->startDate,
911 21
                    $carry->endDate,
912 21
                    $interval->boundaryType[0].$carry->boundaryType[1]
913
                );
914
            }
915
916 81
            if ($carry->endDate < $interval->endDate) {
917 69
                $carry = new self(
918 69
                    $carry->startDate,
919 69
                    $interval->endDate,
920 81
                    $carry->boundaryType[0].$interval->boundaryType[1]
921
                );
922
            }
923
        }
924
925 81
        return $carry;
926
    }
927
928
929
    /**************************************************
930
     * mutation methods
931
     **************************************************/
932
933
    /**
934
     * Returns an instance with the specified starting datepoint.
935
     *
936
     * This method MUST retain the state of the current instance, and return
937
     * an instance that contains the specified starting datepoint.
938
     *
939
     * @param mixed $startDate the new starting datepoint
940
     */
941 105
    public function startingOn($startDate): self
942
    {
943 105
        $startDate = self::getDatepoint($startDate);
944 105
        if ($startDate == $this->startDate) {
945 6
            return $this;
946
        }
947
948 105
        return new self($startDate, $this->endDate, $this->boundaryType);
949
    }
950
951
    /**
952
     * Returns an instance with the specified ending datepoint.
953
     *
954
     * This method MUST retain the state of the current instance, and return
955
     * an instance that contains the specified ending datepoint.
956
     *
957
     * @param mixed $endDate the new ending datepoint
958
     */
959 105
    public function endingOn($endDate): self
960
    {
961 105
        $endDate = self::getDatepoint($endDate);
962 105
        if ($endDate == $this->endDate) {
963 6
            return $this;
964
        }
965
966 105
        return new self($this->startDate, $endDate, $this->boundaryType);
967
    }
968
969
    /**
970
     * Returns an instance with the specified boundary type.
971
     *
972
     * This method MUST retain the state of the current instance, and return
973
     * an instance with the specified range type.
974
     */
975 72
    public function withBoundaryType(string $boundaryType): self
976
    {
977 72
        if ($boundaryType === $this->boundaryType) {
978 57
            return $this;
979
        }
980
981 42
        return new self($this->startDate, $this->endDate, $boundaryType);
982
    }
983
984
    /**
985
     * Returns a new instance with a new ending datepoint.
986
     *
987
     * This method MUST retain the state of the current instance, and return
988
     * an instance that contains the specified ending datepoint.
989
     *
990
     * @param mixed $duration a Duration
991
     */
992 12
    public function withDurationAfterStart($duration): self
993
    {
994 12
        return $this->endingOn($this->startDate->add(self::getDuration($duration)));
995
    }
996
997
    /**
998
     * Returns a new instance with a new starting datepoint.
999
     *
1000
     * This method MUST retain the state of the current instance, and return
1001
     * an instance that contains the specified starting datepoint.
1002
     *
1003
     * @param mixed $duration a Duration
1004
     */
1005 12
    public function withDurationBeforeEnd($duration): self
1006
    {
1007 12
        return $this->startingOn($this->endDate->sub(self::getDuration($duration)));
1008
    }
1009
1010
    /**
1011
     * Returns a new instance with a new starting datepoint
1012
     * moved forward or backward by the given interval.
1013
     *
1014
     * This method MUST retain the state of the current instance, and return
1015
     * an instance that contains the specified starting datepoint.
1016
     *
1017
     * @param mixed $duration a Duration
1018
     */
1019 18
    public function moveStartDate($duration): self
1020
    {
1021 18
        return $this->startingOn($this->startDate->add(self::getDuration($duration)));
1022
    }
1023
1024
    /**
1025
     * Returns a new instance with a new ending datepoint
1026
     * moved forward or backward by the given interval.
1027
     *
1028
     * This method MUST retain the state of the current instance, and return
1029
     * an instance that contains the specified ending datepoint.
1030
     *
1031
     * @param mixed $duration a Duration
1032
     */
1033 15
    public function moveEndDate($duration): self
1034
    {
1035 15
        return $this->endingOn($this->endDate->add(self::getDuration($duration)));
1036
    }
1037
1038
    /**
1039
     * Returns a new instance where the datepoints
1040
     * are moved forwards or backward simultaneously by the given DateInterval.
1041
     *
1042
     * This method MUST retain the state of the current instance, and return
1043
     * an instance that contains the specified new datepoints.
1044
     *
1045
     * @param mixed $duration a Duration
1046
     */
1047 24
    public function move($duration): self
1048
    {
1049 24
        $duration = self::getDuration($duration);
1050 24
        $interval = new self($this->startDate->add($duration), $this->endDate->add($duration), $this->boundaryType);
1051 24
        if ($this->equals($interval)) {
1052 6
            return $this;
1053
        }
1054
1055 24
        return $interval;
1056
    }
1057
1058
    /**
1059
     * Returns an instance where the given DateInterval is simultaneously
1060
     * substracted from the starting datepoint and added to the ending datepoint.
1061
     *
1062
     * Depending on the duration value, the resulting instance duration will be expanded or shrinked.
1063
     *
1064
     * This method MUST retain the state of the current instance, and return
1065
     * an instance that contains the specified new datepoints.
1066
     *
1067
     * @param mixed $duration a Duration
1068
     */
1069 24
    public function expand($duration): self
1070
    {
1071 24
        $duration = self::getDuration($duration);
1072 24
        $interval = new self($this->startDate->sub($duration), $this->endDate->add($duration), $this->boundaryType);
1073 18
        if ($this->equals($interval)) {
1074 6
            return $this;
1075
        }
1076
1077 12
        return $interval;
1078
    }
1079
}
1080