Completed
Push — master ( 7cd69f...11c5ca )
by ignace nyamagana
480:02 queued 467:25
created

Period::createFromISOWeek()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
nc 3
nop 2
dl 0
loc 17
ccs 5
cts 5
cp 1
crap 5
rs 9.3888
c 0
b 0
f 0

1 Method

Rating   Name   Duplication   Size   Complexity  
A Period::isAfter() 0 8 2
1
<?php
2
3
/**
4
 * League.Period (https://period.thephpleague.com).
5
 *
6
 * @author  Ignace Nyamagana Butera <[email protected]>
7
 * @license https://github.com/thephpleague/period/blob/master/LICENSE (MIT License)
8
 * @version 4.0.0
9
 * @link    https://github.com/thephpleague/period
10
 *
11
 * For the full copyright and license information, please view the LICENSE
12
 * file that was distributed with this source code.
13
 */
14
15
declare(strict_types=1);
16
17
namespace League\Period;
18
19
use DateInterval;
20
use DatePeriod;
21
use DateTimeImmutable;
22
use DateTimeInterface;
23
use DateTimeZone;
24
use JsonSerializable;
25
use function array_unshift;
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
    /**
40
     * Period starting included datepoint.
41
     *
42
     * @var DateTimeImmutable
43
     */
44
    private $startDate;
45
46
    /**
47
     * Period ending excluded datepoint.
48
     *
49
     * @var DateTimeImmutable
50
     */
51 3
    private $endDate;
52
53 3
    /**
54
     * Creates new instance from a DatePeriod.
55
     *
56
     * @throws Exception If the submitted DatePeriod lacks an end datepoint.
57
     *                   This is possible if the DatePeriod was created using
58
     *                   recurrences instead of a end datepoint.
59
     *                   https://secure.php.net/manual/en/dateperiod.getenddate.php
60
     */
61
    public static function fromDatePeriod(DatePeriod $datePeriod): self
62
    {
63
        $endDate = $datePeriod->getEndDate();
64 255
        if ($endDate instanceof DateTimeInterface) {
65
            return new self($datePeriod->getStartDate(), $endDate);
66 255
        }
67 255
68 255
        throw new Exception('The submitted DatePeriod object does not contain an end datepoint');
69 39
    }
70
71 246
    /**
72 246
     * @inheritdoc
73 246
     */
74
    public static function __set_state(array $period)
75
    {
76
        return new self($period['startDate'], $period['endDate']);
77
    }
78
79
    /**
80
     * Creates a new instance.
81
     *
82 282
     * @param mixed $startDate the interval start datepoint
83
     * @param mixed $endDate   the interval end datepoint
84 282
     *
85 246
     * @throws Exception If $startDate is greater than $endDate
86
     */
87
    public function __construct($startDate, $endDate)
88 222
    {
89 78
        $startDate = datepoint($startDate);
90
        $endDate = datepoint($endDate);
91
        if ($startDate > $endDate) {
92 153
            throw new Exception('The ending datepoint must be greater or equal to the starting datepoint');
93
        }
94
        $this->startDate = $startDate;
95
        $this->endDate = $endDate;
96
    }
97
98
    /**
99
     * Returns the Interval starting datepoint.
100
     *
101
     * The starting datepoint is included in the specified period.
102
     * The starting datepoint is always less than or equal to the ending datepoint.
103
     */
104 6
    public function getStartDate(): DateTimeImmutable
105
    {
106 6
        return $this->startDate;
107 3
    }
108
109
    /**
110 3
     * Returns the Interval ending datepoint.
111
     *
112
     * The ending datepoint is excluded from the specified period.
113
     * The ending datepoint is always greater than or equal to the starting datepoint.
114
     */
115
    public function getEndDate(): DateTimeImmutable
116
    {
117
        return $this->endDate;
118
    }
119
120
    /**
121
     * Returns the Interval duration as expressed in seconds.
122
     */
123
    public function getTimestampInterval(): float
