Completed
Push — master ( 0ac86a...c35f79 )
by ignace nyamagana
03:29
created

Period::createFromDatePeriod()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 8
ccs 4
cts 4
cp 1
rs 9.4285
cc 2
eloc 4
nc 2
nop 1
crap 2
1
<?php
2
/**
3
 * League.Period (http://period.thephpleague.com)
4
 *
5
 * @package   League.period
6
 * @author    Ignace Nyamagana Butera <[email protected]>
7
 * @copyright 2014-2015 Ignace Nyamagana Butera
8
 * @license   https://github.com/thephpleague/period/blob/master/LICENSE (MIT License)
9
 * @version   4.0.0
10
 * @link      https://github.com/thephpleague/period/
11
 */
12
declare(strict_types=1);
13
14
namespace League\Period;
15
16
use DateInterval;
17
use DatePeriod;
18
use DateTime;
19
use DateTimeImmutable;
20
use DateTimeInterface;
21
use DateTimeZone;
22
use Generator;
23
use JsonSerializable;
24
25
/**
26
 * A immutable value object class to manipulate Time Range.
27
 *
28
 * @package League.period
29
 * @author  Ignace Nyamagana Butera <[email protected]>
30
 * @since   1.0.0
31
 */
32
final class Period implements JsonSerializable
33
{
34
    /**
35
     * Period starting included date point.
36
     *
37
     * @var DateTimeImmutable
38
     */
39
    protected $startDate;
40
41
    /**
42
     * Period ending excluded date point.
43
     *
44
     * @var DateTimeImmutable
45
     */
46
    protected $endDate;
47
48
    /**
49
     * @inheritdoc
50
     */
51 3
    public static function __set_state(array $period): self
52
    {
53 3
        return new self($period['startDate'], $period['endDate']);
54
    }
55
56
    /**
57
     * Create a new instance.
58
     *
59
     * @param DateTimeInterface|string $startDate starting included date point
60
     * @param DateTimeInterface|string $endDate   ending excluded date point
61
     *
62
     * @throws Exception If $startDate is greater than $endDate
63
     */
64 255
    public function __construct($startDate, $endDate)
65
    {
66 255
        $startDate = static::filterDatePoint($startDate);
67 255
        $endDate = static::filterDatePoint($endDate);
68 255
        if ($startDate > $endDate) {
69 39
            throw new Exception('The ending datepoint must be greater or equal to the starting datepoint');
70
        }
71 246
        $this->startDate = $startDate;
72 246
        $this->endDate = $endDate;
73 246
    }
74
75
    /**
76
     * Validate the DateTimeInterface.
77
     *
78
     * @param DateTimeInterface|string $datepoint
79
     *
80
     * @return DateTimeImmutable
81
     */
82 282
    protected static function filterDatePoint($datepoint): DateTimeImmutable
83
    {
84 282
        if ($datepoint instanceof DateTimeImmutable) {
85 246
            return $datepoint;
86
        }
87
88 222
        if ($datepoint instanceof DateTime) {
89 78
            return DateTimeImmutable::createFromMutable($datepoint);
90
        }
91
92 153
        return date_create_immutable($datepoint);
93
    }
94
95
    /**
96
     * Create a Period object from a starting point and an interval.
97
     *
98
     * The interval can be
99
     * <ul>
100
     * <li>a DateInterval object</li>
101
     * <li>an int interpreted as the duration expressed in seconds.</li>
102
     * <li>a string in a format supported by DateInterval::createFromDateString</li>
103
     * </ul>
104
     *
105
     * @param DateTimeInterface|string $startDate The start date point
106
     * @param mixed                    $interval  The interval
107
     *
108
     * @return self
109
     */
110 138
    public static function createFromDuration($startDate, $interval): self
111
    {
112 138
        $startDate = static::filterDatePoint($startDate);
113
114 138
        return new self($startDate, $startDate->add(static::filterDateInterval($interval)));
115
    }
116
117
    /**
118
     * Validate a DateInterval.
119
     *
120
     * The interval can be
121
     * <ul>
122
     * <li>a DateInterval object</li>
123
     * <li>an int interpreted as the duration expressed in seconds.</li>
124
     * <li>a string in a format supported by DateInterval::createFromDateString</li>
125
     * </ul>
126
     *
127
     * @param mixed $interval The interval
128
     *
129
     * @return DateInterval
130
     */
131 177
    protected static function filterDateInterval($interval): DateInterval
132
    {
133 177
        if ($interval instanceof DateInterval) {
134 27
            return $interval;
135
        }
136
137 165
        if (false !== ($res = filter_var($interval, FILTER_VALIDATE_INT))) {
138 30
            return new DateInterval('PT'.$res.'S');
139
        }
140
141 153
        return DateInterval::createFromDateString($interval);
142
    }
143
144
    /**
145
     * Create a Period object from a ending excluded datepoint and an interval.
146
     *
147
     * The interval can be
148
     * <ul>
149
     * <li>a DateInterval object</li>
150
     * <li>an int interpreted as the duration expressed in seconds.</li>
151
     * <li>a string in a format supported by DateInterval::createFromDateString</li>
152
     * </ul>
153
     *
154
     * @param DateTimeInterface|string $endDate  The start date point
155
     * @param mixed                    $interval The interval
156
     *
157
     * @return self
158
     */
159 24
    public static function createFromDurationBeforeEnd($endDate, $interval): self
160
    {
161 24
        $endDate = static::filterDatePoint($endDate);
162
163 24
        return new self($endDate->sub(static::filterDateInterval($interval)), $endDate);
164
    }
165
166
    /**
167
     * Create a Period object from a DatePeriod
168
     *
169
     * @param DatePeriod $datePeriod
170
     *
171
     * @throws Exception If the submitted DatePeriod lacks an End DateTimeInterface
172
     *
173
     * @return self
174
     */
175 6
    public static function createFromDatePeriod(DatePeriod $datePeriod): self
176
    {
177 6
        if (null !== ($endDate = $datePeriod->getEndDate())) {
178 3
            return new self($datePeriod->getStartDate(), $endDate);
179
        }
180
181 3
        throw new Exception('The submitted DatePeriod object does not contain an end date');
182
    }
183
184
    /**
185
     * Create a Period object for a specific Year
186
     *
187
     * @param DateTimeInterface|string|int $year
188
     *
189
     * @return self
190
     */
191 18
    public static function createFromYear($year): self
192
    {
193 18
        if (is_int($year)) {
194 15
            $startDate = date_create_immutable($year.'-01-01');
195
196 15
            return new self($startDate, $startDate->add(new DateInterval('P1Y')));
197
        }
198
199 6
        $startDate = self::approximateDate('Y-01-01 00:00:00', static::filterDatePoint($year));
200
201 6
        return new self($startDate, $startDate->add(new DateInterval('P1Y')));
202
    }
203
204
    /**
205
     * Returns a DateTimeInterface object whose value are
206
     * approximated to the second
207
     *
208
     * @param string            $format
209
     * @param DateTimeInterface $datepoint
210
     *
211
     * @return DateTimeInterface
212
     */
213 21
    protected static function approximateDate(string $format, DateTimeInterface $datepoint): DateTimeInterface
214
    {
215 21
        return $datepoint->createFromFormat('Y-m-d H:i:s', $datepoint->format($format), $datepoint->getTimeZone());
0 ignored issues
show
Bug introduced by
The method createFromFormat() does not exist on DateTimeInterface. Did you maybe mean format()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
216
    }
217
218
    /**
219
     * Create a Period object for a specific semester in a given year
220
     *
221
     * @param DateTimeInterface|string|int $year
222
     * @param int                          $semester Semester Index from 1 to 2
223
     *
224
     * @return self
225
     */
226 12
    public static function createFromSemester($year, int $semester = null): self
227
    {
228 12
        if (1 == func_num_args()) {
229 3
            $date = self::filterDatePoint($year);
230 3
            $month = (intdiv((int) $date->format('m'), 6) * 6) + 1;
231 3
            $startDate = self::approximateDate('Y-m-01 00:00:00', $date->setDate((int) $date->format('Y'), $month, 1));
232
233 3
            return new self($startDate, $startDate->add(new DateInterval('P6M')));
234
        }
235
236 12
        $month = ((static::validateRange($semester, 1, 2) - 1) * 6) + 1;
237 6
        $startDate = date_create_immutable($year.'-'.sprintf("%'.02d", $month).'-01');
238
239 6
        return new self($startDate, $startDate->add(new DateInterval('P6M')));
240
    }
241
242
    /**
243
     * Validate a int according to a range.
244
     *
245
     * @param int $value the value to validate
246
     * @param int $min   the minimum value
247
     * @param int $max   the maximal value
248
     *
249
     * @throws Exception If the value is not in the range
250
     *
251
     * @return int
252
     */
253 75
    protected static function validateRange(int $value, int $min, int $max): int
254
    {
255 75
        $res = filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => $min, 'max_range' => $max]]);
