Completed
Push — master ( ae2747...054d9b )
by ignace nyamagana
30:30 queued 07:37
created

Period::containsPeriod()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 7
rs 9.4285
cc 3
eloc 4
nc 3
nop 1
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   3.1.0
10
 * @link      https://github.com/thephpleague/period/
11
 */
12
namespace League\Period;
13
14
use DateInterval;
15
use DatePeriod;
16
use DateTime;
17
use DateTimeImmutable;
18
use DateTimeInterface;
19
use DateTimeZone;
20
use Generator;
21
use InvalidArgumentException;
22
use JsonSerializable;
23
use LogicException;
24
use OutOfRangeException;
25
26
/**
27
 * A immutable value object class to manipulate Time Range.
28
 *
29
 * @package League.period
30
 * @author  Ignace Nyamagana Butera <[email protected]>
31
 * @since   1.0.0
32
 */
33
class Period implements JsonSerializable
34
{
35
    /**
36
     * DateTime Format to create ISO8601 Interval format
37
     */
38
    const DATE_ISO8601 = 'Y-m-d\TH:i:s\Z';
39
40
    /**
41
     * Date Format for timezoneless DateTimeInterface
42
     */
43
    const DATE_LOCALE = 'Y-m-d H:i:s';
44
45
    /**
46
     * Period starting included date point.
47
     *
48
     * @var DateTimeImmutable
49
     */
50
    protected $startDate;
51
52
    /**
53
     * Period ending excluded date point.
54
     *
55
     * @var DateTimeImmutable
56
     */
57
    protected $endDate;
58
59
    /**
60
     * Create a new instance.
61
     *
62
     * @param DateTimeImmutable|DateTime|string $startDate starting date point
63
     * @param DateTimeImmutable|DateTime|string $endDate   ending date point
64
     *
65
     * @throws LogicException If $startDate is greater than $endDate
66
     */
67
    public function __construct($startDate, $endDate)
68
    {
69
        $startDate = static::filterDatePoint($startDate);
70
        $endDate   = static::filterDatePoint($endDate);
71
        if ($startDate > $endDate) {
72
            throw new LogicException('the ending endpoint must be greater or equal to the starting endpoint');
73
        }
74
        $this->startDate = $startDate;
75
        $this->endDate   = $endDate;
76
    }
77
78
    /**
79
     * Validate a DateTime.
80
     *
81
     * @param DateTimeInterface|string $datetime
82
     *
83
     * @return DateTimeImmutable
84
     */
85
    protected static function filterDatePoint($datetime)
86
    {
87
        if ($datetime instanceof DateTimeImmutable) {
88
            return $datetime;
89
        }
90
91
        if ($datetime instanceof DateTime) {
92
            return new DateTimeImmutable($datetime->format(static::DATE_LOCALE), $datetime->getTimeZone());
93
        }
94
95
        return new DateTimeImmutable($datetime);
96
    }
97
98
    /**
99
     * Create a Period object for a specific day
100
     *
101
     * @param DateTimeInterface|string $day
102
     *
103
     * @return static
104
     */
105
    public static function createFromDay($day)
106
    {
107
        $startDate = static::filterDatePoint($day)->setTime(0, 0, 0);
108
109
        return new static($startDate, $startDate->add(new DateInterval('P1D')));
110
    }
111
112
    /**
113
     * Create a Period object from a starting point and an interval.
114
     *
115
     * @param DateTimeInterface|string $startDate The start date point
116
     * @param DateInterval|int|string  $interval  The duration. If an int is passed, it is
117
     *                                            interpreted as the duration expressed in seconds.
118
     *                                            If a string is passed, it must be a format
119
     *                                            supported by `DateInterval::createFromDateString`
120
     *
121
     * @return static
122
     */
123
    public static function createFromDuration($startDate, $interval)
124
    {
125
        $startDate = static::filterDatePoint($startDate);
126
127
        return new static($startDate, $startDate->add(static::filterDateInterval($interval)));
128
    }
129
130
    /**
131
     * Validate a DateInterval.
132
     *
133
     * @param DateInterval|int|string $interval The duration. If an int is passed, it is
134
     *                                          interpreted as the duration expressed in seconds.
135
     *                                          If a string is passed, it must be a format
136
     *                                          supported by `DateInterval::createFromDateString`
137
     *
138
     * @return DateInterval
139
     */
140
    protected static function filterDateInterval($interval)
141
    {
142
        if ($interval instanceof DateInterval) {
143
            return $interval;
144
        }
145
146
        if (false !== ($res = filter_var($interval, FILTER_VALIDATE_INT))) {
147
            return new DateInterval('PT'.$res.'S');
148
        }
149
150
        return DateInterval::createFromDateString($interval);
151
    }
152
153
    /**
154
     * Create a Period object from a ending endpoint and an interval.
155
     *
156
     * @param DateTimeInterface|string $endDate  The start date point
157
     * @param DateInterval|int|string  $interval The duration. If an int is passed, it is
158
     *                                           interpreted as the duration expressed in seconds.
159
     *                                           If a string is passed, it must be a format
160
     *                                           supported by `DateInterval::createFromDateString`
161
     *
162
     * @return static
163
     */
164
    public static function createFromDurationBeforeEnd($endDate, $interval)
165
    {
166
        $endDate = static::filterDatePoint($endDate);
167
168
        return new static($endDate->sub(static::filterDateInterval($interval)), $endDate);
169
    }
170
171
    /**
172
     * Create a Period object for a specific week
173
     *
174
     * @param int $year
175
     * @param int $week index from 1 to 53
176
     *
177
     * @return static
178
     */
179
    public static function createFromWeek($year, $week)
180
    {
181
        return static::createFromDuration(
182
            static::validateYear($year).'W'.sprintf('%02d', static::validateRange($week, 1, 53)),
183
            '1 WEEK'
184
        );
185
    }
186
187
    /**
188
     * Validate a year.
189
     *
190
     * @param int $year
191
     *
192
     * @throws InvalidArgumentException If year is not a valid int
193
     *
194
     * @return int
195
     */
196
    protected static function validateYear($year)
197
    {
198
        $year = filter_var($year, FILTER_VALIDATE_INT);
199
        if (false === $year) {
200
            throw new InvalidArgumentException('A Year must be a valid int');
201
        }
202
203
        return $year;
204
    }
205
206
    /**
207
     * Validate a int according to a range.
208
     *
209
     * @param int $value the value to validate
210
     * @param int $min   the minimum value
211
     * @param int $max   the maximal value
212
     *
213
     * @throws OutOfRangeException If the value is not in the range
214
     *
215
     * @return int
216
     */
217
    protected static function validateRange($value, $min, $max)
218
    {
219
        $res = filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => $min, 'max_range' => $max]]);