124
    {
125
        return $this->endDate->getTimestamp() - $this->startDate->getTimestamp();
126
    }
127
128 138
    /**
129
     * Returns the Interval duration as a DateInterval object.
130 138
     */
131
    public function getDateInterval(): DateInterval
132 138
    {
133
        return $this->startDate->diff($this->endDate);
134
    }
135
136
    /**
137
     * Allows iteration over a set of dates and times,
138
     * recurring at regular intervals, over the instance.
139
     *
140
     * This method is not part of the Interval.
141
     *
142
     * @see http://php.net/manual/en/dateperiod.construct.php
143
     */
144
    public function getDatePeriod($duration, int $option = 0): DatePeriod
145
    {
146
        return new DatePeriod($this->startDate, duration($duration), $this->endDate, $option);
147
    }
148
149 177
    /**
150
     * Returns the string representation as a ISO8601 interval format.
151 177
     *
152 27
     * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
153
     *
154
     * @return string
155 165
     */
156 30
    public function __toString()
157
    {
158
        $period = $this->jsonSerialize();
159 153
160
        return $period['startDate'].'/'.$period['endDate'];
161
    }
162
163
    /**
164
     * Returns the Json representation of an instance using
165
     * the JSON representation of dates as returned by Javascript Date.toJSON() method.
166
     *
167
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toJSON
168
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
169
     *
170
     * @return string[]
171
     */
172
    public function jsonSerialize()
173
    {
174
        static $utc;
175
        $utc = $utc ?? new DateTimeZone('UTC');
176
177 24
        return [
178
            'startDate' => $this->startDate->setTimezone($utc)->format(self::ISO8601_FORMAT),
179 24
            'endDate' => $this->endDate->setTimezone($utc)->format(self::ISO8601_FORMAT),
180
        ];
181 24
    }
182
183
    /**
184
     * Compares two instances according to their duration.
185
     *
186
     * Returns:
187
     * <ul>
188
     * <li> -1 if the current Interval is lesser than the submitted Interval object</li>
189
     * <li>  1 if the current Interval is greater than the submitted Interval object</li>
190
     * <li>  0 if both Interval objects have the same duration</li>
191 18
     * </ul>
192
     */
193 18
    public function durationCompare(Period $interval): int
194 15
    {
195
        return $this->endDate <=> $this->startDate->add($interval->getDateInterval());
196 15
    }
197
198
    /**
199 6
     * Tells whether the current instance duration is equal to the submitted one.
200
     */
201 6
    public function durationEquals(Period $interval): bool
202
    {
203
        return 0 === $this->durationCompare($interval);
204
    }
205
206
    /**
207
     * Tells whether the current instance duration is greater than the submitted one.
208
     */
209
    public function durationGreaterThan(Period $interval): bool
210
    {
211
        return 1 === $this->durationCompare($interval);
212
    }
213 21
214
    /**
215 21
     * Tells whether the current instance duration is less than the submitted one.
216
     */
217
    public function durationLessThan(Period $interval): bool
218
    {
219
        return -1 === $this->durationCompare($interval);
220
    }
221
222
    /**
223
     * Tells whether two Interval share the same datepoints.
224
     */
225
    public function equals(Period $interval): bool
226 12
    {
227
        return $this->startDate == $interval->getStartDate()
228 12
            && $this->endDate == $interval->getEndDate();
229 3
    }
230 3
231 3
    /**
232
     * Tells whether two Interval abuts.
233 3
     */
234
    public function abuts(Period $interval): bool
235
    {
236 12
        return $this->startDate == $interval->getEndDate()
237 6
            || $this->endDate == $interval->getStartDate();
238
    }
239 6
240
    /**
241
     * Tells whether two Interval overlaps.
242
     */
243
    public function overlaps(Period $interval): bool
244
    {
245
        return $this->startDate < $interval->getEndDate()
246
            && $this->endDate > $interval->getStartDate();
247
    }