256 75
        if (false !== $res) {
257 51
            return $res;
258
        }
259
260 24
        throw new Exception('the submitted value is not contained within the valid range');
261
    }
262
263
    /**
264
     * Create a Period object for a specific quarter in a given year
265
     *
266
     * @param DateTimeInterface|string|int $year
267
     * @param int                          $quarter Quarter Index from 1 to 4
268
     *
269
     * @return self
270
     */
271 12
    public static function createFromQuarter($year, int $quarter = null): self
272
    {
273 12
        if (1 == func_num_args()) {
274 3
            $date = self::filterDatePoint($year);
275 3
            $month = (intdiv((int) $date->format('m'), 3) * 3) + 1;
276 3
            $startDate = self::approximateDate('Y-m-01 00:00:00', $date->setDate((int) $date->format('Y'), $month, 1));
277
278 3
            return new self($startDate, $startDate->add(new DateInterval('P3M')));
279
        }
280
281 12
        $month = ((static::validateRange($quarter, 1, 4) - 1) * 3) + 1;
282 6
        $startDate = date_create_immutable($year.'-'.sprintf("%'.02d", $month).'-01');
283
284 6
        return new self($startDate, $startDate->add(new DateInterval('P3M')));
285
    }
286
287
    /**
288
     * Create a Period object for a specific year and month
289
     *
290
     * @param DateTimeInterface|string|int $year
291
     * @param int                          $month Month index from 1 to 12
292
     *
293
     * @return self
294
     */
