Period::isBefore()   A
last analyzed

Complexity

Conditions 6
Paths 6

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 6

Importance

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

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