248
249
    /**
250
     * Tells whether a Interval is entirely after the specified index.
251
     * The index can be a DateTimeInterface object or another Interval object.
252
     */
253 75
    public function isAfter($index): bool
254
    {
255 75
        if ($index instanceof Period) {
256 75
            return $this->startDate >= $index->getEndDate();
257 51
        }
258
259
        return $this->startDate > datepoint($index);
260 24
    }
261
262
    /**
263
     * Tells whether a Interval is entirely before the specified index.
264
     * The index can be a DateTimeInterface object or another Interval object.
265
     */
266
    public function isBefore($index): bool
267
    {
268
        if ($index instanceof Period) {
269
            return $this->endDate <= $index->getStartDate();
270
        }
271 12
272
        return $this->endDate <= datepoint($index);
273 12
    }
274 3
275 3
    /**
276 3
     * Tells whether the specified index is fully contained within
277
     * the current Period object.
278 3
     */
279
    public function contains($index): bool
280
    {
281 12
        if ($index instanceof Period) {
282 6
            return $this->containsPeriod($index);
283
        }
284 6
285
        return $this->containsDatePoint(datepoint($index));
286
    }
287
288
    /**
289
     * Tells whether the a Interval is fully contained within the current instance.
290
     */
291
    private function containsPeriod(Period $interval): bool
292
    {
293
        return $this->containsDatePoint($interval->getStartDate())
294
            && ($interval->getEndDate() >= $this->startDate && $interval->getEndDate() <= $this->endDate);
295 39
    }
296
297 39
    /**
298 6
     * Tells whether a datepoint is fully contained within the current instance.
299
     */
300 6
    private function containsDatePoint(DateTimeInterface $datepoint): bool
301
    {
302
        return ($datepoint >= $this->startDate && $datepoint < $this->endDate)
303 36
            || ($datepoint == $this->startDate && $datepoint == $this->endDate);
304
    }
305 30
306
    /**
307
     * Allows splitting an instance in smaller Period objects according to a given interval.
308
     *
309
     * The returned iterable Interval set is ordered so that:
310
     * <ul>
311
     * <li>The first returned object MUST share the starting datepoint of the parent object.</li>
312
     * <li>The last returned object MUST share the ending datepoint of the parent object.</li>
313
     * <li>The last returned object MUST have a duration equal or lesser than the submitted interval.</li>
314
     * <li>All returned objects except for the first one MUST start immediately after the previously returned object</li>
315
     * </ul>
316 24
     *
317
     * @param DateInterval|Period|string|int $duration
318 24
     *
319 3
     * @return Period[]
320 3
     */
321 3
    public function split($duration): iterable
322 3
    {
323
        $startDate = $this->startDate;
324
        $duration = duration($duration);
325 3
        do {
326
            $endDate = $startDate->add($duration);
327
            if ($endDate > $this->endDate) {
328 24
                $endDate = $this->endDate;
329 24
            }
330 18
            yield new self($startDate, $endDate);
331
332
            $startDate = $endDate;
333 18
        } while ($startDate < $this->endDate);
334
    }
335
336
    /**
337
     * Allows splitting an instance in smaller Period objects according to a given interval.
338
     *
339
     * The returned iterable Period set is ordered so that:
340
     * <ul>
341
     * <li>The first returned object MUST share the ending datepoint of the parent object.</li>
342
     * <li>The last returned object MUST share the starting datepoint of the parent object.</li>
343
     * <li>The last returned object MUST have a duration equal or lesser than the submitted interval.</li>
344
     * <li>All returned objects except for the first one MUST end immediately before the previously returned object</li>
345
     * </ul>
346 3
     *
347
     * @param DateInterval|Period|string|int $duration
348 3
     *
349
     * @return Period[]
350 3
     */
351
    public function splitBackwards($duration): iterable