295 39
    public static function createFromMonth($year, int $month = null): self
296
    {
297 39
        if (1 == func_num_args()) {
298 6
            $startDate = self::approximateDate('Y-m-01 00:00:00', static::filterDatePoint($year));
299
300 6
            return new self($startDate, $startDate->add(new DateInterval('P1M')));
301
        }
302
303 36
        $startDate = date_create_immutable($year.'-'.sprintf("%'.02d", self::validateRange($month, 1, 12)).'-01');
304
305 30
        return new self($startDate, $startDate->add(new DateInterval('P1M')));
306
    }
307
308
    /**
309
     * Create a Period object for a specific week
310
     *
311
     * @param DateTimeInterface|string|int $year
312
     * @param int                          $week index from 1 to 53
313
     *
314
     * @return self
315
     */
316 24
    public static function createFromWeek($year, int $week = null): self
317
    {
318 24
        if (1 == func_num_args()) {
319 3
            $date = static::filterDatePoint($year);
320 3
            $startDate = self::approximateDate(
321 3
                'Y-m-d 00:00:00',
322 3
                $date->sub(new DateInterval('P'.($date->format('N') - 1).'D'))
323
            );
324
325 3
            return new self($startDate, $startDate->add(new DateInterval('P1W')));
326
        }
327
328 24
        $startDate = date_create_immutable()
329 24
            ->setISODate($year, self::validateRange($week, 1, 53))
330 18
            ->setTime(0, 0, 0)
331
        ;
332
333 18
        return new self($startDate, $startDate->add(new DateInterval('P1W')));
334
    }
335
336
    /**
337
     * Create a Period object for a specific date
338
     *
339
     * The date is truncated so that the time range starts at midnight
340
     * according to the date timezone and last a full day.
341
     *
342
     * @param DateTimeInterface|string $datepoint
343
     *
344
     * @return self
345
     */
346 3
    public static function createFromDay($datepoint): self
347
    {
348 3
        $startDate = self::approximateDate('Y-m-d 00:00:00', static::filterDatePoint($datepoint));
349
350 3
        return new self($startDate, $startDate->add(new DateInterval('P1D')));
351
    }
352
353
    /**
354
     * Create a Period object for a specific date and hour.
355
     *
356
     * The starting datepoint represents the beginning of the hour
357
     * The Period interval is equal to 1 hour
358
     *
359
     * @param DateTimeInterface|string $datepoint
360
     *
361
     * @return self
362
     */
363 3
    public static function createFromHour($datepoint): self
364
    {
365 3
        $startDate = self::approximateDate('Y-m-d H:00:00', static::filterDatePoint($datepoint));
366
367 3
        return new self($startDate, $startDate->add(new DateInterval('PT1H')));
368
    }
