Passed
Pull Request — master (#71)
by ignace nyamagana
03:06
created

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