352
    {
353
        $endDate = $this->endDate;
354
        $duration = duration($duration);
355
        do {
356
            $startDate = $endDate->sub($duration);
357
            if ($startDate < $this->startDate) {
358
                $startDate = $this->startDate;
359
            }
360
            yield new self($startDate, $endDate);
361
362
            $endDate = $startDate;
363 3
        } while ($endDate > $this->startDate);
364
    }
365 3
366
    /**
367 3
     * Computes the intersection between two instances.
368
     *
369
     * @throws Exception If both objects do not overlaps
370
     */
371
    public function intersect(Period $interval): self
372
    {
373
        if (!$this->overlaps($interval)) {
374
            throw new Exception(sprintf('Both %s objects should overlaps', Period::class));
375
        }
376
377
        return new self(
378
            ($interval->getStartDate() > $this->startDate) ? $interval->getStartDate() : $this->startDate,
379
            ($interval->getEndDate() < $this->endDate) ? $interval->getEndDate() : $this->endDate
380 3
        );
381
    }
382 3
383
    /**
384 3
     * Computes the gap between two instances.
385
     *
386
     * @throws Exception If both objects overlaps
387
     */
388
    public function gap(Period $interval): self
389
    {
390
        if ($this->overlaps($interval)) {
391
            throw new Exception(sprintf('Both %s objects should not overlaps', Period::class));
392
        }
393
394
        if ($interval->getStartDate() > $this->startDate) {
395
            return new self($this->endDate, $interval->getStartDate());
396
        }
397 3
398
        return new self($interval->getEndDate(), $this->startDate);
399 3
    }
400
401 3
    /**
402
     * Computes the difference between two overlapsing instances.
403
     *
404
     * This method is not part of the Interval.
405
     *
406
     * Returns an array containing the difference expressed as Period objects
407
     * The array will always contains 2 elements:
408
     *
409
     * <ul>
410
     * <li>an NULL filled array if both objects have the same datepoints</li>
411
     * <li>one Period object and NULL if both objects share one datepoint</li>
412 177
     * <li>two Period objects if both objects share no datepoint</li>
413
     * </ul>
414 177
     *
415
     * @throws Exception if both objects do not overlaps
416
     */
417
    public function diff(Period $interval): array
418
    {
419
        if ($interval->equals($this)) {
420
            return [null, null];
421
        }
422
423
        $intersect = $this->intersect($interval);
424
        $merge = $this->merge($interval);
425 162
        if ($merge->getStartDate() == $intersect->getStartDate()) {
426
            return [$merge->startingOn($intersect->getEndDate()), null];
427 162
        }
428
429
        if ($merge->getEndDate() == $intersect->getEndDate()) {
430
            return [$merge->endingOn($intersect->getStartDate()), null];
431
        }
432
433
        return [
434
            $merge->endingOn($intersect->getStartDate()),
435 18
            $merge->startingOn($intersect->getEndDate()),
436
        ];
437 18
    }
438
439
    /**
440
     * Returns an instance with the specified starting datepoint.
441
     *
442
     * This method MUST retain the state of the current instance, and return
443
     * an instance that contains the specified starting datepoint.
444
     *
445 33
     * @param DateTimeInterface|int|string $datepoint
446
     */
447 33
    public function startingOn($datepoint): self
448
    {
449
        $startDate = datepoint($datepoint);
450
        if ($startDate == $this->startDate) {
451
            return $this;
452
        }
453
454
        return new self($startDate, $this->endDate);
455
    }
456
457
    /**
458
     * Returns an instance with the specified ending datepoint.
459
     *
460
     * This method MUST retain the state of the current instance, and return
461
     * an instance that contains the specified ending datepoint.
462
     *
463
     * @param DateTimeInterface|int|string $datepoint
464
     */
465
    public function endingOn($datepoint): self
466
    {
467
        $endDate = datepoint($datepoint);
468
        if ($endDate == $this->endDate) {
469 24
            return $this;
470
        }
471 24
472
        return new self($this->startDate, $endDate);
473
    }
474
475
    /**
476
     * Returns a new instance with a new ending datepoint.
477
     *
478
     * @param DateInterval|Period|int|string $duration
479
     */