369
370
    /**
371
     * Create a Period object for a specific date, hour and minute.
372
     *
373
     * The starting datepoint represents the beginning of the minute
374
     * The Period interval is equal to 1 minute
375
     *
376
     * @param DateTimeInterface|string $datepoint
377
     *
378
     * @return self
379
     */
380 3
    public static function createFromMinute($datepoint): self
381
    {
382 3
        $startDate = self::approximateDate('Y-m-d H:i:00', static::filterDatePoint($datepoint));
383
384 3
        return new self($startDate, $startDate->add(new DateInterval('PT1M')));
385
    }
386
387
    /**
388
     * Create a Period object for a specific date, hour, minute and second.
389
     *
390
     * The starting datepoint represents the beginning of the second
391
     * The Period interval is equal to 1 second
392
     *
393
     * @param DateTimeInterface|string $datepoint
394
     *
395
     * @return self
396
     */
397 3
    public static function createFromSecond($datepoint): self
398
    {
399 3
        $startDate = self::approximateDate('Y-m-d H:i:s', static::filterDatePoint($datepoint));
400
401 3
        return new self($startDate, $startDate->add(new DateInterval('PT1S')));
402
    }
403
404
    /**
405
     * Returns the Period starting datepoint.
406
     *
407
     * The starting datepoint is included in the specified period.
408
     * The starting datepoint is always less than or equal to the ending datepoint.
409
     *
410
     * @return DateTimeImmutable
411
     */
412 177
    public function getStartDate(): DateTimeImmutable
413
    {
414 177
        return $this->startDate;
415
    }
416
417
    /**
418
     * Returns the Period ending datepoint.
419
     *
420
     * The ending datepoint is excluded from the specified period.
421
     * The ending datepoint is always greater than or equal to the starting datepoint.
422
     *
423
     * @return DateTimeImmutable
424
     */
425 162
    public function getEndDate(): DateTimeImmutable
426
    {
427 162
        return $this->endDate;
428
    }
429
430
    /**
431
     * Returns the Period duration as expressed in seconds
432
     *
433
     * @return float
434
     */
435 18
    public function getTimestampInterval(): float
436
    {
437 18
        return $this->endDate->getTimestamp() - $this->startDate->getTimestamp();
438
    }
439
440
    /**
441
     * Returns the Period duration as a DateInterval object.
442
     *
443
     * @return DateInterval
444
     */
445 33
    public function getDateInterval(): DateInterval
446
    {
447 33
        return $this->startDate->diff($this->endDate);
448
    }
449
450
    /**
451
     * Allows iteration over a set of dates and times,
452
     * recurring at regular intervals, over the Period object.
453
     *
454
     * The interval can be
455
     * <ul>
456
     * <li>a DateInterval object</li>
457
     * <li>an int interpreted as the duration expressed in seconds.</li>
458
     * <li>a string in a format supported by DateInterval::createFromDateString</li>
459
     * </ul>
460
     *
461
     * @param DateInterval|int|string $interval The interval
462
     *
463
     * @param int $option can be set to DatePeriod::EXCLUDE_START_DATE
464
     *                    to exclude the start date from the set of
465
     *                    recurring dates within the period.
466
     *
467
     * @return DatePeriod
468
     */
469 24
    public function getDatePeriod($interval, int $option = 0): DatePeriod
470
    {
471 24
        return new DatePeriod($this->startDate, static::filterDateInterval($interval), $this->endDate, $option);
472
    }
473
474
    /**
475
     * Allows splitting a Period in smaller Period object according
476
     * to a given interval.
477
     *
478
     * The returned iterable Period set is ordered so that:
479
     * <ul>
480
     * <li>The first returned Period MUST share the starting datepoint of the parent object.</li>
481
     * <li>The last returned Period MUST share the ending datepoint of the parent object.</li>
482
     * <li>The last returned Period MUST have a duration equal or lesser than the submitted interval.</li>
483
     * <li>All returned Period except for the first one MUST start immediately after the previously returned Period</li>
484
     * </ul>
485
     *
486
     * The interval can be
487
     * <ul>
488
     * <li>a DateInterval object</li>
489
     * <li>an int interpreted as the duration expressed in seconds.</li>
490
     * <li>a string in a format supported by DateInterval::createFromDateString</li>
491
     * </ul>
492
     *
493
     * @param DateInterval|int|string $interval The interval
494
     *
495
     * @return Generator
496
     */
497 12
    public function split($interval): Generator