220
        if (false === $res) {
221
            throw new OutOfRangeException('the submitted value is not contained within the valid range');
222
        }
223
224
        return $res;
225
    }
226
227
    /**
228
     * Create a Period object for a specific month
229
     *
230
     * @param int $year
231
     * @param int $month Month index from 1 to 12
232
     *
233
     * @return static
234
     */
235
    public static function createFromMonth($year, $month)
236
    {
237
        return static::createFromYearInterval(1, $year, $month);
238
    }
239
240
    /**
241
     * Create a Period object for a specific interval in a given year
242
     *
243
     * @param int $duration
244
     * @param int $year
245
     * @param int $index
246
     *
247
     * @return static
248
     */
249
    protected static function createFromYearInterval($duration, $year, $index)
250
    {
251
        $month = sprintf('%02s', ((static::validateRange($index, 1, 12 / $duration) - 1) * $duration) + 1);
252
        $startDate = new DateTimeImmutable(static::validateYear($year).'-'.$month.'-01');
253
254
        return new static($startDate, $startDate->add(new DateInterval('P'.$duration.'M')));
255
    }
256
257
    /**
258
     * Create a Period object for a specific quarter
259
     *
260
     * @param int $year
261
     * @param int $quarter Quarter Index from 1 to 4
262
     *
263
     * @return static
264
     */
