GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Pull Request — master (#50)
by Jérôme
01:28
created

Period::fromDatePeriod()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.9666
c 0
b 0
f 0
cc 2
nc 2
nop 1
1
<?php
2
3
namespace Spatie\Period;
4
5
use DateTime;
6
use DatePeriod;
7
use DateInterval;
8
use DateTimeImmutable;
9
use DateTimeInterface;
10
use InvalidArgumentException;
11
use IteratorAggregate;
12
use Spatie\Period\Exceptions\InvalidDate;
13
use Spatie\Period\Exceptions\InvalidPeriod;
14
use Spatie\Period\Exceptions\CannotComparePeriods;
15
16
class Period implements IteratorAggregate
17
{
18
    /** @var \DateTimeImmutable */
19
    protected $start;
20
21
    /** @var \DateTimeImmutable */
22
    protected $end;
23
24
    /** @var \DateInterval */
25
    protected $interval;
26
27
    /** @var \DateTimeImmutable */
28
    private $includedStart;
29
30
    /** @var \DateTimeImmutable */
31
    private $includedEnd;
32
33
    /** @var int */
34
    private $boundaryExclusionMask;
35
36
    /** @var int */
37
    private $precisionMask;
38
39
    public function __construct(
40
        DateTimeImmutable $start,
41
        DateTimeImmutable $end,
42
        ?int $precisionMask = null,
43
        ?int $boundaryExclusionMask = null
44
    ) {
45
        if ($start > $end) {
46
            throw InvalidPeriod::endBeforeStart($start, $end);
47
        }
48
49
        $this->boundaryExclusionMask = $boundaryExclusionMask ?? Boundaries::EXCLUDE_NONE;
50
        $this->precisionMask = $precisionMask ?? Precision::DAY;
51
52
        $this->start = $this->roundDate($start, $this->precisionMask);
53
        $this->end = $this->roundDate($end, $this->precisionMask);
54
        $this->interval = $this->createDateInterval($this->precisionMask);
55
56
        $this->includedStart = $this->startIncluded()
57
            ? $this->start
58
            : $this->start->add($this->interval);
59
60
        $this->includedEnd = $this->endIncluded()
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->endIncluded() ? $...d->sub($this->interval) can also be of type false. However, the property $includedEnd is declared as type object<DateTimeImmutable>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
61
            ? $this->end
62
            : $this->end->sub($this->interval);
63
    }
64
65
    /**
66
     * @param string|DateTimeInterface $start
67
     * @param string|DateTimeInterface $end
68
     * @param int|null $precisionMask
69
     * @param int|null $boundaryExclusionMask
70
     * @param string|null $format
71
     *
72
     * @return static
73
     */
74
    public static function make(
75
        $start,
76
        $end,
77
        ?int $precisionMask = null,
78
        ?int $boundaryExclusionMask = null,
79
        ?string $format = null
80
    ): Period {
81
        if ($start === null) {
82
            throw InvalidDate::cannotBeNull('Start date');
83
        }
84
85
        if ($end === null) {
86
            throw InvalidDate::cannotBeNull('End date');
87
        }
88
89
        return new static(
90
            static::resolveDate($start, $format),
91
            static::resolveDate($end, $format),
92
            $precisionMask,
93
            $boundaryExclusionMask
94
        );
95
    }
96
97
    public static function fromDatePeriod(DatePeriod $period): Period
98
    {
99
        $start = $period->getStartDate();
100
        $end = $period->getEndDate();
101
        $precision = static::resolvePrecision($period->getDateInterval());
102
        $boundaries = $period->include_start_date ? Boundaries::EXCLUDE_NONE : Boundaries::EXCLUDE_START;
0 ignored issues
show
Bug introduced by
The property include_start_date does not seem to exist in DatePeriod.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
103
104
        return static::make($start, $end, $precision, $boundaries);
105
    }
106
107
    public function startIncluded(): bool
108
    {
109
        return ! $this->startExcluded();
110
    }
111
112
    public function startExcluded(): bool
113
    {
114
        return Boundaries::EXCLUDE_START & $this->boundaryExclusionMask;
115
    }
116
117
    public function endIncluded(): bool
118
    {
119
        return ! $this->endExcluded();
120
    }
121
122
    public function endExcluded(): bool
123
    {
124
        return Boundaries::EXCLUDE_END & $this->boundaryExclusionMask;
125
    }
126
127
    public function getStart(): DateTimeImmutable
128
    {
129
        return $this->start;
130
    }
131
132
    public function getIncludedStart(): DateTimeImmutable
133
    {
134
        return $this->includedStart;
135
    }
136
137
    public function getEnd(): DateTimeImmutable
138
    {
139
        return $this->end;
140
    }
141
142
    public function getIncludedEnd(): DateTimeImmutable
143
    {
144
        return $this->includedEnd;
145
    }
146
147
    public function length(): int
148
    {
149
        $length = $this->getIncludedStart()->diff($this->getIncludedEnd())->days + 1;
150
151
        return $length;
152
    }
153
154
    public function overlapsWith(Period $period): bool
155
    {
156
        $this->ensurePrecisionMatches($period);
157
158
        if ($this->getIncludedStart() > $period->getIncludedEnd()) {
159
            return false;
160
        }
161
162
        if ($period->getIncludedStart() > $this->getIncludedEnd()) {
163
            return false;
164
        }
165
166
        return true;
167
    }
168
169
    public function touchesWith(Period $period): bool
170
    {
171
        $this->ensurePrecisionMatches($period);
172
173
        if ($this->getIncludedEnd()->diff($period->getIncludedStart())->days <= 1) {
174
            return true;
175
        }
176
177
        if ($this->getIncludedStart()->diff($period->getIncludedEnd())->days <= 1) {
178
            return true;
179
        }
180
181
        return false;
182
    }
183
184
    public function startsBefore(DateTimeInterface $date): bool
185
    {
186
        return $this->getIncludedStart() < $date;
187
    }
188
189
    public function startsBeforeOrAt(DateTimeInterface $date): bool
190
    {
191
        return $this->getIncludedStart() <= $date;
192
    }
193
194
    public function startsAfter(DateTimeInterface $date): bool
195
    {
196
        return $this->getIncludedStart() > $date;
197
    }
198
199
    public function startsAfterOrAt(DateTimeInterface $date): bool
200
    {
201
        return $this->getIncludedStart() >= $date;
202
    }
203
204
    public function startsAt(DateTimeInterface $date): bool
205
    {
206
        return $this->getIncludedStart()->getTimestamp() === $this->roundDate(
207
            $date,
208
            $this->precisionMask
209
        )->getTimestamp();
210
    }
211
212
    public function endsBefore(DateTimeInterface $date): bool
213
    {
214
        return $this->getIncludedEnd() < $this->roundDate(
215
                $date,
216
                $this->precisionMask
217
            );
218
    }
219
220
    public function endsBeforeOrAt(DateTimeInterface $date): bool
221
    {
222
        return $this->getIncludedEnd() <= $this->roundDate(
223
                $date,
224
                $this->precisionMask
225
            );
226
    }
227
228
    public function endsAfter(DateTimeInterface $date): bool
229
    {
230
        return $this->getIncludedEnd() > $this->roundDate(
231
                $date,
232
                $this->precisionMask
233
            );
234
    }
235
236
    public function endsAfterOrAt(DateTimeInterface $date): bool
237
    {
238
        return $this->getIncludedEnd() >= $this->roundDate(
239
                $date,
240
                $this->precisionMask
241
            );
242
    }
243
244
    public function endsAt(DateTimeInterface $date): bool
245
    {
246
        return $this->getIncludedEnd()->getTimestamp() === $this->roundDate(
247
                $date,
248
                $this->precisionMask
249
            )->getTimestamp();
250
    }
251
252
    public function contains(DateTimeInterface $date): bool
253
    {
254
        if ($this->roundDate($date, $this->precisionMask) < $this->getIncludedStart()) {
255
            return false;
256
        }
257
258
        if ($this->roundDate($date, $this->precisionMask) > $this->getIncludedEnd()) {
259
            return false;
260
        }
261
262
        return true;
263
    }
264
265
    public function equals(Period $period): bool
266
    {
267
        $this->ensurePrecisionMatches($period);
268
269
        if ($period->getIncludedStart()->getTimestamp() !== $this->getIncludedStart()->getTimestamp()) {
270
            return false;
271
        }
272
273
        if ($period->getIncludedEnd()->getTimestamp() !== $this->getIncludedEnd()->getTimestamp()) {
274
            return false;
275
        }
276
277
        return true;
278
    }
279
280
    /**
281
     * @param \Spatie\Period\Period $period
282
     *
283
     * @return static|null
284
     * @throws \Exception
285
     */
286
    public function gap(Period $period): ?Period
287
    {
288
        $this->ensurePrecisionMatches($period);
289
290
        if ($this->overlapsWith($period)) {
291
            return null;
292
        }
293
294
        if ($this->touchesWith($period)) {
295
            return null;
296
        }
297
298
        if ($this->getIncludedStart() >= $period->getIncludedEnd()) {
299
            return static::make(
300
                $period->getIncludedEnd()->add($this->interval),
301
                $this->getIncludedStart()->sub($this->interval),
0 ignored issues
show
Security Bug introduced by
It seems like $this->getIncludedStart()->sub($this->interval) targeting DateTimeImmutable::sub() can also be of type false; however, Spatie\Period\Period::make() does only seem to accept string|object<DateTimeInterface>, did you maybe forget to handle an error condition?
Loading history...
302
                $this->getPrecisionMask()
303
            );
304
        }
305
306
        return static::make(
307
            $this->getIncludedEnd()->add($this->interval),
308
            $period->getIncludedStart()->sub($this->interval),
0 ignored issues
show
Security Bug introduced by
It seems like $period->getIncludedStart()->sub($this->interval) targeting DateTimeImmutable::sub() can also be of type false; however, Spatie\Period\Period::make() does only seem to accept string|object<DateTimeInterface>, did you maybe forget to handle an error condition?
Loading history...
309
            $this->getPrecisionMask()
310
        );
311
    }
312
313
    /**
314
     * @param \Spatie\Period\Period $period
315
     *
316
     * @return static|null
317
     */
318
    public function overlapSingle(Period $period): ?Period
319
    {
320
        $this->ensurePrecisionMatches($period);
321
322
        $start = $this->getIncludedStart() > $period->getIncludedStart()
323
            ? $this->getIncludedStart()
324
            : $period->getIncludedStart();
325
326
        $end = $this->getIncludedEnd() < $period->getIncludedEnd()
327
            ? $this->getIncludedEnd()
328
            : $period->getIncludedEnd();
329
330
        if ($start > $end) {
331
            return null;
332
        }
333
334
        return static::make($start, $end, $this->getPrecisionMask());
335
    }
336
337
    /**
338
     * @param \Spatie\Period\Period ...$periods
339
     *
340
     * @return \Spatie\Period\PeriodCollection|static[]
341
     */
342
    public function overlap(Period ...$periods): PeriodCollection
343
    {
344
        $overlapCollection = new PeriodCollection();
345
346
        foreach ($periods as $period) {
347
            $overlap = $this->overlapSingle($period);
348
349
            if ($overlap === null) {
350
                continue;
351
            }
352
353
            $overlapCollection[] = $overlap;
354
        }
355
356
        return $overlapCollection;
357
    }
358
359
    /**
360
     * @param \Spatie\Period\Period ...$periods
361
     *
362
     * @return static
363
     */
364
    public function overlapAll(Period ...$periods): Period
365
    {
366
        $overlap = clone $this;
367
368
        if (! count($periods)) {
369
            return $overlap;
370
        }
371
372
        foreach ($periods as $period) {
373
            $overlap = $overlap->overlapSingle($period);
374
        }
375
376
        return $overlap;
377
    }
378
379
    public function diffSingle(Period $period): PeriodCollection
380
    {
381
        $this->ensurePrecisionMatches($period);
382
383
        $periodCollection = new PeriodCollection();
384
385
        if (! $this->overlapsWith($period)) {
386
            $periodCollection[] = clone $this;
387
            $periodCollection[] = clone $period;
388
389
            return $periodCollection;
390
        }
391
392
        $overlap = $this->overlapSingle($period);
393
394
        $start = $this->getIncludedStart() < $period->getIncludedStart()
395
            ? $this->getIncludedStart()
396
            : $period->getIncludedStart();
397
398
        $end = $this->getIncludedEnd() > $period->getIncludedEnd()
399
            ? $this->getIncludedEnd()
400
            : $period->getIncludedEnd();
401
402
        if ($overlap->getIncludedStart() > $start) {
403
            $periodCollection[] = static::make(
404
                $start,
405
                $overlap->getIncludedStart()->sub($this->interval),
406
                $this->getPrecisionMask()
407
            );
408
        }
409
410
        if ($overlap->getIncludedEnd() < $end) {
411
            $periodCollection[] = static::make(
412
                $overlap->getIncludedEnd()->add($this->interval),
413
                $end,
414
                $this->getPrecisionMask()
415
            );
416
        }
417
418
        return $periodCollection;
419
    }
420
421
    /**
422
     * @param \Spatie\Period\Period ...$periods
423
     *
424
     * @return \Spatie\Period\PeriodCollection|static[]
425
     */
426
    public function diff(Period ...$periods): PeriodCollection
427
    {
428
        if (count($periods) === 1 && ! $this->overlapsWith($periods[0])) {
429
            $collection = new PeriodCollection();
430
431
            $gap = $this->gap($periods[0]);
432
433
            if ($gap !== null) {
434
                $collection[] = $gap;
435
            }
436
437
            return $collection;
438
        }
439
440
        $diffs = [];
441
442
        foreach ($periods as $period) {
443
            $diffs[] = $this->diffSingle($period);
444
        }
445
446
        $collection = (new PeriodCollection($this))->overlap(...$diffs);
447
448
        return $collection;
449
    }
450
451
    public function getPrecisionMask(): int
452
    {
453
        return $this->precisionMask;
454
    }
455
456
    public function asDatePeriod(): DatePeriod
457
    {
458
        return new DatePeriod(
459
            $this->getIncludedStart(),
460
            $this->interval,
461
            $this->getIncludedEnd()
462
        );
463
    }
464
465
    public function getIterator()
466
    {
467
        return new DatePeriod(
468
            $this->getIncludedStart(),
469
            $this->interval,
470
            $this->getIncludedEnd()->add($this->interval)
471
        );
472
    }
473
474
    protected static function resolveDate($date, ?string $format): DateTimeImmutable
475
    {
476
        if ($date instanceof DateTimeImmutable) {
477
            return $date;
478
        }
479
480
        if ($date instanceof DateTime) {
481
            return DateTimeImmutable::createFromMutable($date);
482
        }
483
484
        $format = static::resolveFormat($date, $format);
485
486
        if (! is_string($date)) {
487
            throw InvalidDate::forFormat($date, $format);
488
        }
489
490
        $dateTime = DateTimeImmutable::createFromFormat($format, $date);
491
492
        if ($dateTime === false) {
493
            throw InvalidDate::forFormat($date, $format);
494
        }
495
496
        if (strpos($format, ' ') === false) {
497
            $dateTime = $dateTime->setTime(0, 0, 0);
498
        }
499
500
        return $dateTime;
501
    }
502
503
    protected static function resolveFormat($date, ?string $format): string
504
    {
505
        if ($format !== null) {
506
            return $format;
507
        }
508
509
        if (strpos($format, ' ') === false && strpos($date, ' ') !== false) {
510
            return 'Y-m-d H:i:s';
511
        }
512
513
        return 'Y-m-d';
514
    }
515
516
    protected function roundDate(DateTimeInterface $date, int $precision): DateTimeImmutable
517
    {
518
        [$year, $month, $day, $hour, $minute, $second] = explode(' ', $date->format('Y m d H i s'));
0 ignored issues
show
Bug introduced by
The variable $year does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $month seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
Bug introduced by
The variable $day seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
Bug introduced by
The variable $hour seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
Bug introduced by
The variable $minute seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
Bug introduced by
The variable $second seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
519
520
        $month = (Precision::MONTH & $precision) === Precision::MONTH ? $month : '01';
0 ignored issues
show
Bug introduced by
The variable $month seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
521
        $day = (Precision::DAY & $precision) === Precision::DAY ? $day : '01';
0 ignored issues
show
Bug introduced by
The variable $day seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
522
        $hour = (Precision::HOUR & $precision) === Precision::HOUR ? $hour : '00';
0 ignored issues
show
Bug introduced by
The variable $hour seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
523
        $minute = (Precision::MINUTE & $precision) === Precision::MINUTE ? $minute : '00';
0 ignored issues
show
Bug introduced by
The variable $minute seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
524
        $second = (Precision::SECOND & $precision) === Precision::SECOND ? $second : '00';
0 ignored issues
show
Bug introduced by
The variable $second seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
525
526
        return DateTimeImmutable::createFromFormat(
527
            'Y m d H i s',
528
            implode(' ', [$year, $month, $day, $hour, $minute, $second])
529
        );
530
    }
531
532
    protected static function resolvePrecision(DateInterval $interval): ?int
533
    {
534
        $map = [
535
            's' => Precision::SECOND,
536
            'i' => Precision::MINUTE,
537
            'h' => Precision::HOUR,
538
            'd' => Precision::DAY,
539
            'm' => Precision::MONTH,
540
            'y' => Precision::YEAR,
541
        ];
542
543
        foreach ($map as $property => $precision) {
544
            if ($interval->{$property} !== 0) {
545
                return $precision;
546
            }
547
        }
548
549
        return Precision::DAY;
550
    }
551
552
    protected function createDateInterval(int $precision): DateInterval
553
    {
554
        $interval = [
555
            Precision::SECOND => 'PT1S',
556
            Precision::MINUTE => 'PT1M',
557
            Precision::HOUR => 'PT1H',
558
            Precision::DAY => 'P1D',
559
            Precision::MONTH => 'P1M',
560
            Precision::YEAR => 'P1Y',
561
        ][$precision];
562
563
        return new DateInterval($interval);
564
    }
565
566
    protected function ensurePrecisionMatches(Period $period): void
567
    {
568
        if ($this->precisionMask === $period->precisionMask) {
569
            return;
570
        }
571
572
        throw CannotComparePeriods::precisionDoesNotMatch();
573
    }
574
}
575