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

Period::fromMonth()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
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 1
cts 1
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 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 $precision): self
239 6
    {
240
        if (!isset(self::CALENDAR_LIST[$precision])) {
241
            throw new Exception('Unknown Calendar interval');
242
        }
243
244
        $datepoint = Datepoint::create($datepoint);
245
        if (self::CALENDAR_HOUR === $precision) {
246
            $startDate = $datepoint->setTime((int) $datepoint->format('H'), 0);
247
248
            return new self($startDate, $startDate->add(new DateInterval('PT1H')));
249
        }
250
251
        if (self::CALENDAR_MINUTE === $precision) {
252
            $startDate = $datepoint->setTime((int) $datepoint->format('H'), (int) $datepoint->format('i'));
253 75
254
            return new self($startDate, $startDate->add(new DateInterval('PT1M')));
255 75
        }
256 75
257 51
        if (self::CALENDAR_SECOND === $precision) {
258
            $startDate = $datepoint->setTime(
259
                (int) $datepoint->format('H'),
260 24
                (int) $datepoint->format('i'),
261
                (int) $datepoint->format('s')
262
            );
263
264
            return new self($startDate, $startDate->add(new DateInterval('PT1S')));
265
        }
266
267
        $datepoint = $datepoint->setTime(0, 0);
268
269
        if (self::CALENDAR_DAY === $precision) {
270
            return new self($datepoint, $datepoint->add(new DateInterval('P1D')));
271 12
        }
272
273 12
        if (self::CALENDAR_ISOWEEK === $precision) {
274 3
            $startDate = $datepoint->setISODate((int) $datepoint->format('o'), (int) $datepoint->format('W'), 1);
275 3
276 3
            return new self($startDate, $startDate->add(new DateInterval('P7D')));
277
        }
278 3
279
        $year = (int) $datepoint->format('Y');
280
        if (self::CALENDAR_MONTH === $precision) {
281 12
            $startDate = $datepoint->setDate($year, (int) $datepoint->format('n'), 1);
282 6
283
            return new self($startDate, $startDate->add(new DateInterval('P1M')));
284 6
        }
285
286
        if (self::CALENDAR_QUARTER === $precision) {
287
            $month = (intdiv((int) $datepoint->format('n'), 3) * 3) + 1;
288
            $startDate = $datepoint->setDate($year, $month, 1);
289
290
            return new self($startDate, $startDate->add(new DateInterval('P3M')));
291
        }
292
293
        if (self::CALENDAR_SEMESTER === $precision) {
294
            $month = (intdiv((int) $datepoint->format('n'), 6) * 6) + 1;
295 39
            $startDate = $datepoint->setDate($year, $month, 1);
296
297 39
            return new self($startDate, $startDate->add(new DateInterval('P6M')));
298 6
        }
299
300 6
        if (self::CALENDAR_YEAR === $precision) {
301
            $startDate = $datepoint->setDate($year, 1, 1);
302
303 36
            return new self($startDate, $startDate->add(new DateInterval('P1Y')));
304
        }
305 30
306
        $iso_year = (int) $datepoint->format('o');
307
308
        return new self($datepoint->setISODate($iso_year, 1), $datepoint->setISODate(++$iso_year, 1));
309
    }
310
311
    /**
312
     * Creates new instance from a DatePeriod.
313
     */
314
    public static function fromDatePeriod(DatePeriod $datePeriod): self
315
    {
316 24
        return new self($datePeriod->getStartDate(), $datePeriod->getEndDate());
317
    }
318 24
319 3
    /**
320 3
     * Creates a new instance.
321 3
     *
322 3
     * @param mixed $startDate the starting included datepoint
323
     * @param mixed $endDate   the ending excluded datepoint
324
     *
325 3
     * @throws Exception If $startDate is greater than $endDate
326
     */
327
    public function __construct($startDate, $endDate)
328 24
    {
329 24
        $startDate = Datepoint::create($startDate);
330 18
        $endDate = Datepoint::create($endDate);
331
        if ($startDate > $endDate) {
332
            throw new Exception('The ending datepoint must be greater or equal to the starting datepoint');
333 18
        }
334
        $this->startDate = $startDate;
335
        $this->endDate = $endDate;
336
    }
337
338
    /**
339
     * Returns the starting included datepoint.
340
     */
341
    public function getStartDate(): DateTimeImmutable
342
    {
343
        return $this->startDate;
344
    }
345
346 3
    /**
347
     * Returns the ending excluded datepoint.
348 3
     */