498
    {
499 12
        $startDate = $this->startDate;
500 12
        $interval = static::filterDateInterval($interval);
501
        do {
502 12
            $endDate = $startDate->add($interval);
503 12
            if ($endDate > $this->endDate) {
504 6
                $endDate = $this->endDate;
505
            }
506 12
            yield new self($startDate, $endDate);
507
508 9
            $startDate = $endDate;
509 9
        } while ($startDate < $this->endDate);
510 9
    }
511
512
    /**
513
     * Allows splitting a Period in smaller Period object according
514
     * to a given interval.
515
     *
516
     * The returned iterable Period set is ordered so that:
517
     * <ul>
518
     * <li>The first returned Period MUST share the ending datepoint of the parent object.</li>
519
     * <li>The last returned Period MUST share the starting datepoint of the parent object.</li>
520
     * <li>The last returned Period MUST have a duration equal or lesser than the submitted interval.</li>
521
     * <li>All returned Period except for the first one MUST end immediately before the previously returned Period</li>
522
     * </ul>
523
     *
524
     * The interval can be
525
     * <ul>
526
     * <li>a DateInterval object</li>
527
     * <li>an int interpreted as the duration expressed in seconds.</li>
528
     * <li>a string in a format supported by DateInterval::createFromDateString</li>
529
     * </ul>
530
     *
531
     * @param DateInterval|int|string $interval The interval
532
     *
533
     * @return Generator
534
     */
535 6
    public function splitBackwards($interval): Generator
536
    {
537 6
        $endDate = $this->endDate;
538 6
        $interval = static::filterDateInterval($interval);
539
        do {
540 6
            $startDate = $endDate->sub($interval);
541 6
            if ($startDate < $this->startDate) {
542 3
                $startDate = $this->startDate;
543
            }
544 6
            yield new self($startDate, $endDate);
545
546 6
            $endDate = $startDate;
547 6
        } while ($endDate > $this->startDate);
548 6
    }
549
550
    /**
551
     * Returns the string representation of a Period object
552
     * as a string in the ISO8601 interval format
553
     *
554
     * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
555
     *
556
     * @return string
557
     */
558 3
    public function __toString()
559
    {
560 3
        $period = $this->jsonSerialize();
561
562 3
        return $period['startDate'].'/'.$period['endDate'];
563
    }
564
565
    /**
566
     * Returns the Json representation of a Period object using
567
     * the JSON representation of dates as returned by Javascript Date.toJSON() method
568
     *
569
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toJSON
570
     *
571
     * @return string[]
572
     */
573 6
    public function jsonSerialize()
574
    {
575 6
        static $iso8601_format = 'Y-m-d\TH:i:s.u\Z';
576 6
        static $utc;
577 6
        $utc = $utc ?? new DateTimeZone('UTC');
578
579
        return [
580 6
            'startDate' => $this->startDate->setTimeZone($utc)->format($iso8601_format),
581 6
            'endDate' => $this->endDate->setTimeZone($utc)->format($iso8601_format),
582
        ];
583
    }
584
585
    /**
586
     * Compares two Period objects according to their duration.
587
     *
588
     * Returns:
589
     * <ul>
590
     * <li> -1 if the current Period duration is lesser than the submitted Period duration
591
     * <li> 1 if the current Period duration is greater than the submitted Period duration
592
     * <li> 0 if both Period have the same duration
593
     * </ul>
594
     *
595
     * @param Period $period
596
     *
597
     * @return int
598
     */
599 21
    public function compareDuration(Period $period): int
600
    {
601 21
        return $this->endDate <=> $this->startDate->add($period->getDateInterval());
602
    }
603
604
    /**
605
     * Tells whether the current Period object duration
606
     * is greater than the submitted one.
607
     *
608
     * @param Period $period
609
     *
610
     * @return bool
611
     */
612 9
    public function durationGreaterThan(Period $period): bool
613
    {
614 9
        return 1 == $this->compareDuration($period);
615
    }
616
617
    /**
618
     * Tells whether the current Period object duration
619
     * is less than the submitted one.
620
     *
621
     * @param Period $period
622
     *
623
     * @return bool
624
     */
625 9
    public function durationLessThan(Period $period): bool
626
    {
627 9
        return -1 == $this->compareDuration($period);
628
    }
629
630
    /**
631
     * Tells whether the current Period object duration
632
     * is equal to the submitted one
633
     *
634
     * @param Period $period
635
     *
636
     * @return bool
637
     */
638 3
    public function sameDurationAs(Period $period): bool
