Passed
Push — master ( fdcab4...a2f7a7 )
by ignace nyamagana
09:31 queued 03:59
created

Period::fromMonth()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 5
ccs 3
cts 3
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
23
/**
24
 * A immutable value object class to manipulate Time interval.
25
 *
26
 * @package League.period
27
 * @author  Ignace Nyamagana Butera <[email protected]>
28
 * @since   1.0.0
29
 */
30
final class Period implements JsonSerializable
31
{
32
    private const ISO8601_FORMAT = 'Y-m-d\TH:i:s.u\Z';
33
34
    /**
35
     * The starting included datepoint.
36
     *
37
     * @var DateTimeImmutable
38
     */
39
    private $startDate;
40
41
    /**
42
     * The ending excluded datepoint.
43
     *
44
     * @var DateTimeImmutable
45
     */
46
    private $endDate;
47
48
    /**
49
     * @inheritdoc
50
     */
51 3
    public static function __set_state(array $interval)
52
    {
53 3
        return new self($interval['startDate'], $interval['endDate']);
54
    }
55
56
    /**
57
     * Creates new instance from a starting datepoint and a duration.
58
     *
59
     * @param mixed $startDate the starting included datepoint
60
     * @param mixed $duration  a Duration
61
     */
62 72
    public static function after($startDate, $duration): self
63
    {
64 72
        $startDate = self::getDatepoint($startDate);
65
66 72
        return new self($startDate, $startDate->add(self::getDuration($duration)));
67
    }
68
69
    /**
70
     * Returns a DateTimeImmutable instance.
71
     *
72
     * @param mixed $datepoint a Datepoint
73
     */
74 456
    private static function getDatepoint($datepoint): DateTimeImmutable
75
    {
76 456
        if ($datepoint instanceof DateTimeImmutable) {
77 351
            return $datepoint;
78
        }
79
80 279
        return Datepoint::create($datepoint);
81
    }
82
83
    /**
84
     * Returns a DateInterval instance.
85
     *
86
     * @param mixed $duration a Duration
87
     */
88 210
    private static function getDuration($duration): DateInterval
89
    {
90 210
        if ($duration instanceof DateInterval) {
91 90
            return $duration;
92
        }
93
94 120
        return Duration::create($duration);
95
    }
96
97
    /**
98
     * Creates new instance from a ending datepoint and a duration.
99
     *
100
     * @param mixed $endDate  the ending excluded datepoint
101
     * @param mixed $duration a Duration
102
     */
103 12
    public static function before($endDate, $duration): self
104
    {
105 12
        $endDate = self::getDatepoint($endDate);
106 12
        $duration = self::getDuration($duration);
107
108 12
        return new self($endDate->sub($duration), $endDate);
109
    }
110
111
    /**
112
     * Creates new instance where the given duration is simultaneously
113
     * substracted from and added to the datepoint.
114
     *
115
     * @param mixed $datepoint a Datepoint
116
     * @param mixed $duration  a Duration
117
     */
118 12
    public static function around($datepoint, $duration): self
119
    {
120 12
        $datepoint = self::getDatepoint($datepoint);
121 12
        $duration = self::getDuration($duration);
122
123 12
        return new self($datepoint->sub($duration), $datepoint->add($duration));
124
    }
125
126
    /**
127
     * Creates new instance for a specific year.
128
     */
129 6
    public static function fromYear(int $year): self
130
    {
131 6
        $startDate = (new DateTimeImmutable())->setDate($year, 1, 1)->setTime(0, 0);
132
133 6
        return new self($startDate, $startDate->add(new DateInterval('P1Y')));
134
    }
135
136
    /**
137
     * Creates new instance for a specific ISO year.
138
     */
139 3
    public static function fromIsoYear(int $year): self
140
    {
141 3
        return new self(
142 3
            (new DateTimeImmutable())->setISODate($year, 1)->setTime(0, 0),
143 3
            (new DateTimeImmutable())->setISODate(++$year, 1)->setTime(0, 0)
144
        );
145
    }
146
147
    /**
148
     * Creates new instance for a specific year and semester.
149
     */
150 9
    public static function fromSemester(int $year, int $semester = 1): self
151
    {
152 9
        $month = (($semester - 1) * 6) + 1;
153 9
        $startDate = (new DateTimeImmutable())->setDate($year, $month, 1)->setTime(0, 0);
154
155 9
        return new self($startDate, $startDate->add(new DateInterval('P6M')));
156
    }
157
158
    /**
159
     * Creates new instance for a specific year and quarter.
160
     */
161 9
    public static function fromQuarter(int $year, int $quarter = 1): self
162
    {
163 9
        $month = (($quarter - 1) * 3) + 1;
164 9
        $startDate = (new DateTimeImmutable())->setDate($year, $month, 1)->setTime(0, 0);
165
166 9
        return new self($startDate, $startDate->add(new DateInterval('P3M')));
167
    }
168
169
    /**
170
     * Creates new instance for a specific year and month.
171
     */
172 45
    public static function fromMonth(int $year, int $month = 1): self
173
    {
174 45
        $startDate = (new DateTimeImmutable())->setDate($year, $month, 1)->setTime(0, 0);
175
176 45
        return new self($startDate, $startDate->add(new DateInterval('P1M')));
177
    }
178
179
    /**
180
     * Creates new instance for a specific ISO8601 week.
181
     */
182 12
    public static function fromIsoWeek(int $year, int $week = 1): self
183
    {
184 12
        $startDate = (new DateTimeImmutable())->setISODate($year, $week, 1)->setTime(0, 0);
185
186 12
        return new self($startDate, $startDate->add(new DateInterval('P7D')));
187
    }
188
189
    /**
190
     * Creates new instance for a specific year, month and day.
191
     */
192 15
    public static function fromDay(int $year, int $month = 1, int $day = 1): self
193
    {
194 15
        $startDate = (new DateTimeImmutable())->setDate($year, $month, $day)->setTime(0, 0);
195
196 15
        return new self($startDate, $startDate->add(new DateInterval('P1D')));
197
    }
198
199
    /**
200
     * Creates new instance from a DatePeriod.
201
     */
202 6
    public static function fromDatePeriod(DatePeriod $datePeriod): self
203
    {
204 6
        return new self($datePeriod->getStartDate(), $datePeriod->getEndDate());
205
    }
206
207
    /**
208
     * Creates a new instance.
209
     *
210
     * @param mixed $startDate the starting included datepoint
211
     * @param mixed $endDate   the ending excluded datepoint
212
     *
213
     * @throws Exception If $startDate is greater than $endDate
214
     */
215 435
    public function __construct($startDate, $endDate)
216
    {
217 435
        $startDate = self::getDatepoint($startDate);
218 435
        $endDate = self::getDatepoint($endDate);
219 429
        if ($startDate > $endDate) {
220 33
            throw new Exception('The ending datepoint must be greater or equal to the starting datepoint');
221
        }
222 417
        $this->startDate = $startDate;
223 417
        $this->endDate = $endDate;
224 417
    }
225
226
    /**
227
     * Returns the starting included datepoint.
228
     */
229 132
    public function getStartDate(): DateTimeImmutable
230
    {
231 132
        return $this->startDate;
232
    }
233
234
    /**
235
     * Returns the ending excluded datepoint.
236
     */
237 111
    public function getEndDate(): DateTimeImmutable
238
    {
239 111
        return $this->endDate;
240
    }
241
242
    /**
243
     * Returns the instance duration as expressed in seconds.
244
     */
245 18
    public function getTimestampInterval(): float
246
    {
247 18
        return $this->endDate->getTimestamp() - $this->startDate->getTimestamp();
248
    }
249
250
    /**
251
     * Returns the instance duration as a DateInterval object.
252
     */
253 54
    public function getDateInterval(): DateInterval
254
    {
255 54
        return $this->startDate->diff($this->endDate);
256
    }
257
258
    /**
259
     * Allows iteration over a set of dates and times,
260
     * recurring at regular intervals, over the instance.
261
     *
262
     * @see http://php.net/manual/en/dateperiod.construct.php
263
     *
264
     * @param mixed $duration a Duration
265
     */
266 39
    public function getDatePeriod($duration, int $option = 0): DatePeriod
267
    {
268 39
        return new DatePeriod($this->startDate, self::getDuration($duration), $this->endDate, $option);
269
    }
270
271
    /**
272
     * Allows iteration over a set of dates and times,
273
     * recurring at regular intervals, over the instance backwards starting from
274
     * the instance ending datepoint.
275
     *
276
     * @param mixed $duration a Duration
277
     */
278 24
    public function getDatePeriodBackwards($duration, int $option = 0): iterable
279
    {
280 24
        $duration = self::getDuration($duration);
281 24
        $date = $this->endDate;
282 24
        if ((bool) ($option & DatePeriod::EXCLUDE_START_DATE)) {
283 12
            $date = $this->endDate->sub($duration);
284
        }
285
286 24
        while ($date > $this->startDate) {
287 24
            yield $date;
288 24
            $date = $date->sub($duration);
289
        }
290 24
    }
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 3
    public function __toString()
300
    {
301 3
        $interval = $this->jsonSerialize();
302
303 3
        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 9
    public function jsonSerialize()
318
    {
319 9
        $utc = new DateTimeZone('UTC');
320
321
        return [
322 9
            'startDate' => $this->startDate->setTimezone($utc)->format(self::ISO8601_FORMAT),
323 9
            '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 15
    public function format(string $format): string
337
    {
338 15
        return '['.$this->startDate->format($format).', '.$this->endDate->format($format).')';
339
    }
340
341
    /**
342
     * Compares two instances according to their duration.
343
     *
344
     * Returns:
345
     * <ul>
346
     * <li> -1 if the current Interval is lesser than the submitted Interval object</li>
347
     * <li>  1 if the current Interval is greater than the submitted Interval object</li>
348
     * <li>  0 if both Interval objects have the same duration</li>
349
     * </ul>
350
     */
351 30
    public function durationCompare(self $interval): int
352
    {
353 30
        return $this->endDate <=> $this->startDate->add($interval->getDateInterval());
354
    }
355
356
    /**
357
     * Tells whether the current instance duration is equal to the submitted one.
358
     */
359 3
    public function durationEquals(self $interval): bool
360
    {
361 3
        return 0 === $this->durationCompare($interval);
362
    }
363
364
    /**
365
     * Tells whether the current instance duration is greater than the submitted one.
366
     */
367 9
    public function durationGreaterThan(self $interval): bool
368
    {
369 9
        return 1 === $this->durationCompare($interval);
370
    }
371
372
    /**
373
     * Tells whether the current instance duration is less than the submitted one.
374
     */
375 6
    public function durationLessThan(self $interval): bool
376
    {
377 6
        return -1 === $this->durationCompare($interval);
378
    }
379
380
    /**
381
     * Tells whether two intervals share the same datepoints.
382
     *
383
     * [--------------------)
384
     * [--------------------)
385
     */
386 81
    public function equals(self $interval): bool
387
    {
388 81
        return $this->startDate == $interval->startDate
389 81
            && $this->endDate == $interval->endDate;
390
    }
391
392
    /**
393
     * Tells whether two intervals abuts.
394
     *
395
     * [--------------------)
396
     *                      [--------------------)
397
     * or
398
     *                      [--------------------)
399
     * [--------------------)
400
     */
401 9
    public function abuts(self $interval): bool
402
    {
403 9
        return $this->startDate == $interval->endDate
404 9
            || $this->endDate == $interval->startDate;
405
    }
406
407
    /**
408
     * Tells whether two intervals overlaps.
409
     *
410
     * [--------------------)
411
     *          [--------------------)
412
     */
413 66
    public function overlaps(self $interval): bool
414
    {
415 66
        return $this->startDate < $interval->endDate
416 66
            && $this->endDate > $interval->startDate;
417
    }
418
419
    /**
420
     * Tells whether an interval is entirely after the specified index.
421
     * The index can be a DateTimeInterface object or another Period object.
422
     *
423
     *                          [--------------------)
424
     * [--------------------)
425
     *
426
     * @param mixed $index a datepoint or a Period object
427
     */
428 9
    public function isAfter($index): bool
429
    {
430 9
        if ($index instanceof self) {
431 3
            return $this->startDate >= $index->endDate;
432
        }
433
434 6
        return $this->startDate > self::getDatepoint($index);
435
    }
436
437
    /**
438
     * Tells whether an instance is entirely before the specified index.
439
     *
440
     * The index can be a DateTimeInterface object or another Period object.
441
     *
442
     * [--------------------)
443
     *                          [--------------------)
444
     *
445
     * @param mixed $index a datepoint or a Period object
446
     */
447 12
    public function isBefore($index): bool
448
    {
449 12
        if ($index instanceof self) {
450 6
            return $this->endDate <= $index->startDate;
451
        }
452
453 6
        return $this->endDate <= self::getDatepoint($index);
454
    }
455
456
    /**
457
     * Tells whether an instance fully contains the specified index.
458
     *
459
     * The index can be a DateTimeInterface object or another Period object.
460
     *
461
     * @param mixed $index a datepoint or a Period object
462
     */
463 45
    public function contains($index): bool
464
    {
465 45
        if ($index instanceof self) {
466 24
            return $this->containsInterval($index);
467
        }
468
469 21
        return $this->containsDatepoint(self::getDatepoint($index));
470
    }
471
472
    /**
473
     * Tells whether an instance fully contains another instance.
474
     *
475
     * [--------------------)
476
     *     [----------)
477
     */
478 24
    private function containsInterval(self $interval): bool
479
    {
480 24
        return $this->containsDatepoint($interval->startDate)
481 24
            && ($interval->endDate >= $this->startDate && $interval->endDate <= $this->endDate);
482
    }
483
484
    /**
485
     * Tells whether an instance contains a datepoint.
486
     *
487
     * [------|------------)
488
     */
489 45
    private function containsDatepoint(DateTimeInterface $datepoint): bool
490
    {
491 45
        return $datepoint >= $this->startDate && $datepoint < $this->endDate;
492
    }
493
494
    /**
495
     * Allows splitting an instance in smaller Period objects according to a given interval.
496
     *
497
     * The returned iterable Interval set is ordered so that:
498
     * <ul>
499
     * <li>The first returned object MUST share the starting datepoint of the parent object.</li>
500
     * <li>The last returned object MUST share the ending datepoint of the parent object.</li>
501
     * <li>The last returned object MUST have a duration equal or lesser than the submitted interval.</li>
502
     * <li>All returned objects except for the first one MUST start immediately after the previously returned object</li>
503
     * </ul>
504
     *
505
     * @param mixed $duration a Duration
506
     *
507
     * @return iterable<Period>
508
     */
509 15
    public function split($duration): iterable
510
    {
511 15
        $duration = self::getDuration($duration);
512 15
        foreach ($this->getDatePeriod($duration) as $startDate) {
513 15
            $endDate = $startDate->add($duration);
514 15
            if ($endDate > $this->endDate) {
515 6
                $endDate = $this->endDate;
516
            }
517
518 15
            yield new self($startDate, $endDate);
519
        }
520 15
    }
521
522
    /**
523
     * Allows splitting an instance in smaller Period objects according to a given interval.
524
     *
525
     * The returned iterable Period set is ordered so that:
526
     * <ul>
527
     * <li>The first returned object MUST share the ending datepoint of the parent object.</li>
528
     * <li>The last returned object MUST share the starting datepoint of the parent object.</li>
529
     * <li>The last returned object MUST have a duration equal or lesser than the submitted interval.</li>
530
     * <li>All returned objects except for the first one MUST end immediately before the previously returned object</li>
531
     * </ul>
532
     *
533
     * @param mixed $duration a Duration
534
     *
535
     * @return iterable<Period>
536
     */
537 9
    public function splitBackwards($duration): iterable
538
    {
539 9
        $endDate = $this->endDate;
540 9
        $duration = self::getDuration($duration);
541
        do {
542 9
            $startDate = $endDate->sub($duration);
543 9
            if ($startDate < $this->startDate) {
544 3
                $startDate = $this->startDate;
545
            }
546 9
            yield new self($startDate, $endDate);
547
548 9
            $endDate = $startDate;
549 9
        } while ($endDate > $this->startDate);
550 9
    }
551
552
    /**
553
     * Returns the computed intersection between two instances as a new instance.
554
     *
555
     * [--------------------)
556
     *          ∩
557
     *                 [----------)
558
     *          =
559
     *                 [----)
560
     *
561
     * @throws Exception If both objects do not overlaps
562
     */
563 24
    public function intersect(self $interval): self
564
    {
565 24
        if (!$this->overlaps($interval)) {
566 6
            throw new Exception('Both '.self::class.' objects should overlaps');
567
        }
568
569 18
        return new self(
570 18
            ($interval->startDate > $this->startDate) ? $interval->startDate : $this->startDate,
571 18
            ($interval->endDate < $this->endDate) ? $interval->endDate : $this->endDate
572
        );
573
    }
574
575
    /**
576
     * Returns the computed difference between two overlapping instances as
577
     * an array containing Period objects or the null value.
578
     *
579
     * The array will always contains 2 elements:
580
     *
581
     * <ul>
582
     * <li>an NULL filled array if both objects have the same datepoints</li>
583
     * <li>one Period object and NULL if both objects share one datepoint</li>
584
     * <li>two Period objects if both objects share no datepoint</li>
585
     * </ul>
586
     *
587
     * [--------------------)
588
     *          \
589
     *                [-----------)
590
     *          =
591
     * [--------------)  +  [-----)
592
     *
593
     * @return array<null|Period>
594
     */
595 15
    public function diff(self $interval): array
596
    {
597 15
        if ($interval->equals($this)) {
598 3
            return [null, null];
599
        }
600
601 12
        $intersect = $this->intersect($interval);
602 9
        $merge = $this->merge($interval);
603 9
        if ($merge->startDate == $intersect->startDate) {
604 3
            return [$merge->startingOn($intersect->endDate), null];
605
        }
606
607 6
        if ($merge->endDate == $intersect->endDate) {
608 3
            return [$merge->endingOn($intersect->startDate), null];
609
        }
610
611
        return [
612 3
            $merge->endingOn($intersect->startDate),
613 3
            $merge->startingOn($intersect->endDate),
614
        ];
615
    }
616
617
    /**
618
     * Returns the computed gap between two instances as a new instance.
619
     *
620
     * [--------------------)
621
     *          +
622
     *                          [----------)
623
     *          =
624
     *                      [---)
625
     *
626
     * @throws Exception If both instance overlaps
627
     */
628 18
    public function gap(self $interval): self
629
    {
630 18
        if ($this->overlaps($interval)) {
631 9
            throw new Exception('Both '.self::class.' objects must not overlaps');
632
        }
633
634 9
        if ($interval->startDate > $this->startDate) {
635 9
            return new self($this->endDate, $interval->startDate);
636
        }
637
638 3
        return new self($interval->endDate, $this->startDate);
639
    }
640
641
    /**
642
     * Returns the difference between two instances expressed in seconds.
643
     */
644 3
    public function timestampIntervalDiff(self $interval): float
645
    {
646 3
        return $this->getTimestampInterval() - $interval->getTimestampInterval();
647
    }
648
649
    /**
650
     * Returns the difference between two instances expressed with a DateInterval object.
651
     */
652 6
    public function dateIntervalDiff(self $interval): DateInterval
653
    {
654 6
        return $this->endDate->diff($this->startDate->add($interval->getDateInterval()));
655
    }
656
657
    /**
658
     * Returns an instance with the specified starting datepoint.
659
     *
660
     * This method MUST retain the state of the current instance, and return
661
     * an instance that contains the specified starting datepoint.
662
     *
663
     * @param mixed $startDate the new starting datepoint
664
     */
665 36
    public function startingOn($startDate): self
666
    {
667 36
        $startDate = self::getDatepoint($startDate);
668 36
        if ($startDate == $this->startDate) {
669 3
            return $this;
670
        }
671
672 36
        return new self($startDate, $this->endDate);
673
    }
674
675
    /**
676
     * Returns an instance with the specified ending datepoint.
677
     *
678
     * This method MUST retain the state of the current instance, and return
679
     * an instance that contains the specified ending datepoint.
680
     *
681
     * @param mixed $endDate the new ending datepoint
682
     */
683 36
    public function endingOn($endDate): self
684
    {
685 36
        $endDate = self::getDatepoint($endDate);
686 36
        if ($endDate == $this->endDate) {
687 3
            return $this;
688
        }
689
690 36
        return new self($this->startDate, $endDate);
691
    }
692
693
    /**
694
     * Returns a new instance with a new ending datepoint.
695
     *
696
     * This method MUST retain the state of the current instance, and return
697
     * an instance that contains the specified ending datepoint.
698
     *
699
     * @param mixed $duration a Duration
700
     */
701 6
    public function withDurationAfterStart($duration): self
702
    {
703 6
        return $this->endingOn($this->startDate->add(self::getDuration($duration)));
704
    }
705
706
    /**
707
     * Returns a new instance with a new starting datepoint.
708
     *
709
     * This method MUST retain the state of the current instance, and return
710
     * an instance that contains the specified starting datepoint.
711
     *
712
     * @param mixed $duration a Duration
713
     */
714 6
    public function withDurationBeforeEnd($duration): self
715
    {
716 6
        return $this->startingOn($this->endDate->sub(self::getDuration($duration)));
717
    }
718
719
    /**
720
     * Returns a new instance with a new starting datepoint
721
     * moved forward or backward by the given interval.
722
     *
723
     * This method MUST retain the state of the current instance, and return
724
     * an instance that contains the specified starting datepoint.
725
     *
726
     * @param mixed $duration a Duration
727
     */
728 9
    public function moveStartDate($duration): self
729
    {
730 9
        return $this->startingOn($this->startDate->add(self::getDuration($duration)));
731
    }
732
733
    /**
734
     * Returns a new instance with a new ending datepoint
735
     * moved forward or backward by the given interval.
736
     *
737
     * This method MUST retain the state of the current instance, and return
738
     * an instance that contains the specified ending datepoint.
739
     *
740
     * @param mixed $duration a Duration
741
     */
742 9
    public function moveEndDate($duration): self
743
    {
744 9
        return $this->endingOn($this->endDate->add(self::getDuration($duration)));
745
    }
746
747
    /**
748
     * Returns a new instance where the datepoints
749
     * are moved forwards or backward simultaneously by the given DateInterval.
750
     *
751
     * This method MUST retain the state of the current instance, and return
752
     * an instance that contains the specified new datepoints.
753
     *
754
     * @param mixed $duration a Duration
755
     */
756 12
    public function move($duration): self
757
    {
758 12
        $duration = self::getDuration($duration);
759 12
        $interval = new self($this->startDate->add($duration), $this->endDate->add($duration));
760 12
        if ($this->equals($interval)) {
761 3
            return $this;
762
        }
763
764 12
        return $interval;
765
    }
766
767
    /**
768
     * Returns an instance where the given DateInterval is simultaneously
769
     * substracted from the starting datepoint and added to the ending datepoint.
770
     *
771
     * Depending on the duration value, the resulting instance duration will be expanded or shrinked.
772
     *
773
     * This method MUST retain the state of the current instance, and return
774
     * an instance that contains the specified new datepoints.
775
     *
776
     * @param mixed $duration a Duration
777
     */
778 12
    public function expand($duration): self
779
    {
780 12
        $duration = self::getDuration($duration);
781 12
        $interval = new self($this->startDate->sub($duration), $this->endDate->add($duration));
782 9
        if ($this->equals($interval)) {
783 3
            return $this;
784
        }
785
786 6
        return $interval;
787
    }
788
789
    /**
790
     * Merges one or more instances to return a new instance.
791
     * The resulting instance represents the largest duration possible.
792
     *
793
     * This method MUST retain the state of the current instance, and return
794
     * an instance that contains the specified new datepoints.
795
     *
796
     * [--------------------)
797
     *          U
798
     *                 [----------)
799
     *          =
800
     * [--------------------------)
801
     *
802
     *
803
     * @param Period ...$intervals
804
     */
805 18
    public function merge(self $interval, self ...$intervals): self
806
    {
807 18
        $intervals[] = $interval;
808 18
        $carry = $this;
809 18
        foreach ($intervals as $interval) {
810 18
            if ($carry->startDate > $interval->startDate) {
811 9
                $carry = $carry->startingOn($interval->startDate);
812
            }
813
814 18
            if ($carry->endDate < $interval->endDate) {
815 18
                $carry = $carry->endingOn($interval->endDate);
816
            }
817
        }
818
819 18
        return $carry;
820
    }
821
}
822