Completed
Pull Request — master (#80)
by
unknown
14:28
created

Period::substract()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 8
nc 4
nop 1
dl 0
loc 16
ccs 7
cts 7
cp 1
crap 5
rs 9.6111
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * League.Period (https://period.thephpleague.com)
5
 *
6
 * (c) Ignace Nyamagana Butera <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace League\Period;
15
16
use DateInterval;
17
use DatePeriod;
18
use DateTimeImmutable;
19
use DateTimeInterface;
20
use DateTimeZone;
21
use JsonSerializable;
22
use function array_keys;
23
use function implode;
24
use function sprintf;
25
26
/**
27
 * A immutable value object class to manipulate Time interval.
28
 *
29
 * @package League.period
30
 * @author  Ignace Nyamagana Butera <[email protected]>
31
 * @since   1.0.0
32
 */
33
final class Period implements JsonSerializable
34
{
35
    private const ISO8601_FORMAT = 'Y-m-d\TH:i:s.u\Z';
36
37
    private const BOUNDARY_TYPE = [
38
        self::INCLUDE_START_EXCLUDE_END => 1,
39
        self::INCLUDE_ALL => 1,
40
        self::EXCLUDE_START_INCLUDE_END => 1,
41
        self::EXCLUDE_ALL => 1,
42
    ];
43
44
    public const INCLUDE_START_EXCLUDE_END = '[)';
45
46
    public const EXCLUDE_START_INCLUDE_END = '(]';
47
48
    public const EXCLUDE_ALL = '()';
49
50
    public const INCLUDE_ALL = '[]';
51
52
    /**
53
     * The starting datepoint.
54
     *
55
     * @var DateTimeImmutable
56
     */
57
    private $startDate;
58
59
    /**
60
     * The ending datepoint.
61
     *
62
     * @var DateTimeImmutable
63
     */
64
    private $endDate;
65
66
    /**
67
     * The boundary type.
68
     *
69
     * @var string
70
     */
71
    private $boundaryType;
72
73
    /**
74
     * Creates a new instance.
75
     *
76
     * @param mixed $startDate the starting datepoint
77
     * @param mixed $endDate   the ending datepoint
78
     *
79
     * @throws Exception If $startDate is greater than $endDate
80
     */
81 861
    public function __construct($startDate, $endDate, string $boundaryType = self::INCLUDE_START_EXCLUDE_END)
82
    {
83 861
        $startDate = self::getDatepoint($startDate);
84 861
        $endDate = self::getDatepoint($endDate);
85 849
        if ($startDate > $endDate) {
86 66
            throw new Exception('The ending datepoint must be greater or equal to the starting datepoint');
87
        }
88
89 825
        if (!isset(self::BOUNDARY_TYPE[$boundaryType])) {
90 6
            throw new Exception(sprintf(
91 6
                'The boundary type `%s` is invalid. The only valid values are %s',
92 6
                $boundaryType,
93 6
                '`'.implode('`, `', array_keys(self::BOUNDARY_TYPE)).'`'
94
            ));
95
        }
96
97 822
        $this->startDate = $startDate;
98 822
        $this->endDate = $endDate;
99 822
        $this->boundaryType = $boundaryType;
100 822
    }
101
102
    /**
103
     * Returns a DateTimeImmutable instance.
104
     *
105
     * @param mixed $datepoint a Datepoint
106
     */
107 1026
    private static function getDatepoint($datepoint): DateTimeImmutable
108
    {
109 1026
        if ($datepoint instanceof DateTimeImmutable) {
110 813
            return $datepoint;
111
        }
112
113 693
        return Datepoint::create($datepoint);
114
    }
115
116
    /**
117
     * Returns a DateInterval instance.
118
     *
119
     * @param mixed $duration a Duration
120
     */
121 345
    private static function getDuration($duration): DateInterval
122
    {
123 345
        if ($duration instanceof DateInterval) {
124 150
            return $duration;
125
        }
126
127 195
        return Duration::create($duration);
128
    }
129
130
    /**************************************************
131
     * Named constructors
132
     **************************************************/
133
134
    /**
135
     * @inheritdoc
136
     */
137 6
    public static function __set_state(array $interval)
138
    {
139 6
        return new self($interval['startDate'], $interval['endDate'], $interval['boundaryType'] ?? self::INCLUDE_START_EXCLUDE_END);
140
    }
141
142
    /**
143
     * Creates new instance from a starting datepoint and a duration.
144
     *
145
     * @param mixed $startDate the starting datepoint
146
     * @param mixed $duration  a Duration
147
     */
148 120
    public static function after($startDate, $duration, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
149
    {
150 120
        $startDate = self::getDatepoint($startDate);
151
152 120
        return new self($startDate, $startDate->add(self::getDuration($duration)), $boundaryType);
153
    }
154
155
    /**
156
     * Creates new instance from a ending datepoint and a duration.
157
     *
158
     * @param mixed $endDate  the ending datepoint
159
     * @param mixed $duration a Duration
160
     */
161 27
    public static function before($endDate, $duration, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
162
    {
163 27
        $endDate = self::getDatepoint($endDate);
164
165 27
        return new self($endDate->sub(self::getDuration($duration)), $endDate, $boundaryType);
166
    }
167
168
    /**
169
     * Creates new instance where the given duration is simultaneously
170
     * substracted from and added to the datepoint.
171
     *
172
     * @param mixed $datepoint a Datepoint
173
     * @param mixed $duration  a Duration
174
     */
175 21
    public static function around($datepoint, $duration, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
176
    {
177 21
        $datepoint = self::getDatepoint($datepoint);
178 21
        $duration = self::getDuration($duration);
179
180 21
        return new self($datepoint->sub($duration), $datepoint->add($duration), $boundaryType);
181
    }
182
183
    /**
184
     * Creates new instance from a DatePeriod.
185
     */
186 12
    public static function fromDatePeriod(DatePeriod $datePeriod, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
187
    {
188 12
        return new self($datePeriod->getStartDate(), $datePeriod->getEndDate(), $boundaryType);
189
    }
190
191
    /**
192
     * Creates new instance for a specific year.
193
     */
194 12
    public static function fromYear(int $year): self
195
    {
196 12
        $startDate = (new DateTimeImmutable())->setDate($year, 1, 1)->setTime(0, 0);
197
198 12
        return new self($startDate, $startDate->add(new DateInterval('P1Y')));
199
    }
200
201
    /**
202
     * Creates new instance for a specific ISO year.
203
     */
204 6
    public static function fromIsoYear(int $year): self
205
    {
206 6
        return new self(
207 6
            (new DateTimeImmutable())->setISODate($year, 1)->setTime(0, 0),
208 6
            (new DateTimeImmutable())->setISODate(++$year, 1)->setTime(0, 0)
209
        );
210
    }
211
212
    /**
213
     * Creates new instance for a specific year and semester.
214
     */
215 18
    public static function fromSemester(int $year, int $semester = 1): self
216
    {
217 18
        $month = (($semester - 1) * 6) + 1;
218 18
        $startDate = (new DateTimeImmutable())->setDate($year, $month, 1)->setTime(0, 0);
219
220 18
        return new self($startDate, $startDate->add(new DateInterval('P6M')));
221
    }
222
223
    /**
224
     * Creates new instance for a specific year and quarter.
225
     */
226 18
    public static function fromQuarter(int $year, int $quarter = 1): self
227
    {
228 18
        $month = (($quarter - 1) * 3) + 1;
229 18
        $startDate = (new DateTimeImmutable())->setDate($year, $month, 1)->setTime(0, 0);
230
231 18
        return new self($startDate, $startDate->add(new DateInterval('P3M')));
232
    }
233
234
    /**
235
     * Creates new instance for a specific year and month.
236
     */
237 75
    public static function fromMonth(int $year, int $month = 1): self
238
    {
239 75
        $startDate = (new DateTimeImmutable())->setDate($year, $month, 1)->setTime(0, 0);
240
241 75
        return new self($startDate, $startDate->add(new DateInterval('P1M')));
242
    }
243
244
    /**
245
     * Creates new instance for a specific ISO8601 week.
246
     */
247 21
    public static function fromIsoWeek(int $year, int $week = 1): self
248
    {
249 21
        $startDate = (new DateTimeImmutable())->setISODate($year, $week, 1)->setTime(0, 0);
250
251 21
        return new self($startDate, $startDate->add(new DateInterval('P7D')));
252
    }
253
254
    /**
255
     * Creates new instance for a specific year, month and day.
256
     */
257 51
    public static function fromDay(int $year, int $month = 1, int $day = 1): self
258
    {
259 51
        $startDate = (new DateTimeImmutable())->setDate($year, $month, $day)->setTime(0, 0);
260
261 51
        return new self($startDate, $startDate->add(new DateInterval('P1D')));
262
    }
263
264
    /**************************************************
265
     * Basic getters
266
     **************************************************/
267
268
    /**
269
     * Returns the starting datepoint.
270
     */
271 240
    public function getStartDate(): DateTimeImmutable
272
    {
273 240
        return $this->startDate;
274
    }
275
276
    /**
277
     * Returns the ending datepoint.
278
     */
279 213
    public function getEndDate(): DateTimeImmutable
280
    {
281 213
        return $this->endDate;
282
    }
283
284
    /**
285
     * Returns the instance boundary type.
286
     */
287 159
    public function getBoundaryType(): string
288
    {
289 159
        return $this->boundaryType;
290
    }
291
292
    /**
293
     * Returns the instance duration as expressed in seconds.
294
     */
295 33
    public function getTimestampInterval(): float
296
    {
297 33
        return $this->endDate->getTimestamp() - $this->startDate->getTimestamp();
298
    }
299
300
    /**
301
     * Returns the instance duration as a DateInterval object.
302
     */
303 132
    public function getDateInterval(): DateInterval
304
    {
305 132
        return $this->startDate->diff($this->endDate);
306
    }
307
308
    /**************************************************
309
     * String representation
310
     **************************************************/
311
312
    /**
313
     * Returns the string representation as a ISO8601 interval format.
314
     *
315
     * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
316
     *
317
     * @return string
318
     */
319 6
    public function __toString()
320
    {
321 6
        $interval = $this->jsonSerialize();
322
323 6
        return $interval['startDate'].'/'.$interval['endDate'];
324
    }
325
326
    /**
327
     * Returns the JSON representation of an instance.
328
     *
329
     * Based on the JSON representation of dates as
330
     * returned by Javascript Date.toJSON() method.
331
     *
332
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toJSON
333
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
334
     *
335
     * @return array<string>
336
     */
337 15
    public function jsonSerialize()
338
    {
339 15
        $utc = new DateTimeZone('UTC');
340
341
        return [
342 15
            'startDate' => $this->startDate->setTimezone($utc)->format(self::ISO8601_FORMAT),
343 15
            'endDate' => $this->endDate->setTimezone($utc)->format(self::ISO8601_FORMAT),
344
        ];
345
    }
346
347
    /**
348
     * Returns the mathematical representation of an instance as a left close, right open interval.
349
     *
350
     * @see https://en.wikipedia.org/wiki/Interval_(mathematics)#Notations_for_intervals
351
     * @see https://php.net/manual/en/function.date.php
352
     * @see https://www.postgresql.org/docs/9.3/static/rangetypes.html
353
     *
354
     * @param string $format the format of the outputted date string
355
     */
356 18
    public function format(string $format): string
357
    {
358 18
        return $this->boundaryType[0]
359 18
            .$this->startDate->format($format)
360 18
            .', '
361 18
            .$this->endDate->format($format)
362 18
            .$this->boundaryType[1];
363
    }
364
365
    /**************************************************
366
     * Boundary related methods
367
     **************************************************/
368
369
    /**
370
     * Tells whether the start datepoint is included in the boundary.
371
     */
372 12
    public function isStartIncluded(): bool
373
    {
374 12
        return '[' === $this->boundaryType[0];
375
    }
376
377
    /**
378
     * Tells whether the start datepoint is excluded from the boundary.
379
     */
380 81
    public function isStartExcluded(): bool
381
    {
382 81
        return '(' === $this->boundaryType[0];
383
    }
384
385
    /**
386
     * Tells whether the end datepoint is included in the boundary.
387
     */
388 12
    public function isEndIncluded(): bool
389
    {
390 12
        return ']' === $this->boundaryType[1];
391
    }
392
393
    /**
394
     * Tells whether the end datepoint is excluded from the boundary.
395
     */
396 81
    public function isEndExcluded(): bool
397
    {
398 81
        return ')' === $this->boundaryType[1];
399
    }
400
401
    /**************************************************
402
     * Duration comparison methods
403
     **************************************************/
404
405
    /**
406
     * Compares two instances according to their duration.
407
     *
408
     * Returns:
409
     * <ul>
410
     * <li> -1 if the current Interval is lesser than the submitted Interval object</li>
411
     * <li>  1 if the current Interval is greater than the submitted Interval object</li>
412
     * <li>  0 if both Interval objects have the same duration</li>
413
     * </ul>
414
     */
415 60
    public function durationCompare(self $interval): int
416
    {
417 60
        return $this->startDate->add($this->getDateInterval())
418 60
            <=> $this->startDate->add($interval->getDateInterval());
419
    }
420
421
    /**
422
     * Tells whether the current instance duration is equal to the submitted one.
423
     */
424 6
    public function durationEquals(self $interval): bool
425
    {
426 6
        return 0 === $this->durationCompare($interval);
427
    }
428
429
    /**
430
     * Tells whether the current instance duration is greater than the submitted one.
431
     */
432 18
    public function durationGreaterThan(self $interval): bool
433
    {
434 18
        return 1 === $this->durationCompare($interval);
435
    }
436
437
    /**
438
     * Tells whether the current instance duration is less than the submitted one.
439
     */
440 12
    public function durationLessThan(self $interval): bool
441
    {
442 12
        return -1 === $this->durationCompare($interval);
443
    }
444
445
    /**************************************************
446
     * Relation methods
447
     **************************************************/
448
449
    /**
450
     * Tells whether an instance is entirely before the specified index.
451
     *
452
     * The index can be a DateTimeInterface object or another Period object.
453
     *
454
     * [--------------------)
455
     *                          [--------------------)
456
     *
457
     * @param mixed $index a datepoint or a Period object
458
     */
459 90
    public function isBefore($index): bool
460
    {
461 90
        if ($index instanceof self) {
462 48
            return $this->endDate < $index->startDate
463 48
                || ($this->endDate == $index->startDate && $this->boundaryType[1] !== $index->boundaryType[0]);
464
        }
465
466 42
        $datepoint = self::getDatepoint($index);
467 42
        return $this->endDate < $datepoint
468 42
            || ($this->endDate == $datepoint && ')' === $this->boundaryType[1]);
469
    }
470
471
    /**
472
     * Tells whether the current instance end date meets the interval start date.
473
     *
474
     * [--------------------)
475
     *                      [--------------------)
476
     */
477 285
    public function bordersOnStart(self $interval): bool
478
    {
479 285
        return $this->endDate == $interval->startDate
480 285
            && '][' !== $this->boundaryType[1].$interval->boundaryType[0];
481
    }
482
483
    /**
484
     * Tells whether two intervals share the same start datepoint
485
     * and the same starting boundary type.
486
     *
487
     *    [----------)
488
     *    [--------------------)
489
     *
490
     * or
491
     *
492
     *    [--------------------)
493
     *    [---------)
494
     *
495
     * @param mixed $index a datepoint or a Period object
496
     */
497 27
    public function isStartedBy($index): bool
498
    {
499 27
        if ($index instanceof self) {
500 15
            return $this->startDate == $index->startDate
501 15
                && $this->boundaryType[0] === $index->boundaryType[0];
502
        }
503
504 12
        $index = self::getDatepoint($index);
505
506 12
        return $index == $this->startDate && '[' === $this->boundaryType[0];
507
    }
508
509
    /**
510
     * Tells whether an instance is fully contained in the specified interval.
511
     *
512
     *     [----------)
513
     * [--------------------)
514
     */
515 39
    public function isDuring(self $interval): bool
516
    {
517 39
        return $interval->containsInterval($this);
518
    }
519
520
    /**
521
     * Tells whether an instance fully contains the specified index.
522
     *
523
     * The index can be a DateTimeInterface object or another Period object.
524
     *
525
     * @param mixed $index a datepoint or a Period object
526
     */
527 144
    public function contains($index): bool
528
    {
529 144
        if ($index instanceof self) {
530 66
            return $this->containsInterval($index);
531
        }
532
533 78
        return $this->containsDatepoint(self::getDatepoint($index), $this->boundaryType);
534
    }
535
536
    /**
537
     * Tells whether an instance fully contains another instance.
538
     *
539
     * [--------------------)
540
     *     [----------)
541
     */
542 66
    private function containsInterval(self $interval): bool
543
    {
544 66
        if ($this->startDate < $interval->startDate && $this->endDate > $interval->endDate) {
545 18
            return true;
546
        }
547
548 63
        if ($this->startDate == $interval->startDate && $this->endDate == $interval->endDate) {
549 21
            return $this->boundaryType === $interval->boundaryType || '[]' === $this->boundaryType;
550
        }
551
552 42
        if ($this->startDate == $interval->startDate) {
553 12
            return ($this->boundaryType[0] === $interval->boundaryType[0] || '[' === $this->boundaryType[0])
554 12
                && $this->containsDatepoint($this->startDate->add($interval->getDateInterval()), $this->boundaryType);
555
        }
556
557 30
        if ($this->endDate == $interval->endDate) {
558 18
            return ($this->boundaryType[1] === $interval->boundaryType[1] || ']' === $this->boundaryType[1])
559 18
                && $this->containsDatepoint($this->endDate->sub($interval->getDateInterval()), $this->boundaryType);
0 ignored issues
show
Bug introduced by
It seems like $this->endDate->sub($interval->getDateInterval()) can also be of type false; however, parameter $datepoint of League\Period\Period::containsDatepoint() does only seem to accept DateTimeInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

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