639
    {
640 3
        return 0 == $this->compareDuration($period);
641
    }
642
643
    /**
644
     * Tells whether two Period share the same datepoints.
645
     *
646
     * @param Period $period
647
     *
648
     * @return bool
649
     */
650 18
    public function sameValueAs(Period $period): bool
651
    {
652 18
        return $this->startDate == $period->getStartDate()
653 18
            && $this->endDate == $period->getEndDate();
654
    }
655
656
    /**
657
     * Tells whether two Period object abuts
658
     *
659
     * @param Period $period
660
     *
661
     * @return bool
662
     */
663 42
    public function abuts(Period $period): bool
664
    {
665 42
        return $this->startDate == $period->getEndDate()
666 42
            || $this->endDate == $period->getStartDate();
667
    }
668
669
    /**
670
     * Tells whether two Period objects overlaps
671
     *
672
     * @param Period $period
673
     *
674
     * @return bool
675
     */
676 36
    public function overlaps(Period $period): bool
677
    {
678 36
        return !$this->abuts($period)
679 36
            && $this->startDate < $period->getEndDate()
680 36
            && $this->endDate > $period->getStartDate();
681
    }
682
683
    /**
684
     * Tells whether a Period is entirely after the specified index
685
     *
686
     * The specified index can be
687
     * <ul>
688
     * <li>a Period</li>
689
     * <li>a DateTimeInterface</li>
690
     * <li>a string in a format supported by DateTimeImmutable::__construct</li>
691
     * </ul>
692
     *
693
     * @param Period|DateTimeInterface|string $index
694
     *
695
     * @return bool
696
     */
697 9
    public function isAfter($index): bool
698
    {
699 9
        if ($index instanceof Period) {
700 3
            return $this->startDate >= $index->getEndDate();
701
        }
702
703 6
        return $this->startDate > static::filterDatePoint($index);
704
    }
705
706
    /**
707
     * Tells whether a Period is entirely before the specified index
708
     *
709
     * The specified index can be
710
     * <ul>
711
     * <li>a Period</li>
712
     * <li>a DateTimeInterface</li>
713
     * <li>a string in a format supported by DateTimeImmutable::__construct</li>
714
     * </ul>
715
     *
716
     * @param Period|DateTimeInterface|string $index
717
     *
718
     * @return bool
719
     */
720 12
    public function isBefore($index): bool
721
    {
722 12
        if ($index instanceof Period) {
723 6
            return $this->endDate <= $index->getStartDate();
724
        }
725
726 6
        return $this->endDate <= static::filterDatePoint($index);
727
    }
728
729
    /**
730
     * Tells whether the specified index is fully contained within
731
     * the current Period object.
732
     *
733
     * The specified index can be
734
     * <ul>
735
     * <li>a Period</li>
736
     * <li>a DateTimeInterface</li>
737
     * <li>a string in a format supported by DateTimeImmutable::__construct</li>
738
     * </ul>
739
     *
740
     * @param Period|DateTimeInterface|string $index
741
     *
742
     * @return bool
743
     */
744 24
    public function contains($index): bool
745
    {
746 24
        if ($index instanceof Period) {
747 9
            return $this->containsPeriod($index);
748
        }
749
750 24
        return $this->containsDatePoint($index);
751
    }
752
753
    /**
754
     * Tells whether a Period object is fully contained within
755
     * the current Period object.
756
     *
757
     * @param Period $period
758
     *
759
     * @return bool
760
     */
761 9
    protected function containsPeriod(Period $period): bool
762
    {
763 9
        return $this->contains($period->getStartDate())
764 9
            && ($period->getEndDate() >= $this->startDate && $period->getEndDate() <= $this->endDate);
765
    }
766
767
    /**
768
     * Tells whether a datepoint is fully contained within
769
     * the current Period object.
770
     *
771
     * @param DateTimeInterface|string $datepoint
772
     *
773
     * @return bool
774
     */
775 24
    protected function containsDatePoint($datepoint): bool
776
    {
777 24
        $datetime = static::filterDatePoint($datepoint);
778
779 24
        return ($datetime >= $this->startDate && $datetime < $this->endDate)
780 24
            || ($datetime == $this->startDate && $datetime == $this->endDate);
781
    }
782
783
    /**
784
     * Returns a new Period object with a new included starting date point.
785
     *
786
     * @param DateTimeInterface|string $startDate date point
787
     *
788
     * @return self
789
     */
790 9
    public function startingOn($startDate): self
