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 IteratorAggregate; |
11
|
|
|
use Spatie\Period\Exceptions\InvalidDate; |
12
|
|
|
use Spatie\Period\Exceptions\InvalidPeriod; |
13
|
|
|
use Spatie\Period\Exceptions\CannotComparePeriods; |
14
|
|
|
|
15
|
|
|
class EndlessPeriod implements IteratorAggregate, PeriodInterface |
16
|
|
|
{ |
17
|
|
|
/** @var \DateTimeImmutable */ |
18
|
|
|
protected $start; |
19
|
|
|
|
20
|
|
|
/** @var \DateTimeImmutable */ |
21
|
|
|
protected $end; |
22
|
|
|
|
23
|
|
|
/** @var \DateInterval */ |
24
|
|
|
protected $interval; |
25
|
|
|
|
26
|
|
|
/** @var \DateTimeImmutable */ |
27
|
|
|
private $includedStart; |
28
|
|
|
|
29
|
|
|
/** @var \DateTimeImmutable */ |
30
|
|
|
private $includedEnd; |
31
|
|
|
|
32
|
|
|
/** @var int */ |
33
|
|
|
private $boundaryExclusionMask; |
34
|
|
|
|
35
|
|
|
/** @var int */ |
36
|
|
|
public $precisionMask; |
37
|
|
|
|
38
|
|
|
public function __construct( |
39
|
|
|
DateTimeImmutable $start, |
40
|
|
|
?DateTimeImmutable $end, |
41
|
|
|
?int $precisionMask = null, |
42
|
|
|
?int $boundaryExclusionMask = null |
43
|
|
|
) { |
44
|
|
|
// if ($start > $end) { |
45
|
|
|
// throw InvalidPeriod::endBeforeStart($start, $end); |
46
|
|
|
// } |
47
|
|
|
|
48
|
|
|
$this->boundaryExclusionMask = $boundaryExclusionMask ?? Boundaries::EXCLUDE_NONE; |
49
|
|
|
$this->precisionMask = $precisionMask ?? Precision::DAY; |
50
|
|
|
|
51
|
|
|
$this->start = $this->roundDate($start, $this->precisionMask); |
52
|
|
|
$this->end = $end ? $this->roundDate($end, $this->precisionMask) : null; |
53
|
|
|
$this->interval = $this->createDateInterval($this->precisionMask); |
54
|
|
|
|
55
|
|
|
$this->includedStart = $this->startIncluded() |
56
|
|
|
? $this->start |
57
|
|
|
: $this->start->add($this->interval); |
58
|
|
|
|
59
|
|
|
$this->includedEnd = $this->endIncluded() |
60
|
|
|
? $this->end |
61
|
|
|
: $this->end->sub($this->interval); |
62
|
|
|
} |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* @param string|DateTimeInterface $start |
66
|
|
|
* @param string|DateTimeInterface $end |
67
|
|
|
* @param int|null $precisionMask |
68
|
|
|
* @param int|null $boundaryExclusionMask |
69
|
|
|
* @param string|null $format |
70
|
|
|
* |
71
|
|
|
* @return static |
72
|
|
|
*/ |
73
|
|
|
public static function make( |
74
|
|
|
$start, |
75
|
|
|
$end = null, |
76
|
|
|
?int $precisionMask = null, |
77
|
|
|
?int $boundaryExclusionMask = null, |
78
|
|
|
?string $format = null |
79
|
|
|
): PeriodInterface { |
80
|
|
|
if ($start === null) { |
81
|
|
|
throw InvalidDate::cannotBeNull('Start date'); |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
// if ($end !== null) { |
85
|
|
|
// throw InvalidDate::shouldBeNull('End date'); |
86
|
|
|
// } |
87
|
|
|
|
88
|
|
|
return new static( |
89
|
|
|
static::resolveDate($start, $format), |
90
|
|
|
$end ? static::resolveDate($end, $format) : null, |
91
|
|
|
$precisionMask, |
92
|
|
|
$boundaryExclusionMask |
93
|
|
|
); |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
public function startIncluded(): bool |
97
|
|
|
{ |
98
|
|
|
return ! $this->startExcluded(); |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
public function startExcluded(): bool |
102
|
|
|
{ |
103
|
|
|
return Boundaries::EXCLUDE_START & $this->boundaryExclusionMask; |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
public function endIncluded(): bool |
107
|
|
|
{ |
108
|
|
|
return ! $this->endExcluded(); |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
public function endExcluded(): bool |
112
|
|
|
{ |
113
|
|
|
return Boundaries::EXCLUDE_END & $this->boundaryExclusionMask; |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
public function getStart(): DateTimeImmutable |
117
|
|
|
{ |
118
|
|
|
return $this->start; |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
public function getIncludedStart(): DateTimeImmutable |
122
|
|
|
{ |
123
|
|
|
return $this->includedStart; |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
public function getEnd(): DateTimeImmutable |
127
|
|
|
{ |
128
|
|
|
return $this->end; |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
public function getIncludedEnd(): ?DateTimeImmutable |
132
|
|
|
{ |
133
|
|
|
return $this->includedEnd; |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
public function length(): ?int |
137
|
|
|
{ |
138
|
|
|
return null; |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
public function overlapsWith(PeriodInterface $period): bool |
142
|
|
|
{ |
143
|
|
|
$this->ensurePrecisionMatches($period); |
144
|
|
|
|
145
|
|
|
if ($this->getIncludedStart() > $period->getIncludedEnd()) { |
146
|
|
|
return false; |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
if ($period->getIncludedStart() > $this->getIncludedEnd()) { |
150
|
|
|
return false; |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
return true; |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
public function touchesWith(PeriodInterface $period): bool |
157
|
|
|
{ |
158
|
|
|
$this->ensurePrecisionMatches($period); |
159
|
|
|
|
160
|
|
|
if ($period instanceof EndlessPeriod |
161
|
|
|
&& (null == $this->getIncludedEnd() && null == $period->getIncludedEnd())) { |
162
|
|
|
return true; |
163
|
|
|
} else { |
164
|
|
|
return false; |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
if ($this->getIncludedStart()->diff($period->getIncludedEnd())->days <= 1) { |
|
|
|
|
168
|
|
|
return true; |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
return false; |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
public function startsBefore(DateTimeInterface $date): bool |
175
|
|
|
{ |
176
|
|
|
return $this->getIncludedStart() < $date; |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
public function startsBeforeOrAt(DateTimeInterface $date): bool |
180
|
|
|
{ |
181
|
|
|
return $this->getIncludedStart() <= $date; |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
public function startsAfter(DateTimeInterface $date): bool |
185
|
|
|
{ |
186
|
|
|
return $this->getIncludedStart() > $date; |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
public function startsAfterOrAt(DateTimeInterface $date): bool |
190
|
|
|
{ |
191
|
|
|
return $this->getIncludedStart() >= $date; |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
public function startsAt(DateTimeInterface $date): bool |
195
|
|
|
{ |
196
|
|
|
return $this->getIncludedStart()->getTimestamp() === $this->roundDate( |
197
|
|
|
$date, |
198
|
|
|
$this->precisionMask |
199
|
|
|
)->getTimestamp(); |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
public function endsBefore(DateTimeInterface $date): bool |
203
|
|
|
{ |
204
|
|
|
return $this->getIncludedEnd() < $this->roundDate( |
205
|
|
|
$date, |
206
|
|
|
$this->precisionMask |
207
|
|
|
); |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
public function endsBeforeOrAt(DateTimeInterface $date): bool |
211
|
|
|
{ |
212
|
|
|
return $this->getIncludedEnd() <= $this->roundDate( |
213
|
|
|
$date, |
214
|
|
|
$this->precisionMask |
215
|
|
|
); |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
public function endsAfter(DateTimeInterface $date): bool |
219
|
|
|
{ |
220
|
|
|
return $this->getIncludedEnd() > $this->roundDate( |
221
|
|
|
$date, |
222
|
|
|
$this->precisionMask |
223
|
|
|
); |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
public function endsAfterOrAt(DateTimeInterface $date): bool |
227
|
|
|
{ |
228
|
|
|
return $this->getIncludedEnd() >= $this->roundDate( |
229
|
|
|
$date, |
230
|
|
|
$this->precisionMask |
231
|
|
|
); |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
public function endsAt(DateTimeInterface $date): bool |
235
|
|
|
{ |
236
|
|
|
return $this->getIncludedEnd()->getTimestamp() === $this->roundDate( |
237
|
|
|
$date, |
238
|
|
|
$this->precisionMask |
239
|
|
|
)->getTimestamp(); |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
public function contains(DateTimeInterface $date): bool |
243
|
|
|
{ |
244
|
|
|
if ($this->roundDate($date, $this->precisionMask) < $this->getIncludedStart()) { |
245
|
|
|
return false; |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
if ($this->roundDate($date, $this->precisionMask) > $this->getIncludedEnd()) { |
249
|
|
|
return false; |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
return true; |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
public function equals(PeriodInterface $period): bool |
256
|
|
|
{ |
257
|
|
|
$this->ensurePrecisionMatches($period); |
258
|
|
|
|
259
|
|
|
if ($period->getIncludedStart()->getTimestamp() !== $this->getIncludedStart()->getTimestamp()) { |
260
|
|
|
return false; |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
if ($period->getIncludedEnd()->getTimestamp() !== $this->getIncludedEnd()->getTimestamp()) { |
264
|
|
|
return false; |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
return true; |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
/** |
271
|
|
|
* @param \Spatie\Period\Period $period |
272
|
|
|
* |
273
|
|
|
* @return static|null |
274
|
|
|
* @throws \Exception |
275
|
|
|
*/ |
276
|
|
|
public function gap(PeriodInterface $period): ?Period |
277
|
|
|
{ |
278
|
|
|
$this->ensurePrecisionMatches($period); |
279
|
|
|
|
280
|
|
|
if ($this->overlapsWith($period)) { |
281
|
|
|
return null; |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
if ($this->touchesWith($period)) { |
285
|
|
|
return null; |
286
|
|
|
} |
287
|
|
|
|
288
|
|
|
if ($this->getIncludedStart() >= $period->getIncludedEnd()) { |
289
|
|
|
return static::make( |
290
|
|
|
$period->getIncludedEnd()->add($this->interval), |
291
|
|
|
$this->getIncludedStart()->sub($this->interval), |
|
|
|
|
292
|
|
|
$this->getPrecisionMask() |
293
|
|
|
); |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
return static::make( |
297
|
|
|
$this->getIncludedEnd()->add($this->interval), |
298
|
|
|
$period->getIncludedStart()->sub($this->interval), |
|
|
|
|
299
|
|
|
$this->getPrecisionMask() |
300
|
|
|
); |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
/** |
304
|
|
|
* @param \Spatie\Period\Period $period |
305
|
|
|
* |
306
|
|
|
* @return static|null |
307
|
|
|
*/ |
308
|
|
|
public function overlapSingle(PeriodInterface $period): ?Period |
309
|
|
|
{ |
310
|
|
|
$this->ensurePrecisionMatches($period); |
311
|
|
|
|
312
|
|
|
$start = $this->getIncludedStart() > $period->getIncludedStart() |
313
|
|
|
? $this->getIncludedStart() |
314
|
|
|
: $period->getIncludedStart(); |
315
|
|
|
|
316
|
|
|
$end = $this->getIncludedEnd() < $period->getIncludedEnd() |
317
|
|
|
? $this->getIncludedEnd() |
318
|
|
|
: $period->getIncludedEnd(); |
319
|
|
|
|
320
|
|
|
if ($start > $end) { |
321
|
|
|
return null; |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
return static::make($start, $end, $this->getPrecisionMask()); |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
/** |
328
|
|
|
* @param \Spatie\Period\Period ...$periods |
329
|
|
|
* |
330
|
|
|
* @return \Spatie\Period\PeriodCollection|static[] |
331
|
|
|
*/ |
332
|
|
|
public function overlap(PeriodInterface ...$periods): PeriodCollection |
333
|
|
|
{ |
334
|
|
|
$overlapCollection = new PeriodCollection(); |
335
|
|
|
|
336
|
|
|
foreach ($periods as $period) { |
337
|
|
|
$overlap = $this->overlapSingle($period); |
338
|
|
|
|
339
|
|
|
if ($overlap === null) { |
340
|
|
|
continue; |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
$overlapCollection[] = $overlap; |
344
|
|
|
} |
345
|
|
|
|
346
|
|
|
return $overlapCollection; |
347
|
|
|
} |
348
|
|
|
|
349
|
|
|
/** |
350
|
|
|
* @param \Spatie\Period\Period ...$periods |
351
|
|
|
* |
352
|
|
|
* @return static |
353
|
|
|
*/ |
354
|
|
|
public function overlapAll(PeriodInterface ...$periods): Period |
355
|
|
|
{ |
356
|
|
|
$overlap = clone $this; |
357
|
|
|
|
358
|
|
|
if (! count($periods)) { |
359
|
|
|
return $overlap; |
360
|
|
|
} |
361
|
|
|
|
362
|
|
|
foreach ($periods as $period) { |
363
|
|
|
$overlap = $overlap->overlapSingle($period); |
364
|
|
|
} |
365
|
|
|
|
366
|
|
|
return $overlap; |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
public function diffSingle(PeriodInterface $period): PeriodCollection |
370
|
|
|
{ |
371
|
|
|
$this->ensurePrecisionMatches($period); |
372
|
|
|
|
373
|
|
|
$periodCollection = new PeriodCollection(); |
374
|
|
|
|
375
|
|
|
if (! $this->overlapsWith($period)) { |
376
|
|
|
$periodCollection[] = clone $this; |
377
|
|
|
$periodCollection[] = clone $period; |
378
|
|
|
|
379
|
|
|
return $periodCollection; |
380
|
|
|
} |
381
|
|
|
|
382
|
|
|
$overlap = $this->overlapSingle($period); |
383
|
|
|
|
384
|
|
|
$start = $this->getIncludedStart() < $period->getIncludedStart() |
385
|
|
|
? $this->getIncludedStart() |
386
|
|
|
: $period->getIncludedStart(); |
387
|
|
|
|
388
|
|
|
$end = $this->getIncludedEnd() > $period->getIncludedEnd() |
389
|
|
|
? $this->getIncludedEnd() |
390
|
|
|
: $period->getIncludedEnd(); |
391
|
|
|
|
392
|
|
|
if ($overlap->getIncludedStart() > $start) { |
393
|
|
|
$periodCollection[] = static::make( |
394
|
|
|
$start, |
395
|
|
|
$overlap->getIncludedStart()->sub($this->interval), |
396
|
|
|
$this->getPrecisionMask() |
397
|
|
|
); |
398
|
|
|
} |
399
|
|
|
|
400
|
|
|
if ($overlap->getIncludedEnd() < $end) { |
401
|
|
|
$periodCollection[] = static::make( |
402
|
|
|
$overlap->getIncludedEnd()->add($this->interval), |
403
|
|
|
$end, |
404
|
|
|
$this->getPrecisionMask() |
405
|
|
|
); |
406
|
|
|
} |
407
|
|
|
|
408
|
|
|
return $periodCollection; |
409
|
|
|
} |
410
|
|
|
|
411
|
|
|
/** |
412
|
|
|
* @param \Spatie\Period\Period ...$periods |
413
|
|
|
* |
414
|
|
|
* @return \Spatie\Period\PeriodCollection|static[] |
415
|
|
|
*/ |
416
|
|
|
public function diff(PeriodInterface ...$periods): PeriodCollection |
417
|
|
|
{ |
418
|
|
|
if (count($periods) === 1 && ! $this->overlapsWith($periods[0])) { |
419
|
|
|
$collection = new PeriodCollection(); |
420
|
|
|
|
421
|
|
|
$gap = $this->gap($periods[0]); |
422
|
|
|
|
423
|
|
|
if ($gap !== null) { |
424
|
|
|
$collection[] = $gap; |
425
|
|
|
} |
426
|
|
|
|
427
|
|
|
return $collection; |
428
|
|
|
} |
429
|
|
|
|
430
|
|
|
$diffs = []; |
431
|
|
|
|
432
|
|
|
foreach ($periods as $period) { |
433
|
|
|
$diffs[] = $this->diffSingle($period); |
434
|
|
|
} |
435
|
|
|
|
436
|
|
|
$collection = (new PeriodCollection($this))->overlap(...$diffs); |
437
|
|
|
|
438
|
|
|
return $collection; |
439
|
|
|
} |
440
|
|
|
|
441
|
|
|
public function getPrecisionMask(): int |
442
|
|
|
{ |
443
|
|
|
return $this->precisionMask; |
444
|
|
|
} |
445
|
|
|
|
446
|
|
|
public function getIterator() |
447
|
|
|
{ |
448
|
|
|
return new DatePeriod( |
449
|
|
|
$this->getIncludedStart(), |
450
|
|
|
$this->interval, |
451
|
|
|
$this->getIncludedEnd()->add($this->interval) |
452
|
|
|
); |
453
|
|
|
} |
454
|
|
|
|
455
|
|
|
protected static function resolveDate($date, ?string $format): DateTimeImmutable |
456
|
|
|
{ |
457
|
|
|
if ($date instanceof DateTimeImmutable) { |
458
|
|
|
return $date; |
459
|
|
|
} |
460
|
|
|
|
461
|
|
|
if ($date instanceof DateTime) { |
462
|
|
|
return DateTimeImmutable::createFromMutable($date); |
463
|
|
|
} |
464
|
|
|
|
465
|
|
|
$format = static::resolveFormat($date, $format); |
466
|
|
|
|
467
|
|
|
if (! is_string($date)) { |
468
|
|
|
throw InvalidDate::forFormat($date, $format); |
469
|
|
|
} |
470
|
|
|
|
471
|
|
|
$dateTime = DateTimeImmutable::createFromFormat($format, $date); |
472
|
|
|
|
473
|
|
|
if ($dateTime === false) { |
474
|
|
|
throw InvalidDate::forFormat($date, $format); |
475
|
|
|
} |
476
|
|
|
|
477
|
|
|
if (strpos($format, ' ') === false) { |
478
|
|
|
$dateTime = $dateTime->setTime(0, 0, 0); |
479
|
|
|
} |
480
|
|
|
|
481
|
|
|
return $dateTime; |
482
|
|
|
} |
483
|
|
|
|
484
|
|
|
protected static function resolveFormat($date, ?string $format): string |
485
|
|
|
{ |
486
|
|
|
if ($format !== null) { |
487
|
|
|
return $format; |
488
|
|
|
} |
489
|
|
|
|
490
|
|
|
if (strpos($format, ' ') === false && strpos($date, ' ') !== false) { |
491
|
|
|
return 'Y-m-d H:i:s'; |
492
|
|
|
} |
493
|
|
|
|
494
|
|
|
return 'Y-m-d'; |
495
|
|
|
} |
496
|
|
|
|
497
|
|
|
protected function roundDate(DateTimeInterface $date, int $precision): DateTimeImmutable |
498
|
|
|
{ |
499
|
|
|
[$year, $month, $day, $hour, $minute, $second] = explode(' ', $date->format('Y m d H i s')); |
|
|
|
|
500
|
|
|
|
501
|
|
|
$month = (Precision::MONTH & $precision) === Precision::MONTH ? $month : '01'; |
|
|
|
|
502
|
|
|
$day = (Precision::DAY & $precision) === Precision::DAY ? $day : '01'; |
|
|
|
|
503
|
|
|
$hour = (Precision::HOUR & $precision) === Precision::HOUR ? $hour : '00'; |
|
|
|
|
504
|
|
|
$minute = (Precision::MINUTE & $precision) === Precision::MINUTE ? $minute : '00'; |
|
|
|
|
505
|
|
|
$second = (Precision::SECOND & $precision) === Precision::SECOND ? $second : '00'; |
|
|
|
|
506
|
|
|
|
507
|
|
|
return DateTimeImmutable::createFromFormat( |
508
|
|
|
'Y m d H i s', |
509
|
|
|
implode(' ', [$year, $month, $day, $hour, $minute, $second]) |
510
|
|
|
); |
511
|
|
|
} |
512
|
|
|
|
513
|
|
|
protected function createDateInterval(int $precision): DateInterval |
514
|
|
|
{ |
515
|
|
|
$interval = [ |
516
|
|
|
Precision::SECOND => 'PT1S', |
517
|
|
|
Precision::MINUTE => 'PT1M', |
518
|
|
|
Precision::HOUR => 'PT1H', |
519
|
|
|
Precision::DAY => 'P1D', |
520
|
|
|
Precision::MONTH => 'P1M', |
521
|
|
|
Precision::YEAR => 'P1Y', |
522
|
|
|
][$precision]; |
523
|
|
|
|
524
|
|
|
return new DateInterval($interval); |
525
|
|
|
} |
526
|
|
|
|
527
|
|
|
protected function ensurePrecisionMatches(PeriodInterface $period): void |
528
|
|
|
{ |
529
|
|
|
if ($this->precisionMask === $period->precisionMask) { |
|
|
|
|
530
|
|
|
return; |
531
|
|
|
} |
532
|
|
|
|
533
|
|
|
throw CannotComparePeriods::precisionDoesNotMatch(); |
534
|
|
|
} |
535
|
|
|
} |
536
|
|
|
|
This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.
Unreachable code is most often the result of
return
,die
orexit
statements that have been added for debug purposes.In the above example, the last
return false
will never be executed, because a return statement has already been met in every possible execution path.