349
    public function getEndDate(): DateTimeImmutable
350 3
    {
351
        return $this->endDate;
352
    }
353
354
    /**
355
     * Returns the instance duration as expressed in seconds.
356
     */
357
    public function getTimestampInterval(): float
358
    {
359
        return $this->endDate->getTimestamp() - $this->startDate->getTimestamp();
360
    }
361
362
    /**
363 3
     * Returns the instance duration as a DateInterval object.
364
     */
365 3
    public function getDateInterval(): DateInterval
366
    {
367 3
        return $this->startDate->diff($this->endDate);
368
    }
369
370
    /**
371
     * Allows iteration over a set of dates and times,
372
     * recurring at regular intervals, over the instance.
373
     *
374
     * @see http://php.net/manual/en/dateperiod.construct.php
375
     */
376
    public function getDatePeriod($duration, int $option = 0): DatePeriod
377
    {
378
        return new DatePeriod($this->startDate, Duration::create($duration), $this->endDate, $option);
379
    }
380 3
381
    /**
382 3
     * Allows iteration over a set of dates and times,
383
     * recurring at regular intervals, over the instance backwards starting from
384 3
     * the instance ending datepoint.
385
     */
386
    public function getDatePeriodBackwards($duration, int $option = 0): iterable
387
    {
388
        $duration = Duration::create($duration);
389
        $date = $this->endDate;
390
        if ((bool) ($option & DatePeriod::EXCLUDE_START_DATE)) {
391
            $date = $this->endDate->sub($duration);
392
        }
393
394
        while ($date > $this->startDate) {
395
            yield $date;
396
            $date = $date->sub($duration);
397 3
        }
398
    }
399 3
400
    /**
401 3
     * Returns the string representation as a ISO8601 interval format.
402
     *
403
     * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
404
     *
405
     * @return string
406
     */
407
    public function __toString()
408
    {
409
        $interval = $this->jsonSerialize();
410
411
        return $interval['startDate'].'/'.$interval['endDate'];
412 177
    }
413
414 177
    /**
415
     * Returns the JSON representation of an instance.
416
     *
417
     * Based on the JSON representation of dates as
418
     * returned by Javascript Date.toJSON() method.
419
     *
420
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toJSON
421
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
422
     *
423
     * @return array<string>
424
     */
425 162
    public function jsonSerialize()
426
    {
427 162
        $utc = new DateTimeZone('UTC');
428
429
        return [
430
            'startDate' => $this->startDate->setTimezone($utc)->format(self::ISO8601_FORMAT),
431
            'endDate' => $this->endDate->setTimezone($utc)->format(self::ISO8601_FORMAT),
432
        ];
433
    }
434
435 18
    /**
436
     * Returns the mathematical representation of an instance as a left close, right open interval.
437 18
     *
438
     * @see https://en.wikipedia.org/wiki/Interval_(mathematics)#Notations_for_intervals
439
     * @see https://php.net/manual/en/function.date.php
440
     * @see https://www.postgresql.org/docs/9.3/static/rangetypes.html
441
     *
442
     * @param string $format the format of the outputted date string
443
     */
444
    public function format(string $format): string
445 33
    {
446
        return '['.$this->startDate->format($format).', '.$this->endDate->format($format).')';
447 33
    }
448
449
    /**
450
     * Compares two instances according to their duration.
451
     *
452
     * Returns:
453
     * <ul>
454
     * <li> -1 if the current Interval is lesser than the submitted Interval object</li>
455
     * <li>  1 if the current Interval is greater than the submitted Interval object</li>
456
     * <li>  0 if both Interval objects have the same duration</li>
457
     * </ul>
458
     */
459
    public function durationCompare(self $interval): int
460
    {
461
        return $this->endDate <=> $this->startDate->add($interval->getDateInterval());
462
    }
463
464
    /**
465
     * Tells whether the current instance duration is equal to the submitted one.
466
     */
467
    public function durationEquals(self $interval): bool
468
    {
469 24
        return 0 === $this->durationCompare($interval);
470
    }
471 24
472
    /**
473
     * Tells whether the current instance duration is greater than the submitted one.
474
     */
475
    public function durationGreaterThan(self $interval): bool
476
    {
477
        return 1 === $this->durationCompare($interval);
478
    }
479
480
    /**
481
     * Tells whether the current instance duration is less than the submitted one.
482
     */
483
    public function durationLessThan(self $interval): bool
