Passed
Pull Request — master (#71)
by ignace nyamagana
01:58
created

Period::equals()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 2

Importance

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