265
    public static function createFromQuarter($year, $quarter)
266
    {
267
        return static::createFromYearInterval(3, $year, $quarter);
268
    }
269
270
    /**
271
     * Create a Period object for a specific semester
272
     *
273
     * @param int $year
274
     * @param int $semester Semester Index from 1 to 2
275
     *
276
     * @return static
277
     */
278
    public static function createFromSemester($year, $semester)
279
    {
280
        return static::createFromYearInterval(6, $year, $semester);
281
    }
282
283
    /**
284
     * Create a Period object for a specific Year
285
     *
286
     * @param int $year
287
     *
288
     * @return static
289
     */
290
    public static function createFromYear($year)
291
    {
292
        $startDate = new DateTimeImmutable(static::validateYear($year).'-01-01');
293
294
        return new static($startDate, $startDate->add(new DateInterval('P1Y')));
295
    }
296
297
    /**
298
     * String representation of a Period using ISO8601 Time interval format
299
     *
300
     * @return string
301
     */
302
    public function __toString()
303
    {
304
        $utc = new DateTimeZone('UTC');
305
306
        return $this->startDate->setTimeZone($utc)->format(static::DATE_ISO8601)
307
            .'/'.$this->endDate->setTimeZone($utc)->format(static::DATE_ISO8601);
308
    }
309
310
    /**
311
     * implement JsonSerializable interface
312
     *
313
     * @return array
314
     */
315
    public function jsonSerialize()
316
    {
317
        return [
318
            'startDate' => new DateTime($this->startDate->format(static::DATE_LOCALE), $this->startDate->getTimeZone()),
319
            'endDate' => new DateTime($this->endDate->format(static::DATE_LOCALE), $this->endDate->getTimeZone()),
320
        ];
321
    }
322
323
    /**
324
     * Returns the starting date point.
325
     *
326
     * @return DateTimeImmutable
327
     */
328
    public function getStartDate()
329
    {
330
        return $this->startDate;
331
    }
332
333
    /**
334
     * Returns the ending endpoint.
335
     *
336
     * @return DateTimeImmutable
337
     */
338
    public function getEndDate()
339
    {
340
        return $this->endDate;
341
    }
342
343
    /**
344
     * Returns the Period duration as expressed in seconds
345
     *
346
     * @return int
347
     */
348
    public function getTimestampInterval()
349
    {
350
        return $this->endDate->getTimestamp() - $this->startDate->getTimestamp();
351
    }
352
353
    /**
354
     * Returns the Period duration as a DateInterval object.
355
     *
356
     * @return DateInterval
357
     */
358
    public function getDateInterval()
359
    {
360
        return $this->startDate->diff($this->endDate);
361
    }
362
363
    /**
364
     * Allows iteration over a set of dates and times,
365
     * recurring at regular intervals, over the Period object.
366
     *
367
     * @param DateInterval|int|string $interval The duration. If an int is passed, it is
368
     *                                          interpreted as the duration expressed in seconds.
369
     *                                          If a string is passed, it must be a format
370
     *                                          supported by `DateInterval::createFromDateString`
371
     *
372
     * @return DatePeriod
373
     */
374
    public function getDatePeriod($interval)
375
    {
376
        return new DatePeriod($this->startDate, static::filterDateInterval($interval), $this->endDate);
377
    }
378
379
    /**
380
     * Tells whether two Period share the same endpoints.
381
     *
382
     * @param Period $period
383
     *
384
     * @return bool
385
     */
386
    public function sameValueAs(Period $period)
387
    {
388
        return $this->startDate == $period->getStartDate() && $this->endDate == $period->getEndDate();
389
    }
390
391
    /**
392
     * Tells whether two Period object abuts
393
     *
394
     * @param Period $period
395
     *
396
     * @return bool
397
     */
398
    public function abuts(Period $period)
399
    {
400
        return $this->startDate == $period->getEndDate() || $this->endDate == $period->getStartDate();
401
    }
402
403
    /**
404
     * Tells whether two Period objects overlaps.
405
     *
406
     * @param Period $period
407
     *
408
     * @return bool
409
     */