484
    {
485
        return -1 === $this->durationCompare($interval);
486
    }
487
488
    /**
489
     * Tells whether two intervals share the same datepoints.
490
     *
491
     * [--------------------)
492
     * [--------------------)
493
     */
494
    public function equals(self $interval): bool
495
    {
496
        return $this->startDate == $interval->startDate
497 12
            && $this->endDate == $interval->endDate;
498
    }
499 12
500 12
    /**
501
     * Tells whether two intervals abuts.
502 12
     *
503 12
     * [--------------------)
504 6
     *                      [--------------------)
505
     * or
506 12
     *                      [--------------------)
507
     * [--------------------)
508 9
     */
509 9
    public function abuts(self $interval): bool
510 9
    {
511
        return $this->startDate == $interval->endDate
512
            || $this->endDate == $interval->startDate;
513
    }
514
515
    /**
516
     * Tells whether two intervals overlaps.
517
     *
518
     * [--------------------)
519
     *          [--------------------)
520
     */
521
    public function overlaps(self $interval): bool
522
    {
523
        return $this->startDate < $interval->endDate
524
            && $this->endDate > $interval->startDate;
525
    }
526
527
    /**
528
     * Tells whether an interval is entirely after the specified index.
529
     * The index can be a DateTimeInterface object or another Period object.
530
     *
531
     *                          [--------------------)
532
     * [--------------------)
533
     */
534
    public function isAfter($index): bool
535 6
    {
536
        if ($index instanceof self) {
537 6
            return $this->startDate >= $index->endDate;
538 6
        }
539
540 6
        return $this->startDate > Datepoint::create($index);
541 6
    }
542 3
543
    /**
544 6
     * Tells whether an instance is entirely before the specified index.
545
     *
546 6
     * The index can be a DateTimeInterface object or another Period object.
547 6
     *
548 6
     * [--------------------)
549
     *                          [--------------------)
550
     */
551
    public function isBefore($index): bool
552
    {
553
        if ($index instanceof self) {
554
            return $this->endDate <= $index->startDate;
555
        }
556
557
        return $this->endDate <= Datepoint::create($index);
558 3
    }
559
560 3
    /**
561
     * Tells whether an instance fully contains the specified index.
562 3
     *
563
     * The index can be a DateTimeInterface object or another Period object.
564
     *
565
     */
566
    public function contains($index): bool
567
    {
568
        if ($index instanceof self) {
569
            return $this->containsInterval($index);
570
        }
571
572
        return $this->containsDatepoint(Datepoint::create($index));
573 6
    }
574
575 6
    /**
576 6
     * Tells whether an instance fully contains another instance.
577 6
     *
578
     * [--------------------)
579
     *     [----------)
580 6
     */
581 6
    private function containsInterval(self $interval): bool
582
    {
583
        return $this->containsDatepoint($interval->startDate)
584
            && ($interval->endDate >= $this->startDate && $interval->endDate <= $this->endDate);
585
    }
586
587
    /**
588
     * Tells whether an instance contains a datepoint.
589
     *
590
     * [------|------------)
591
     */
592
    private function containsDatepoint(DateTimeInterface $datepoint): bool
593
    {
594
        return $datepoint >= $this->startDate && $datepoint < $this->endDate;
595
    }
596
597
    /**
598
     * Allows splitting an instance in smaller Period objects according to a given interval.
599 21
     *
600
     * The returned iterable Interval set is ordered so that:
601 21
     * <ul>
602
     * <li>The first returned object MUST share the starting datepoint of the parent object.</li>
603
     * <li>The last returned object MUST share the ending datepoint of the parent object.</li>
604
     * <li>The last returned object MUST have a duration equal or lesser than the submitted interval.</li>
605
     * <li>All returned objects except for the first one MUST start immediately after the previously returned object</li>
606
     * </ul>
607
     *
608
     * @return iterable<Period>
609
     */
610
    public function split($duration): iterable
611
    {
612 9
        $duration = Duration::create($duration);
613
        foreach ($this->getDatePeriod($duration) as $startDate) {
614 9
            $endDate = $startDate->add($duration);
615
            if ($endDate > $this->endDate) {
616
                $endDate = $this->endDate;
617
            }
618
619
            yield new self($startDate, $endDate);
620
        }
621
    }
