Completed
Push — master ( a2f7a7...4d2430 )
by ignace nyamagana
22s queued 10s
created

Period::isStartExcluded()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
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 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 939
    private static function getDatepoint($datepoint): DateTimeImmutable
79
    {
80 939
        if ($datepoint instanceof DateTimeImmutable) {
81 732
            return $datepoint;
82
        }
83
84 681
        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 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 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 855
    public function __construct($startDate, $endDate, string $boundaryType = self::INCLUDE_START_EXCLUDE_END)
244
    {
245 855
        $startDate = self::getDatepoint($startDate);
246 855
        $endDate = self::getDatepoint($endDate);
247 843
        if ($startDate > $endDate) {
248 66
            throw new Exception('The ending datepoint must be greater or equal to the starting datepoint');
249
        }
250
251 819
        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 816
        $this->startDate = $startDate;
260 816
        $this->endDate = $endDate;
261 816
        $this->boundaryType = $boundaryType;
262 816
    }
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 isStartIncluded(): bool
353
    {
354 12
        return '[' === $this->boundaryType[0];
355
    }
356
357
    /**
358
     * Tells whether the start datepoint is excluded from the boundary.
359
     */
360 78
    public function isStartExcluded(): bool
361
    {
362 78
        return '(' === $this->boundaryType[0];
363
    }
364
365
    /**
366
     * Tells whether the end datepoint is included in the boundary.
367
     */
368 12
    public function isEndIncluded(): bool
369
    {
370 12
        return ']' === $this->boundaryType[1];
371
    }
372
373
    /**
374
     * Tells whether the end datepoint is excluded from the boundary.
375
     */
376 78
    public function isEndExcluded(): bool
377
    {
378 78
        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 264
    public function equals(self $interval): bool
435
    {
436 264
        return $this->startDate == $interval->startDate
437 264
            && $this->endDate == $interval->endDate
438 264
            && $this->boundaryType === $interval->boundaryType;
439
    }
440
441
    /**
442
     * Tells whether two intervals abuts.
443
     *
444
     * [--------------------)
445
     *                      [--------------------)
446
     * or
447
     *                      [--------------------)
448
     * [--------------------)
449
     */
450 285
    public function abuts(self $interval): bool
451
    {
452 285
        return $this->bordersOnStart($interval) || $this->bordersOnEnd($interval);
453
    }
454
455
    /**
456
     * Tells whether the current instance end date meets the interval start date.
457
     *
458
     * [--------------------)
459
     *                      [--------------------)
460
     */
461 285
    public function bordersOnStart(self $interval): bool
462
    {
463 285
        return $this->endDate == $interval->startDate
464 285
            && '][' !== $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 267
    public function bordersOnEnd(self $interval): bool
474
    {
475 267
        return $interval->bordersOnStart($this);
476
    }
477
478
    /**
479
     * Tells whether two intervals share the same start datepoint
480
     * and the same starting boundary type.
481
     *
482
     *    [----------)
483
     *    [--------------------)
484
     *
485
     * or
486
     *
487
     *    [--------------------)
488
     *    [---------)
489
     */
490 12
    public function startsBy(self $interval): bool
491
    {
492 12
        return $this->startDate == $interval->startDate
493 12
            && $this->boundaryType[0] === $interval->boundaryType[0];
494
    }
495
496
    /**
497
     * Tells whether two intervals share the same end datepoint
498
     * and the same ending boundary type.
499
     *
500
     *              [----------)
501
     *    [--------------------)
502
     *
503
     * or
504
     *
505
     *    [--------------------)
506
     *               [---------)
507
     */
508 12
    public function endsBy(self $interval): bool
509
    {
510 12
        return $this->endDate == $interval->endDate
511 12
            && $this->boundaryType[1] === $interval->boundaryType[1];
512
    }
513
514
    /**
515
     * Tells whether two intervals overlaps.
516
     *
517
     * [--------------------)
518
     *          [--------------------)
519
     */
520 267
    public function overlaps(self $interval): bool
521
    {
522 267
        return !$this->abuts($interval)
523 267
            && $this->startDate < $interval->endDate
524 267
            && $this->endDate > $interval->startDate;
525
    }
526
527
    /**
528
     * Tells whether an instance is entirely before the specified index.
529
     *
530
     * The index can be a DateTimeInterface object or another Period object.
531
     *
532
     * [--------------------)
533
     *                          [--------------------)
534
     *
535
     * @param mixed $index a datepoint or a Period object
536
     */
537 72
    public function isBefore($index): bool
538
    {
539 72
        if ($index instanceof self) {
540 48
            return $this->endDate < $index->startDate
541 48
                || ($this->endDate == $index->startDate && $this->boundaryType[1] !== $index->boundaryType[0]);
542
        }
543
544 24
        $datepoint = self::getDatepoint($index);
545 24
        return $this->endDate < $datepoint
546 24
            || ($this->endDate == $datepoint && ')' === $this->boundaryType[1]);
547
    }
548
549
    /**
550
     * Tells whether an interval is entirely after the specified index.
551
     * The index can be a DateTimeInterface object or another Period object.
552
     *
553
     *                          [--------------------)
554
     * [--------------------)
555
     *
556
     * @param mixed $index a datepoint or a Period object
557
     */
558 42
    public function isAfter($index): bool
559
    {
560 42
        if ($index instanceof self) {
561 24
            return $index->isBefore($this);
562
        }
563
564 18
        $datepoint = self::getDatepoint($index);
565 18
        return $this->startDate > $datepoint
566 18
            || ($this->startDate == $datepoint && '(' === $this->boundaryType[0]);
567
    }
568
569
    /**
570
     * Tells whether an instance is fully contained in the specified interval.
571
     *
572
     *     [----------)
573
     * [--------------------)
574
     */
575 39
    public function isDuring(self $interval): bool
576
    {
577 39
        return $interval->containsInterval($this);
578
    }
579
580
    /**
581
     * Tells whether an instance fully contains the specified index.
582
     *
583
     * The index can be a DateTimeInterface object or another Period object.
584
     *
585
     * @param mixed $index a datepoint or a Period object
586
     */
587 117
    public function contains($index): bool
588
    {
589 117
        if ($index instanceof self) {
590 66
            return $this->containsInterval($index);
591
        }
592
593 51
        return $this->containsDatepoint(self::getDatepoint($index), $this->boundaryType);
594
    }
595
596
    /**
597
     * Tells whether an instance fully contains another instance.
598
     *
599
     * [--------------------)
600
     *     [----------)
601
     */
602 66
    private function containsInterval(self $interval): bool
603
    {
604 66
        if ($this->startDate < $interval->startDate && $this->endDate > $interval->endDate) {
605 18
            return true;
606
        }
607
608 60
        if ($this->startDate == $interval->startDate && $this->endDate == $interval->endDate) {
609 21
            return $this->boundaryType === $interval->boundaryType || '[]' === $this->boundaryType;
610
        }
611
612 39
        if ($this->startDate == $interval->startDate) {
613 12
            return ($this->boundaryType[0] === $interval->boundaryType[0] || '[' === $this->boundaryType[0])
614 12
                && $this->containsDatepoint($this->startDate->add($interval->getDateInterval()), $this->boundaryType);
615
        }
616
617 27
        if ($this->endDate == $interval->endDate) {
618 18
            return ($this->boundaryType[1] === $interval->boundaryType[1] || ']' === $this->boundaryType[1])
619 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

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