791
    {
792 9
        return new self(static::filterDatePoint($startDate), $this->endDate);
793
    }
794
795
    /**
796
     * Returns a new Period object with a new ending date point.
797
     *
798
     * @param DateTimeInterface|string $endDate date point
799
     *
800
     * @return self
801
     */
802 12
    public function endingOn($endDate): self
803
    {
804 12
        return new self($this->startDate, static::filterDatePoint($endDate));
805
    }
806
807
    /**
808
     * Returns a new Period object with a new ending date point.
809
     *
810
     * The interval can be
811
     * <ul>
812
     * <li>a DateInterval object</li>
813
     * <li>an int interpreted as the duration expressed in seconds.</li>
814
     * <li>a string in a format supported by DateInterval::createFromDateString</li>
815
     * </ul>
816
     *
817
     * @param DateInterval|int|string $interval The interval
818
     *
819
     * @return self
820
     */
821 6
    public function withDuration($interval): self
822
    {
823 6
        return new self($this->startDate, $this->startDate->add(static::filterDateInterval($interval)));
824
    }
825
826
    /**
827
     * Returns a new Period object with a new starting date point.
828
     *
829
     * The interval can be
830
     * <ul>
831
     * <li>a DateInterval object</li>
832
     * <li>an int interpreted as the duration expressed in seconds.</li>
833
     * <li>a string in a format supported by DateInterval::createFromDateString</li>
834
     * </ul>
835
     *
836
     * @param DateInterval|int|string $interval The interval
837
     *
838
     * @return self
839
     */
840 6
    public function withDurationBeforeEnd($interval): self
841
    {
842 6
        return new self($this->endDate->sub(static::filterDateInterval($interval)), $this->endDate);
843
    }
844
845
    /**
846
     * Returns a new Period object with a new starting date point
847
     * moved forward or backward by the given interval
848
     *
849
     * The interval can be
850
     * <ul>
851
     * <li>a DateInterval object</li>
852
     * <li>an int interpreted as the duration expressed in seconds.</li>
853
     * <li>a string in a format supported by DateInterval::createFromDateString</li>
854
     * </ul>
855
     *
856
     * @param DateInterval|int|string $interval The interval
857
     *
858
     * @return self
859
     */
860 9
    public function moveStartDate($interval): self
861
    {
862 9
        return new self($this->startDate->add(static::filterDateInterval($interval)), $this->endDate);
863
    }
864
865
    /**
866
     * Returns a new Period object with a new ending date point
867
     * moved forward or backward by the given interval
868
     *
869
     * The interval can be
870
     * <ul>
871
     * <li>a DateInterval object</li>
872
     * <li>an int interpreted as the duration expressed in seconds.</li>
873
     * <li>a string in a format supported by DateInterval::createFromDateString</li>
874
     * </ul>
875
     *
876
     * @param DateInterval|int|string $interval The interval
877
     *
878
     * @return self
879
     */
880 12
    public function moveEndDate($interval): self
881
    {
882 12
        return new self($this->startDate, $this->endDate->add(static::filterDateInterval($interval)));
883
    }
884
885
    /**
886
     * Returns a new Period object where the datepoints
887
     * are moved forwards or backward simultaneously by the given DateInterval
888
     *
889
     * The interval can be
890
     * <ul>
891
     * <li>a DateInterval object</li>
892
     * <li>an int interpreted as the duration expressed in seconds.</li>
893
     * <li>a string in a format supported by DateInterval::createFromDateString</li>
894
     * </ul>
895
     *
896
     * @param DateInterval|int|string $interval The interval
897
     *
898
     * @return self
899
     */
900 12
    public function move($interval): self
901
    {
902 12
        $interval = static::filterDateInterval($interval);
903
904 12
        return new self($this->startDate->add($interval), $this->endDate->add($interval));
905
    }
906
907
    /**
908
     * Merges one or more Period objects to return a new Period object.
909
     *
910
     * The resultant object represents the largest duration possible.
911
     *
912
     * @param Period... $periods one or more Period objects
0 ignored issues
show
Documentation introduced by
The doc-type Period... could not be parsed: Unknown type name "Period..." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
913
     *
914
     * @return self
915
     */
916 6
    public function merge(Period ...$periods): self
917
    {
918 6
        return array_reduce($periods, [$this, 'reducer'], $this);
919
    }
920
921
    /**
922
923
     * Returns a Period whose endpoints are the larget possible
924
     * between 2 instance of Period objects
925
     *
926
     * @param Period $carry
927
     * @param Period $period
928
     *
929
     * @return self
930
     */