410
    public function overlaps(Period $period)
411
    {
412
        if ($this->abuts($period)) {
413
            return false;
414
        }
415
416
        return $this->startDate < $period->getEndDate() && $this->endDate > $period->getStartDate();
417
    }
418
419
    /**
420
     * Tells whether a Period is entirely after the specified index
421
     *
422
     * @param Period|DateTimeInterface|string $index
423
     *
424
     * @return bool
425
     */
426
    public function isAfter($index)
427
    {
428
        if ($index instanceof Period) {
429
            return $this->startDate >= $index->getEndDate();
430
        }
431
432
        return $this->startDate > static::filterDatePoint($index);
433
    }
434
435
    /**
436
     * Tells whether a Period is entirely before the specified index
437
     *
438
     * @param Period|DateTimeInterface|string $index
439
     *
440
     * @return bool
441
     */
442
    public function isBefore($index)
443
    {
444
        if ($index instanceof Period) {
445
            return $this->endDate <= $index->getStartDate();
446
        }
447
448
        return $this->endDate <= static::filterDatePoint($index);
449
    }
450
451
    /**
452
     * Tells whether the specified index is fully contained within
453
     * the current Period object.
454
     *
455
     * @param Period|DateTimeInterface|string $index
456
     *
457
     * @return bool
458
     */
459
    public function contains($index)
460
    {
461
        if ($index instanceof Period) {
462
            return $this->containsPeriod($index);
463
        }
464
465
        $datetime = static::filterDatePoint($index);
466
467
        return $datetime >= $this->startDate && $datetime < $this->endDate;
468
    }
469
470
    /**
471
     * Tells whether a Period object is fully contained within
472
     * the current Period object.
473
     *
474
     * @param Period $period
475
     *
476
     * @return bool
477
     */
478
    protected function containsPeriod(Period $period)
479
    {
480
        $endDate = $period->getEndDate();
481
482
        return $this->contains($period->getStartDate())
483
            && ($endDate >= $this->startDate && $endDate <= $this->endDate);
484
    }
485
486
    /**
487
     * Compares two Period objects according to their duration.
488
     *
489
     * @param Period $period
490
     *
491
     * @return int
492
     */
493
    public function compareDuration(Period $period)
494
    {
495
        $datetime = $this->startDate->add($period->getDateInterval());
496
        if ($this->endDate > $datetime) {
497
            return 1;
498
        }
499
500
        if ($this->endDate < $datetime) {
501
            return -1;
502
        }
503
504
        return 0;
505
    }
506
507
    /**
508
     * Tells whether the current Period object duration
509
     * is greater than the submitted one.
510
     *
511
     * @param Period $period
512
     *
513
     * @return bool
514
     */
515
    public function durationGreaterThan(Period $period)
516
    {
517
        return 1 === $this->compareDuration($period);
518
    }
519
520
    /**
521
     * Tells whether the current Period object duration
522
     * is less than the submitted one.
523
     *
524
     * @param Period $period
525
     *
526
     * @return bool
527
     */
528
    public function durationLessThan(Period $period)
529
    {
530
        return -1 === $this->compareDuration($period);
531
    }
532
533
    /**
534
     * Tells whether the current Period object duration
535
     * is equal to the submitted one
536
     *
537
     * @param Period $period
538
     *
539
     * @return bool
540
     */
541
    public function sameDurationAs(Period $period)
542
    {
543
        return 0 === $this->compareDuration($period);
544
    }
545
546
    /**
547
     * Returns a new Period object with a new included starting date point.
548
     *
549
     * @param DateTimeInterface|string $startDate date point
550
     *
551
     * @return static
552
     */
553
    public function startingOn($startDate)
554
    {
555
        return new static(static::filterDatePoint($startDate), $this->endDate);
556
    }
557
558
    /**
559
     * Returns a new Period object with a new ending date point.
560
     *
561
     * @param DateTimeInterface|string $endDate date point
562
     *
563
     * @return static
564
     */
565
    public function endingOn($endDate)
566
    {
567
        return new static($this->startDate, static::filterDatePoint($endDate));
568
    }