480
    public function withDurationAfterStart($duration): self
481
    {
482
        return $this->endingOn($this->startDate->add(duration($duration)));
483
    }
484
485
    /**
486
     * Returns a new instance with a new starting datepoint.
487
     *
488
     * @param DateInterval|Period|int|string $duration
489
     */
490
    public function withDurationBeforeEnd($duration): self
491
    {
492
        return $this->startingOn($this->endDate->sub(duration($duration)));
493
    }
494
495
    /**
496
     * Returns a new instance with a new starting datepoint
497 12
     * moved forward or backward by the given interval.
498
     *
499 12
     * @param DateInterval|Period|int|string $duration
500 12
     */
501
    public function moveStartDate($duration): self
502 12
    {
503 12
        return $this->startingOn($this->startDate->add(duration($duration)));
504 6
    }
505
506 12
    /**
507
     * Returns a new instance with a new ending datepoint
508 9
     * moved forward or backward by the given interval.
509 9
     *
510 9
     * @param DateInterval|Period|int|string $duration
511
     */
512
    public function moveEndDate($duration): self
513
    {
514
        return $this->endingOn($this->endDate->add(duration($duration)));
515
    }
516
517
    /**
518
     * Returns a new instance where the datepoints
519
     * are moved forwards or backward simultaneously by the given DateInterval.
520
     *
521
     * This method MUST retain the state of the current instance, and return
522
     * an instance that contains the specified new datepoints.
523
     *
524
     * @param DateInterval|Period|int|string $duration
525
     */
526
    public function move($duration): self
527
    {
528
        $duration = duration($duration);
529
        $period = new self($this->startDate->add($duration), $this->endDate->add($duration));
530
        if ($period->equals($this)) {
531
            return $this;
532
        }
533
534
        return $period;
535 6
    }
536
537 6
    /**
538 6
     * Returns an instance where the given DateInterval is simultaneously
539
     * substracted from the starting datepoint and added to the ending datepoint.
540 6
     *
541 6
     * This method MUST retain the state of the current instance, and return
542 3
     * an instance that contains the specified new datepoints.
543
     *
544 6
     * Depending on the duration value, the resulting instance duration will be expanded or shrinked.
545
     *
546 6
     * @param DateInterval|Period|int|string $duration
547 6
     */
548 6
    public function expand($duration): self
549
    {
550
        $duration = duration($duration);
551
        $period = new self($this->startDate->sub($duration), $this->endDate->add($duration));
552
        if ($period->equals($this)) {
553
            return $this;
554
        }
555
556
        return $period;
557
    }
558 3
559
    /**
560 3
     * Returns the difference between two instances expressed in seconds.
561
     */
562 3
    public function timestampIntervalDiff(Period $interval): float
563
    {
564
        return $this->getTimestampInterval() - $interval->getTimestampInterval();
565
    }
566
567
    /**
568
     * Returns the difference between two instances expressed in DateInterval.
569
     */
570
    public function dateIntervalDiff(Period $interval): DateInterval
571
    {
572
        return $this->endDate->diff($this->startDate->add($interval->getDateInterval()));
573 6
    }
574
575 6
    /**
576 6
     * Merges one or more instances to return a new instance.
577 6
     * The resulting instance represents the largest duration possible.
578
     *
579
     * @param Period ...$intervals
580 6
     */
581 6
    public function merge(Period $interval, Period ...$intervals): self
582
    {
583
        array_unshift($intervals, $interval);
584
        $carry = $this;
585
        foreach ($intervals as $interval) {
586
            if ($carry->getStartDate() > $interval->getStartDate()) {
587
                $carry = $carry->startingOn($interval->getStartDate());
588
            }
589
590
            if ($carry->getEndDate() < $interval->getEndDate()) {
591
                $carry = $carry->endingOn($interval->getEndDate());
592
            }
593
        }
594
595
        return $carry;
596
    }
597
}
598