622
623
    /**
624
     * Allows splitting an instance in smaller Period objects according to a given interval.
625 9
     *
626
     * The returned iterable Period set is ordered so that:
627 9
     * <ul>
628
     * <li>The first returned object MUST share the ending datepoint of the parent object.</li>
629
     * <li>The last returned object MUST share the starting datepoint of the parent object.</li>
630
     * <li>The last returned object MUST have a duration equal or lesser than the submitted interval.</li>
631
     * <li>All returned objects except for the first one MUST end immediately before the previously returned object</li>
632
     * </ul>
633
     *
634
     * @return iterable<Period>
635
     */
636
    public function splitBackwards($duration): iterable
637
    {
638 3
        $endDate = $this->endDate;
639
        $duration = Duration::create($duration);
640 3
        do {
641
            $startDate = $endDate->sub($duration);
642
            if ($startDate < $this->startDate) {
643
                $startDate = $this->startDate;
644
            }
645
            yield new self($startDate, $endDate);
646
647
            $endDate = $startDate;
648
        } while ($endDate > $this->startDate);
649
    }
650 18
651
    /**
652 18
     * Returns the computed intersection between two instances as a new instance.
653 18
     *
654
     * [--------------------)
655
     *          ∩
656
     *                 [----------)
657
     *          =
658
     *                 [----)
659
     *
660
     * @throws Exception If both objects do not overlaps
661
     */
662
    public function intersect(self $interval): self
663 42
    {
664
        if (!$this->overlaps($interval)) {
665 42
            throw new Exception('Both '.self::class.' objects should overlaps');
666 42
        }
667
668
        return new self(
669
            ($interval->startDate > $this->startDate) ? $interval->startDate : $this->startDate,
670
            ($interval->endDate < $this->endDate) ? $interval->endDate : $this->endDate
671
        );
672
    }
673
674
    /**
675
     * Returns the computed difference between two overlapping instances as
676 36
     * an array containing Period objects or the null value.
677
     *
678 36
     * The array will always contains 2 elements:
679 36
     *
680 36
     * <ul>
681
     * <li>an NULL filled array if both objects have the same datepoints</li>
682
     * <li>one Period object and NULL if both objects share one datepoint</li>
683
     * <li>two Period objects if both objects share no datepoint</li>
684
     * </ul>
685
     *
686
     * [--------------------)
687
     *          \
688
     *                [-----------)
689
     *          =
690
     * [--------------)  +  [-----)
691
     *
692
     * @return array<null|Period>
693
     */
694
    public function diff(self $interval): array
695
    {
696
        if ($interval->equals($this)) {
697 9
            return [null, null];
698
        }
699 9
700 3
        $intersect = $this->intersect($interval);
701
        $merge = $this->merge($interval);
702
        if ($merge->startDate == $intersect->startDate) {
703 6
            return [$merge->startingOn($intersect->endDate), null];
704
        }
705
706
        if ($merge->endDate == $intersect->endDate) {
707
            return [$merge->endingOn($intersect->startDate), null];
708
        }
709
710
        return [
711
            $merge->endingOn($intersect->startDate),
712
            $merge->startingOn($intersect->endDate),
713
        ];
714
    }
715
716
    /**
717
     * Returns the computed gap between two instances as a new instance.
718
     *
719
     * [--------------------)
720 12
     *          +
721
     *                          [----------)
722 12
     *          =
723 6
     *                      [---)
724
     *
725
     * @throws Exception If both instance overlaps
726 6
     */
727
    public function gap(self $interval): self
728
    {
729
        if ($this->overlaps($interval)) {
730
            throw new Exception('Both '.self::class.' objects must not overlaps');
731
        }
732
733
        if ($interval->startDate > $this->startDate) {
734
            return new self($this->endDate, $interval->startDate);
735
        }
736
737
        return new self($interval->endDate, $this->startDate);
738
    }
739
740
    /**
741
     * Returns the difference between two instances expressed in seconds.
742
     */
743
    public function timestampIntervalDiff(self $interval): float
744 24
    {
745
        return $this->getTimestampInterval() - $interval->getTimestampInterval();
746 24
    }
747 9
748
    /**
749
     * Returns the difference between two instances expressed with a DateInterval object.
750 24
     */
751
    public function dateIntervalDiff(self $interval): DateInterval
752
    {
753
        return $this->endDate->diff($this->startDate->add($interval->getDateInterval()));
754
    }
755
756
    /**
757
     * Returns an instance with the specified starting datepoint.
758
     *
759
     * This method MUST retain the state of the current instance, and return
760
     * an instance that contains the specified starting datepoint.
761 9
     */
762
    public function startingOn($datepoint): self