569
570
    /**
571
     * Returns a new Period object with a new ending date point.
572
     *
573
     * @param DateInterval|int|string $interval The duration. If an int is passed, it is
574
     *                                          interpreted as the duration expressed in seconds.
575
     *                                          If a string is passed, it must be a format
576
     *                                          supported by `DateInterval::createFromDateString`
577
     *
578
     * @return static
579
     */
580
    public function withDuration($interval)
581
    {
582
        return static::createFromDuration($this->startDate, $interval);
583
    }
584
585
    /**
586
     * Returns a new Period object with an added interval
587
     *
588
     * @param DateInterval|int|string $interval The duration. If an int is passed, it is
589
     *                                          interpreted as the duration expressed in seconds.
590
     *                                          If a string is passed, it must be a format
591
     *                                          supported by `DateInterval::createFromDateString`
592
     *
593
     * @return static
594
     */
595
    public function add($interval)
596
    {
597
        return new static($this->startDate, $this->endDate->add(static::filterDateInterval($interval)));
598
    }
599
600
    /**
601
     * Returns a new Period object with a Removed interval
602
     *
603
     * @param DateInterval|int|string $interval The duration. If an int is passed, it is
604
     *                                          interpreted as the duration expressed in seconds.
605
     *                                          If a string is passed, it must be a format
606
     *                                          supported by `DateInterval::createFromDateString`
607
     *
608
     * @return static
609
     */
610
    public function sub($interval)
611
    {
612
        return new static($this->startDate, $this->endDate->sub(static::filterDateInterval($interval)));
613
    }
614
615
    /**
616
     * Returns a new Period object adjacent to the current Period
617
     * and starting with its ending endpoint.
618
     * If no duration is provided the new Period will be created
619
     * using the current object duration
620
     *
621
     * @param  DateInterval|int|string $interval The duration. If an int is passed, it is
622
     *                                           interpreted as the duration expressed in seconds.
623
     *                                           If a string is passed, it must be a format
624
     *                                           supported by `DateInterval::createFromDateString`
625
     * @return static
626
     */
627
    public function next($interval = null)
628
    {
629
        if (is_null($interval)) {
630
            $interval = $this->getDateInterval();
631
        }
632
633
        return static::createFromDuration($this->endDate, $interval);
634
    }
635
636
    /**
637
     * Returns a new Period object adjacent to the current Period
638
     * and ending with its starting endpoint.
639
     * If no duration is provided the new Period will have the
640
     * same duration as the current one
641
     *
642
     * @param  DateInterval|int|string $interval The duration. If an int is passed, it is
643
     *                                           interpreted as the duration expressed in seconds.
644
     *                                           If a string is passed, it must be a format
645
     *                                           supported by `DateInterval::createFromDateString`
646
     * @return static
647
     */
648
    public function previous($interval = null)
649
    {
650
        if (is_null($interval)) {
651
            $interval = $this->getDateInterval();
652
        }
653
654
        return static::createFromDurationBeforeEnd($this->startDate, $interval);
655
    }
656
657
    /**
658
     * Merges one or more Period objects to return a new Period object.
659
     *
660
     * The resultant object represents the largest duration possible.
661
     *
662
     * @param Period ...$arg one or more Period objects
663
     *
664
     * @return static
665
     */
666
    public function merge(Period $arg)
0 ignored issues
show
Unused Code introduced by
The parameter $arg is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
667
    {
668
        $reducer = function (Period $carry, Period $period) {
669
            if ($carry->getStartDate() > $period->getStartDate()) {
670
                $carry = $carry->startingOn($period->getStartDate());
671
            }
672
673
            if ($carry->getEndDate() < $period->getEndDate()) {
674
                $carry = $carry->endingOn($period->getEndDate());
675
            }
676
677
            return $carry;
678
        };
679
680
        return array_reduce(func_get_args(), $reducer, $this);
681
    }