931 6
    protected function reducer(Period $carry, Period $period): self
932
    {
933 6
        if ($carry->getStartDate() > $period->getStartDate()) {
934 3
            $carry = $carry->startingOn($period->getStartDate());
935
        }
936
937 6
        if ($carry->getEndDate() < $period->getEndDate()) {
938 6
            $carry = $carry->endingOn($period->getEndDate());
939
        }
940
941 6
        return $carry;
942
    }
943
944
    /**
945
     * Computes the intersection between two Period objects.
946
     *
947
     * @param Period $period
948
     *
949
     * @throws Exception If Both objects do not overlaps
950
     *
951
     * @return self
952
     */
953 6
    public function intersect(Period $period): self
954
    {
955 6
        if (!$this->overlaps($period)) {
956 3
            throw new Exception('Both object should at least overlaps');
957
        }
958
959 3
        return new self(
960 3
            ($period->getStartDate() > $this->startDate) ? $period->getStartDate() : $this->startDate,
961 3
            ($period->getEndDate() < $this->endDate) ? $period->getEndDate() : $this->endDate
962
        );
963
    }
964
965
    /**
966
     * Computes the gap between two Period objects.
967
     *
968
     * @param Period $period
969
     *
970
     * @return self
971
     */
972 15
    public function gap(Period $period): self
973
    {
974 15
        if ($period->getStartDate() > $this->startDate) {
975 12
            return new self($this->endDate, $period->getStartDate());
976
        }
977
978 6
        return new self($period->getEndDate(), $this->startDate);
979
    }
980
981
    /**
982
     * Returns the difference between two Period objects expressed in seconds
983
     *
984
     * @param Period $period
985
     *
986
     * @return float
987
     */
988 3
    public function timestampIntervalDiff(Period $period): float
989
    {
990 3
        return $this->getTimestampInterval() - $period->getTimestampInterval();
991
    }
992
993
    /**
994
     * Returns the difference between two Period objects expressed in DateInterval
995
     *
996
     * @param Period $period
997
     *
998
     * @return DateInterval
999
     */
1000 6
    public function dateIntervalDiff(Period $period): DateInterval
1001
    {
1002 6
        return $this->endDate->diff($this->startDate->add($period->getDateInterval()));
1003
    }
1004
1005
    /**
1006
     * Computes the difference between two overlapsing Period objects
1007
     *
1008
     * Returns an array containing the difference expressed as Period objects
1009
     * The array will:
1010
     *
1011
     * <ul>
1012
     * <li>be empty if both objects have the same datepoints</li>
1013
     * <li>contain one Period object if both objects share one datepoint</li>
1014
     * <li>contain two Period objects if both objects share no datepoint</li>
1015
     * </ul>
1016
     *
1017
     * @param Period $period
1018
     *
1019
     * @throws Exception if both object do not overlaps
1020
     *
1021
     * @return Period[]
1022
     */
1023 12
    public function diff(Period $period): array
1024
    {
1025 12
        if (!$this->overlaps($period)) {
1026 3
            throw new Exception('Both Period objects must overlaps');
1027
        }
1028
1029
        $res = [
1030 9
            static::createFromDatepoints($this->startDate, $period->getStartDate()),
1031 9
            static::createFromDatepoints($this->endDate, $period->getEndDate()),
1032
        ];
1033
1034 9
        $filter = function (Period $period) {
1035 9
            return $period->getStartDate() != $period->getEndDate();
1036 9
        };
1037
1038 9
        return array_values(array_filter($res, $filter));
1039
    }
1040
1041
    /**
1042
     * Create a new instance given two datepoints
1043
     *
1044
     * The datepoints will be used as to allow the creation of
1045
     * a Period object
1046
     *
1047
     * @param DateTimeInterface|string $datePoint1 datepoint
1048
     * @param DateTimeInterface|string $datePoint2 datepoint
1049
     *
1050
     * @return Period
1051
     */
1052 9
    protected static function createFromDatepoints($datePoint1, $datePoint2): self
1053
    {
1054 9
        $startDate = static::filterDatePoint($datePoint1);
1055 9
        $endDate = static::filterDatePoint($datePoint2);
1056 9
        if ($startDate > $endDate) {
1057 3
            return new self($endDate, $startDate);
1058
        }
1059
1060 6
        return new self($startDate, $endDate);
1061
    }
1062
}
1063