Completed
Pull Request — master (#93)
by ignace nyamagana
03:23
created

Period   F

Complexity

Total Complexity 146

Size/Duplication

Total Lines 1154
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 80
Bugs 6 Features 1
Metric Value
eloc 261
dl 0
loc 1154
ccs 314
cts 314
cp 1
rs 2
c 80
b 6
f 1
wmc 146

67 Methods

Rating   Name   Duplication   Size   Complexity  
A fromMonth() 0 5 1
A fromSemester() 0 6 1
A fromQuarter() 0 6 1
A fromIsoWeek() 0 5 1
A fromYear() 0 5 1
A fromIsoYear() 0 6 1
A fromDay() 0 5 1
A getDatepoint() 0 7 2
A fromDatePeriod() 0 3 1
A after() 0 5 1
A __set_state() 0 3 1
A around() 0 6 1
A before() 0 5 1
A durationEquals() 0 3 1
A isDuring() 0 3 1
A endingOn() 0 8 2
A bordersOnStart() 0 4 2
A isEndedBy() 0 10 4
A getTimestampInterval() 0 3 1
A withDurationBeforeEnd() 0 3 1
A bordersOnEnd() 0 3 1
B diff() 0 31 8
A splitBackwards() 0 13 3
A durationCompare() 0 4 1
A jsonSerialize() 0 7 1
A merge() 0 22 4
A equals() 0 5 3
A durationGreaterThan() 0 3 1
A isAfter() 0 9 4
A move() 0 9 2
A subtract() 0 11 3
A getBoundaryType() 0 3 1
A durationLessThan() 0 3 1
A getDuration() 0 7 2
A withDurationAfterStart() 0 3 1
B containsDatepoint() 0 12 9
A moveEndDate() 0 3 1
A format() 0 7 1
A toISO8601() 0 5 1
A __toString() 0 3 1
A expand() 0 9 2
A abuts() 0 3 2
A split() 0 10 3
A timestampIntervalDiff() 0 3 1
C containsInterval() 0 21 12
A startingOn() 0 8 2
A __construct() 0 19 3
A substract() 0 3 1
A getDatePeriod() 0 3 1
A getStartDate() 0 3 1
A intersect() 0 25 5
A getDateInterval() 0 3 1
A gap() 0 13 5
A fromISO8601() 0 25 5
A dateIntervalDiff() 0 3 1
A getEndDate() 0 3 1
A isEndExcluded() 0 3 1
A isStartExcluded() 0 3 1
A isStartIncluded() 0 3 1
A moveStartDate() 0 3 1
A contains() 0 7 2
A isEndIncluded() 0 3 1
A withBoundaryType() 0 7 2
A overlaps() 0 5 3
A isStartedBy() 0 10 4
A getDatePeriodBackwards() 0 11 3
A isBefore() 0 10 6

How to fix   Complexity   

Complex Class

Complex classes like Period often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Period, and based on these observations, apply Extract Interface, too.

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 Cassandra\Date;
0 ignored issues
show
Bug introduced by
The type Cassandra\Date was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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

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