682
683
    /**
684
     * Split a Period by a given interval
685
     *
686
     * @param  DateInterval|int|string $interval The duration. If an int is passed, it is
687
     *                                           interpreted as the duration expressed in seconds.
688
     *                                           If a string is passed, it must be a format
689
     *                                           supported by `DateInterval::createFromDateString`
690
     * @return Generator
691
     */
692
    public function split($interval)
693
    {
694
        $startDate = $this->startDate;
695
        $interval = static::filterDateInterval($interval);
696
        do {
697
            $endDate = $startDate->add($interval);
698
            if ($endDate > $this->endDate) {
699
                $endDate = $this->endDate;
700
            }
701
            yield new static($startDate, $endDate);
702
703
            $startDate = $endDate;
704
        } while ($startDate < $this->endDate);
705
    }
706
707
    /**
708
     * Computes the intersection between two Period objects.
709
     *
710
     * @param Period $period
711
     *
712
     * @throws LogicException If Both objects do not overlaps
713
     *
714
     * @return static
715
     */
716
    public function intersect(Period $period)
717
    {
718
        if ($this->abuts($period)) {
719
            throw new LogicException('Both object should not abuts');
720
        }
721
722
        return new static(
723
            ($period->getStartDate() > $this->startDate) ? $period->getStartDate() : $this->startDate,
724
            ($period->getEndDate() < $this->endDate) ? $period->getEndDate() : $this->endDate
725
        );
726
    }
727
728
    /**
729
     * Computes the gap between two Period objects.
730
     *
731
     * @param Period $period
732
     *
733
     * @return static
734
     */
735
    public function gap(Period $period)
736
    {
737
        if ($period->getStartDate() > $this->startDate) {
738
            return new static($this->endDate, $period->getStartDate());
739
        }
740
741
        return new static($period->getEndDate(), $this->startDate);
742
    }
743
744
    /**
745
     * Returns the difference between two Period objects expressed in seconds
746
     *
747
     * @param Period $period
748
     *
749
     * @return float
750
     */
751
    public function timestampIntervalDiff(Period $period)
752
    {
753
        return $this->getTimestampInterval() - $period->getTimestampInterval();
754
    }
755
756
    /**
757
     * Returns the difference between two Period objects expressed in \DateInterval
758
     *
759
     * @param Period $period
760
     *
761
     * @return DateInterval
762
     */
763
    public function dateIntervalDiff(Period $period)
764
    {
765
        return $this->endDate->diff($this->withDuration($period->getDateInterval())->endDate);
766
    }
767
768
    /**
769
     * Computes the difference between two Period objects which overlap
770
     * and return an array containing the difference expressed as Period objects
771
     * The array will:
772
     * - be empty if both objects have the same endpoints
773
     * - contain one Period object if both objects share one endpoint
774
     * - contain two Period objects if both objects share no endpoint
775
     *
776
     * @param Period $period
777
     *
778
     * @throws LogicException if both object do not overlaps
779
     *
780
     * @return Period[]
781
     */
782
    public function diff(Period $period)
783
    {
784
        if (! $this->overlaps($period)) {
785
            throw new LogicException('Both Period objects should overlaps');
786
        }
787
788
        $res = [
789
            static::createFromEndpoints($this->startDate, $period->getStartDate()),
790
            static::createFromEndpoints($this->endDate, $period->getEndDate()),
791
        ];
792
793
        return array_values(array_filter($res, function (Period $period) {
794
            return $period->getStartDate() != $period->getEndDate();
795
        }));
796
    }
797
798
    /**
799
     * Create a new Period instance given two endpoints
800
     * The endpoints will be used as to allow the creation of
801
     * a Period object
802
     *
803
     * @param DateTimeInterface|string $datePoint1 endpoint
804
     * @param DateTimeInterface|string $datePoint2 endpoint
805
     *
806
     * @return Period
807
     */
808
    protected static function createFromEndpoints($datePoint1, $datePoint2)
809
    {
810
        $startDate = static::filterDatePoint($datePoint1);
811
        $endDate   = static::filterDatePoint($datePoint2);
812
        if ($startDate > $endDate) {
813
            return new static($endDate, $startDate);
814
        }
815
816
        return new static($startDate, $endDate);
817
    }
818
}
819