763 9
    {
764 9
        $startDate = Datepoint::create($datepoint);
765
        if ($startDate == $this->startDate) {
766
            return $this;
767
        }
768
769
        return new self($startDate, $this->endDate);
770
    }
771
772
    /**
773
     * Returns an instance with the specified ending datepoint.
774
     *
775 24
     * This method MUST retain the state of the current instance, and return
776
     * an instance that contains the specified ending datepoint.
777 24
     */
778
    public function endingOn($datepoint): self
779 24
    {
780 24
        $endDate = Datepoint::create($datepoint);
781
        if ($endDate == $this->endDate) {
782
            return $this;
783
        }
784
785
        return new self($this->startDate, $endDate);
786
    }
787
788
    /**
789
     * Returns a new instance with a new ending datepoint.
790 9
     *
791
     * This method MUST retain the state of the current instance, and return
792 9
     * an instance that contains the specified ending datepoint.
793
     */
794
    public function withDurationAfterStart($duration): self
795
    {
796
        return $this->endingOn($this->startDate->add(Duration::create($duration)));
797
    }
798
799
    /**
800
     * Returns a new instance with a new starting datepoint.
801
     *
802 12
     * This method MUST retain the state of the current instance, and return
803
     * an instance that contains the specified starting datepoint.
804 12
     */
805
    public function withDurationBeforeEnd($duration): self
806
    {
807
        return $this->startingOn($this->endDate->sub(Duration::create($duration)));
808
    }
809
810
    /**
811
     * Returns a new instance with a new starting datepoint
812
     * moved forward or backward by the given interval.
813
     *
814
     * This method MUST retain the state of the current instance, and return
815
     * an instance that contains the specified starting datepoint.
816
     */
817
    public function moveStartDate($duration): self
818
    {
819
        return $this->startingOn($this->startDate->add(Duration::create($duration)));
820
    }
821 6
822
    /**
823 6
     * Returns a new instance with a new ending datepoint
824
     * moved forward or backward by the given interval.
825
     *
826
     * This method MUST retain the state of the current instance, and return
827
     * an instance that contains the specified ending datepoint.
828
     */
829
    public function moveEndDate($duration): self
830
    {
831
        return $this->endingOn($this->endDate->add(Duration::create($duration)));
832
    }
833
834
    /**
835
     * Returns a new instance where the datepoints
836
     * are moved forwards or backward simultaneously by the given DateInterval.
837
     *
838
     * This method MUST retain the state of the current instance, and return
839
     * an instance that contains the specified new datepoints.
840 6
     */
841
    public function move($duration): self
842 6
    {
843
        $duration = Duration::create($duration);
844
        $interval = new self($this->startDate->add($duration), $this->endDate->add($duration));
845
        if ($this->equals($interval)) {
846
            return $this;
847
        }
848
849
        return $interval;
850
    }
851
852
    /**
853
     * Returns an instance where the given DateInterval is simultaneously
854
     * substracted from the starting datepoint and added to the ending datepoint.
855
     *
856
     * Depending on the duration value, the resulting instance duration will be expanded or shrinked.
857
     *
858
     * This method MUST retain the state of the current instance, and return
859
     * an instance that contains the specified new datepoints.
860 9
     */
861
    public function expand($duration): self
862 9
    {
863
        $duration = Duration::create($duration);
864
        $interval = new self($this->startDate->sub($duration), $this->endDate->add($duration));
865
        if ($this->equals($interval)) {
866
            return $this;
867
        }
868
869
        return $interval;
870
    }
871
872
    /**
873
     * Merges one or more instances to return a new instance.
874
     * The resulting instance represents the largest duration possible.
875
     *
876
     * This method MUST retain the state of the current instance, and return
877
     * an instance that contains the specified new datepoints.
878
     *
879
     * [--------------------)
880 12
     *          U
881
     *                 [----------)
882 12
     *          =
883
     * [--------------------------)
884
     *
885
     *
886
     * @param Period ...$intervals
887
     */
888
    public function merge(self $interval, self ...$intervals): self
889
    {
890
        $intervals[] = $interval;
891
        $carry = $this;
892
        foreach ($intervals as $interval) {
893
            if ($carry->startDate > $interval->startDate) {
894
                $carry = $carry->startingOn($interval->startDate);
895
            }
896
897
            if ($carry->endDate < $interval->endDate) {
898
                $carry = $carry->endingOn($interval->endDate);
899
            }
900 12
        }
901
902 12
        return $carry;
903
    }
904
}
905