Passed
Pull Request — master (#93)
by ignace nyamagana
03:06 queued 01:08
created

Period::jsonSerialize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 6
Bugs 0 Features 0
Metric Value
cc 1
eloc 4
c 6
b 0
f 0
nc 1
nop 0
dl 0
loc 7
ccs 4
cts 4
cp 1
crap 1
rs 10
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 897
    public function __construct($startDate, $endDate, string $boundaryType = self::INCLUDE_START_EXCLUDE_END)
83
    {
84 897
        $startDate = self::getDatepoint($startDate);
85 897
        $endDate = self::getDatepoint($endDate);
86 885
        if ($startDate > $endDate) {
87 66
            throw new Exception('The ending datepoint must be greater or equal to the starting datepoint');
88
        }
89
90 861
        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 858
        $this->startDate = $startDate;
99 858
        $this->endDate = $endDate;
100 858
        $this->boundaryType = $boundaryType;
101 858
    }
102
103
    /**
104
     * Returns a DateTimeImmutable instance.
105
     *
106
     * @param mixed $datepoint a Datepoint
107
     */
108 1062
    private static function getDatepoint($datepoint): DateTimeImmutable
109
    {
110 1062
        if ($datepoint instanceof DateTimeImmutable) {
111 840
            return $datepoint;
112
        }
113
114 720
        return Datepoint::create($datepoint);
115
    }
116
117
    /**
118
     * Returns a DateInterval instance.
119
     *
120
     * @param mixed $duration a Duration
121
     */
122 357
    private static function getDuration($duration): DateInterval
123
    {
124 357
        if ($duration instanceof DateInterval) {
125 150
            return $duration;
126
        }
127
128 207
        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::getDatepoint($startDate);
152
153 132
        return new self($startDate, $startDate->add(self::getDuration($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::getDatepoint($endDate);
165
166 27
        return new self($endDate->sub(self::getDuration($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 21
    public static function around($datepoint, $duration, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
177
    {
178 21
        $datepoint = self::getDatepoint($datepoint);
179 21
        $duration = self::getDuration($duration);
180
181 21
        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 6
            $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 51
    public static function fromDay(int $year, int $month = 1, int $day = 1, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
260
    {
261 51
        $startDate = (new DateTimeImmutable())->setDate($year, $month, $day)->setTime(0, 0);
262
263 51
        return new self($startDate, $startDate->add(new DateInterval('P1D')), $boundaryType);
264
    }
265
266
    /**
267
     * Creates new instance from an ISO 8601 time interval.
268
     * This named constructor only support full datepoint and interval.
269
     *
270
     * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
271
     *
272
     * @throws \Exception
273
     * @throws Exception
274
     */
275 39
    public static function fromISO8601(
276
        string $format,
277
        string $separator = '/',
278
        string $boundaryType = self::INCLUDE_START_EXCLUDE_END
279
    ): self {
280 39
        if (false === strpos($format, $separator)) {
281 3
            throw new Exception(sprintf('The submitted separator `%s` is not present in interval string `%s`.', $separator, $format));
282
        }
283
284
        /** @var string[] $parts */
285 36
        $parts = explode($separator, $format);
286 36
        if (2 !== count($parts)) {
287 3
            throw new Exception(sprintf('The submitted interval string `%s` is not a valid ISO8601 interval format.', $format));
288
        }
289
290 33
        [$start, $end] = $parts;
291 33
        if ('P' === $start[0]) {
292 3
            return self::before(Datepoint::fromIso8601($end), new DateInterval($start), $boundaryType);
293
        }
294
295 30
        if ('P' === $end[0]) {
296 3
            return self::after(Datepoint::fromIso8601($start), new DateInterval($end), $boundaryType);
297
        }
298
299 27
        return new self(Datepoint::fromIso8601($start), Datepoint::fromIso8601($end, $start), $boundaryType);
300
    }
301
302
    /**************************************************
303
     * Basic getters
304
     **************************************************/
305
306
    /**
307
     * Returns the starting datepoint.
308
     */
309 243
    public function getStartDate(): DateTimeImmutable
310
    {
311 243
        return $this->startDate;
312
    }
313
314
    /**
315
     * Returns the ending datepoint.
316
     */
317 219
    public function getEndDate(): DateTimeImmutable
318
    {
319 219
        return $this->endDate;
320
    }
321
322
    /**
323
     * Returns the instance boundary type.
324
     */
325 159
    public function getBoundaryType(): string
326
    {
327 159
        return $this->boundaryType;
328
    }
329
330
    /**
331
     * Returns the instance duration as expressed in seconds.
332
     */
333 36
    public function getTimestampInterval(): float
334
    {
335 36
        return $this->endDate->getTimestamp() - $this->startDate->getTimestamp();
336
    }
337
338
    /**
339
     * Returns the instance duration as a DateInterval object.
340
     */
341 129
    public function getDateInterval(): DateInterval
342
    {
343 129
        return $this->startDate->diff($this->endDate);
344
    }
345
346
    /**************************************************
347
     * String representation
348
     **************************************************/
349
350
    /**
351
     * Returns the string representation as a ISO8601 interval format.
352
     *
353
     * @see Period::toISO8601()
354
     */
355 6
    public function __toString()
356
    {
357 6
        return $this->toISO8601();
358
    }
359
360
    /**
361
     * Returns the string representation as a ISO8601 interval format.
362
     *
363
     * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
364
     *
365
     */
366 6
    public function toISO8601(): string
367
    {
368 6
        $interval = $this->jsonSerialize();
369
370 6
        return $interval['startDate'].'/'.$interval['endDate'];
371
    }
372
373
    /**
374
     * Returns the JSON representation of an instance.
375
     *
376
     * Based on the JSON representation of dates as
377
     * returned by Javascript Date.toJSON() method.
378
     *
379
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toJSON
380
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
381
     *
382
     * @return array<string>
383
     */
384 15
    public function jsonSerialize()
385
    {
386 15
        $utc = new DateTimeZone('UTC');
387
388
        return [
389 15
            'startDate' => $this->startDate->setTimezone($utc)->format(self::ISO8601_FORMAT),
390 15
            'endDate' => $this->endDate->setTimezone($utc)->format(self::ISO8601_FORMAT),
391
        ];
392
    }
393
394
    /**
395
     * Returns the mathematical representation of an instance as a left close, right open interval.
396
     *
397
     * @see https://en.wikipedia.org/wiki/Interval_(mathematics)#Notations_for_intervals
398
     * @see https://php.net/manual/en/function.date.php
399
     * @see https://www.postgresql.org/docs/9.3/static/rangetypes.html
400
     *
401
     * @param string $format the format of the outputted date string
402
     */
403 24
    public function format(string $format): string
404
    {
405 24
        return $this->boundaryType[0]
406 24
            .$this->startDate->format($format)
407 24
            .', '
408 24
            .$this->endDate->format($format)
409 24
            .$this->boundaryType[1];
410
    }
411
412
    /**************************************************
413
     * Boundary related methods
414
     **************************************************/
415
416
    /**
417
     * Tells whether the start datepoint is included in the boundary.
418
     */
419 12
    public function isStartIncluded(): bool
420
    {
421 12
        return '[' === $this->boundaryType[0];
422
    }
423
424
    /**
425
     * Tells whether the start datepoint is excluded from the boundary.
426
     */
427 81
    public function isStartExcluded(): bool
428
    {
429 81
        return '(' === $this->boundaryType[0];
430
    }
431
432
    /**
433
     * Tells whether the end datepoint is included in the boundary.
434
     */
435 12
    public function isEndIncluded(): bool
436
    {
437 12
        return ']' === $this->boundaryType[1];
438
    }
439
440
    /**
441
     * Tells whether the end datepoint is excluded from the boundary.
442
     */
443 81
    public function isEndExcluded(): bool
444
    {
445 81
        return ')' === $this->boundaryType[1];
446
    }
447
448
    /**************************************************
449
     * Duration comparison methods
450
     **************************************************/
451
452
    /**
453
     * Compares two instances according to their duration.
454
     *
455
     * Returns:
456
     * <ul>
457
     * <li> -1 if the current Interval is lesser than the submitted Interval object</li>
458
     * <li>  1 if the current Interval is greater than the submitted Interval object</li>
459
     * <li>  0 if both Interval objects have the same duration</li>
460
     * </ul>
461
     */
462 60
    public function durationCompare(self $interval): int
463
    {
464 60
        return $this->startDate->add($this->getDateInterval())
465 60
            <=> $this->startDate->add($interval->getDateInterval());
466
    }
467
468
    /**
469
     * Tells whether the current instance duration is equal to the submitted one.
470
     */
471 6
    public function durationEquals(self $interval): bool
472
    {
473 6
        return 0 === $this->durationCompare($interval);
474
    }
475
476
    /**
477
     * Tells whether the current instance duration is greater than the submitted one.
478
     */
479 18
    public function durationGreaterThan(self $interval): bool
480
    {
481 18
        return 1 === $this->durationCompare($interval);
482
    }
483
484
    /**
485
     * Tells whether the current instance duration is less than the submitted one.
486
     */
487 12
    public function durationLessThan(self $interval): bool
488
    {
489 12
        return -1 === $this->durationCompare($interval);
490
    }
491
492
    /**************************************************
493
     * Relation methods
494
     **************************************************/
495
496
    /**
497
     * Tells whether an instance is entirely before the specified index.
498
     *
499
     * The index can be a DateTimeInterface object or another Period object.
500
     *
501
     * [--------------------)
502
     *                          [--------------------)
503
     *
504
     * @param mixed $index a datepoint or a Period object
505
     */
506 90
    public function isBefore($index): bool
507
    {
508 90
        if ($index instanceof self) {
509 48
            return $this->endDate < $index->startDate
510 48
                || ($this->endDate == $index->startDate && $this->boundaryType[1] !== $index->boundaryType[0]);
511
        }
512
513 42
        $datepoint = self::getDatepoint($index);
514 42
        return $this->endDate < $datepoint
515 42
            || ($this->endDate == $datepoint && ')' === $this->boundaryType[1]);
516
    }
517
518
    /**
519
     * Tells whether the current instance end date meets the interval start date.
520
     *
521
     * [--------------------)
522
     *                      [--------------------)
523
     */
524 309
    public function bordersOnStart(self $interval): bool
525
    {
526 309
        return $this->endDate == $interval->startDate
527 309
            && '][' !== $this->boundaryType[1].$interval->boundaryType[0];
528
    }
529
530
    /**
531
     * Tells whether two intervals share the same start datepoint
532
     * and the same starting boundary type.
533
     *
534
     *    [----------)
535
     *    [--------------------)
536
     *
537
     * or
538
     *
539
     *    [--------------------)
540
     *    [---------)
541
     *
542
     * @param mixed $index a datepoint or a Period object
543
     */
544 27
    public function isStartedBy($index): bool
545
    {
546 27
        if ($index instanceof self) {
547 15
            return $this->startDate == $index->startDate
548 15
                && $this->boundaryType[0] === $index->boundaryType[0];
549
        }
550
551 12
        $index = self::getDatepoint($index);
552
553 12
        return $index == $this->startDate && '[' === $this->boundaryType[0];
554
    }
555
556
    /**
557
     * Tells whether an instance is fully contained in the specified interval.
558
     *
559
     *     [----------)
560
     * [--------------------)
561
     */
562 39
    public function isDuring(self $interval): bool
563
    {
564 39
        return $interval->containsInterval($this);
565
    }
566
567
    /**
568
     * Tells whether an instance fully contains the specified index.
569
     *
570
     * The index can be a DateTimeInterface object or another Period object.
571
     *
572
     * @param mixed $index a datepoint or a Period object
573
     */
574 144
    public function contains($index): bool
575
    {
576 144
        if ($index instanceof self) {
577 66
            return $this->containsInterval($index);
578
        }
579
580 78
        return $this->containsDatepoint(self::getDatepoint($index), $this->boundaryType);
581
    }
582
583
    /**
584
     * Tells whether an instance fully contains another instance.
585
     *
586
     * [--------------------)
587
     *     [----------)
588
     */
589 66
    private function containsInterval(self $interval): bool
590
    {
591 66
        if ($this->startDate < $interval->startDate && $this->endDate > $interval->endDate) {
592 18
            return true;
593
        }
594
595 63
        if ($this->startDate == $interval->startDate && $this->endDate == $interval->endDate) {
596 21
            return $this->boundaryType === $interval->boundaryType || '[]' === $this->boundaryType;
597
        }
598
599 42
        if ($this->startDate == $interval->startDate) {
600 12
            return ($this->boundaryType[0] === $interval->boundaryType[0] || '[' === $this->boundaryType[0])
601 12
                && $this->containsDatepoint($this->startDate->add($interval->getDateInterval()), $this->boundaryType);
602
        }
603
604 30
        if ($this->endDate == $interval->endDate) {
605 18
            return ($this->boundaryType[1] === $interval->boundaryType[1] || ']' === $this->boundaryType[1])
606 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

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