1 | <?php |
||
2 | |||
3 | /** |
||
4 | * League.Period (https://period.thephpleague.com) |
||
5 | * |
||
6 | * (c) Ignace Nyamagana Butera <[email protected]> |
||
7 | * |
||
8 | * For the full copyright and license information, please view the LICENSE |
||
9 | * file that was distributed with this source code. |
||
10 | */ |
||
11 | |||
12 | declare(strict_types=1); |
||
13 | |||
14 | namespace League\Period; |
||
15 | |||
16 | use DateInterval; |
||
17 | use DatePeriod; |
||
18 | use DateTimeImmutable; |
||
19 | use DateTimeInterface; |
||
20 | use DateTimeZone; |
||
21 | use JsonSerializable; |
||
22 | use function array_filter; |
||
23 | use function array_keys; |
||
24 | use function implode; |
||
25 | use function sprintf; |
||
26 | |||
27 | /** |
||
28 | * A immutable value object class to manipulate Time interval. |
||
29 | * |
||
30 | * @package League.period |
||
31 | * @author Ignace Nyamagana Butera <[email protected]> |
||
32 | * @since 1.0.0 |
||
33 | */ |
||
34 | final class Period implements JsonSerializable |
||
35 | { |
||
36 | private const ISO8601_FORMAT = 'Y-m-d\TH:i:s.u\Z'; |
||
37 | |||
38 | private const BOUNDARY_TYPE = [ |
||
39 | self::INCLUDE_START_EXCLUDE_END => 1, |
||
40 | self::INCLUDE_ALL => 1, |
||
41 | self::EXCLUDE_START_INCLUDE_END => 1, |
||
42 | self::EXCLUDE_ALL => 1, |
||
43 | ]; |
||
44 | |||
45 | public const INCLUDE_START_EXCLUDE_END = '[)'; |
||
46 | |||
47 | public const EXCLUDE_START_INCLUDE_END = '(]'; |
||
48 | |||
49 | public const EXCLUDE_ALL = '()'; |
||
50 | |||
51 | public const INCLUDE_ALL = '[]'; |
||
52 | |||
53 | /** |
||
54 | * The starting datepoint. |
||
55 | * |
||
56 | * @var DateTimeImmutable |
||
57 | */ |
||
58 | private $startDate; |
||
59 | |||
60 | /** |
||
61 | * The ending datepoint. |
||
62 | * |
||
63 | * @var DateTimeImmutable |
||
64 | */ |
||
65 | private $endDate; |
||
66 | |||
67 | /** |
||
68 | * The boundary type. |
||
69 | * |
||
70 | * @var string |
||
71 | */ |
||
72 | private $boundaryType; |
||
73 | |||
74 | /** |
||
75 | * Creates a new instance. |
||
76 | * |
||
77 | * @param mixed $startDate the starting datepoint |
||
78 | * @param mixed $endDate the ending datepoint |
||
79 | * |
||
80 | * @throws Exception If $startDate is greater than $endDate |
||
81 | */ |
||
82 | 930 | public function __construct($startDate, $endDate, string $boundaryType = self::INCLUDE_START_EXCLUDE_END) |
|
83 | { |
||
84 | 930 | $startDate = self::filterDatepoint($startDate); |
|
85 | 930 | $endDate = self::filterDatepoint($endDate); |
|
86 | 918 | if ($startDate > $endDate) { |
|
87 | 72 | throw new Exception('The ending datepoint must be greater or equal to the starting datepoint'); |
|
88 | } |
||
89 | |||
90 | 888 | if (!isset(self::BOUNDARY_TYPE[$boundaryType])) { |
|
91 | 6 | throw new Exception(sprintf( |
|
92 | 6 | 'The boundary type `%s` is invalid. The only valid values are %s', |
|
93 | 6 | $boundaryType, |
|
94 | 6 | '`'.implode('`, `', array_keys(self::BOUNDARY_TYPE)).'`' |
|
95 | )); |
||
96 | } |
||
97 | |||
98 | 885 | $this->startDate = $startDate; |
|
99 | 885 | $this->endDate = $endDate; |
|
100 | 885 | $this->boundaryType = $boundaryType; |
|
101 | 885 | } |
|
102 | |||
103 | /** |
||
104 | * Returns a DateTimeImmutable instance. |
||
105 | * |
||
106 | * @param mixed $datepoint a Datepoint |
||
107 | */ |
||
108 | 1089 | private static function filterDatepoint($datepoint): DateTimeImmutable |
|
109 | { |
||
110 | 1089 | if ($datepoint instanceof DateTimeImmutable) { |
|
111 | 873 | return $datepoint; |
|
112 | } |
||
113 | |||
114 | 738 | return Datepoint::create($datepoint); |
|
115 | } |
||
116 | |||
117 | /** |
||
118 | * Returns a DateInterval instance. |
||
119 | * |
||
120 | * @param mixed $duration a Duration |
||
121 | */ |
||
122 | 360 | private static function filterDuration($duration): DateInterval |
|
123 | { |
||
124 | 360 | if ($duration instanceof DateInterval) { |
|
125 | 150 | return $duration; |
|
126 | } |
||
127 | |||
128 | 210 | return Duration::create($duration); |
|
129 | } |
||
130 | |||
131 | /************************************************** |
||
132 | * Named constructors |
||
133 | **************************************************/ |
||
134 | |||
135 | /** |
||
136 | * @inheritDoc |
||
137 | */ |
||
138 | 6 | public static function __set_state(array $interval) |
|
139 | { |
||
140 | 6 | return new self($interval['startDate'], $interval['endDate'], $interval['boundaryType'] ?? self::INCLUDE_START_EXCLUDE_END); |
|
141 | } |
||
142 | |||
143 | /** |
||
144 | * Creates new instance from a starting datepoint and a duration. |
||
145 | * |
||
146 | * @param mixed $startDate the starting datepoint |
||
147 | * @param mixed $duration a Duration |
||
148 | */ |
||
149 | 132 | public static function after($startDate, $duration, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self |
|
150 | { |
||
151 | 132 | $startDate = self::filterDatepoint($startDate); |
|
152 | |||
153 | 132 | return new self($startDate, $startDate->add(self::filterDuration($duration)), $boundaryType); |
|
154 | } |
||
155 | |||
156 | /** |
||
157 | * Creates new instance from a ending datepoint and a duration. |
||
158 | * |
||
159 | * @param mixed $endDate the ending datepoint |
||
160 | * @param mixed $duration a Duration |
||
161 | */ |
||
162 | 27 | public static function before($endDate, $duration, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self |
|
163 | { |
||
164 | 27 | $endDate = self::filterDatepoint($endDate); |
|
165 | |||
166 | 27 | return new self($endDate->sub(self::filterDuration($duration)), $endDate, $boundaryType); |
|
167 | } |
||
168 | |||
169 | /** |
||
170 | * Creates new instance where the given duration is simultaneously |
||
171 | * subtracted from and added to the datepoint. |
||
172 | * |
||
173 | * @param mixed $datepoint a Datepoint |
||
174 | * @param mixed $duration a Duration |
||
175 | */ |
||
176 | 24 | public static function around($datepoint, $duration, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self |
|
177 | { |
||
178 | 24 | $datepoint = self::filterDatepoint($datepoint); |
|
179 | 24 | $duration = self::filterDuration($duration); |
|
180 | |||
181 | 24 | return new self($datepoint->sub($duration), $datepoint->add($duration), $boundaryType); |
|
182 | } |
||
183 | |||
184 | /** |
||
185 | * Creates new instance from a DatePeriod. |
||
186 | */ |
||
187 | 12 | public static function fromDatePeriod(DatePeriod $datePeriod, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self |
|
188 | { |
||
189 | 12 | return new self($datePeriod->getStartDate(), $datePeriod->getEndDate(), $boundaryType); |
|
190 | } |
||
191 | |||
192 | /** |
||
193 | * Creates new instance for a specific year. |
||
194 | */ |
||
195 | 12 | public static function fromYear(int $year, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self |
|
196 | { |
||
197 | 12 | $startDate = (new DateTimeImmutable())->setDate($year, 1, 1)->setTime(0, 0); |
|
198 | |||
199 | 12 | return new self($startDate, $startDate->add(new DateInterval('P1Y')), $boundaryType); |
|
200 | } |
||
201 | |||
202 | /** |
||
203 | * Creates new instance for a specific ISO year. |
||
204 | */ |
||
205 | 6 | public static function fromIsoYear(int $year, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self |
|
206 | { |
||
207 | 6 | return new self( |
|
208 | 6 | (new DateTimeImmutable())->setISODate($year, 1)->setTime(0, 0), |
|
209 | 6 | (new DateTimeImmutable())->setISODate(++$year, 1)->setTime(0, 0), |
|
210 | 2 | $boundaryType |
|
211 | ); |
||
212 | } |
||
213 | |||
214 | /** |
||
215 | * Creates new instance for a specific year and semester. |
||
216 | */ |
||
217 | 18 | public static function fromSemester(int $year, int $semester = 1, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self |
|
218 | { |
||
219 | 18 | $month = (($semester - 1) * 6) + 1; |
|
220 | 18 | $startDate = (new DateTimeImmutable())->setDate($year, $month, 1)->setTime(0, 0); |
|
221 | |||
222 | 18 | return new self($startDate, $startDate->add(new DateInterval('P6M')), $boundaryType); |
|
223 | } |
||
224 | |||
225 | /** |
||
226 | * Creates new instance for a specific year and quarter. |
||
227 | */ |
||
228 | 18 | public static function fromQuarter(int $year, int $quarter = 1, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self |
|
229 | { |
||
230 | 18 | $month = (($quarter - 1) * 3) + 1; |
|
231 | 18 | $startDate = (new DateTimeImmutable())->setDate($year, $month, 1)->setTime(0, 0); |
|
232 | |||
233 | 18 | return new self($startDate, $startDate->add(new DateInterval('P3M')), $boundaryType); |
|
234 | } |
||
235 | |||
236 | /** |
||
237 | * Creates new instance for a specific year and month. |
||
238 | */ |
||
239 | 75 | public static function fromMonth(int $year, int $month = 1, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self |
|
240 | { |
||
241 | 75 | $startDate = (new DateTimeImmutable())->setDate($year, $month, 1)->setTime(0, 0); |
|
242 | |||
243 | 75 | return new self($startDate, $startDate->add(new DateInterval('P1M')), $boundaryType); |
|
244 | } |
||
245 | |||
246 | /** |
||
247 | * Creates new instance for a specific ISO8601 week. |
||
248 | */ |
||
249 | 21 | public static function fromIsoWeek(int $year, int $week = 1, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self |
|
250 | { |
||
251 | 21 | $startDate = (new DateTimeImmutable())->setISODate($year, $week, 1)->setTime(0, 0); |
|
252 | |||
253 | 21 | return new self($startDate, $startDate->add(new DateInterval('P7D')), $boundaryType); |
|
254 | } |
||
255 | |||
256 | /** |
||
257 | * Creates new instance for a specific year, month and day. |
||
258 | */ |
||
259 | 54 | public static function fromDay(int $year, int $month = 1, int $day = 1, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self |
|
260 | { |
||
261 | 54 | $startDate = (new DateTimeImmutable())->setDate($year, $month, $day)->setTime(0, 0); |
|
262 | |||
263 | 54 | return new self($startDate, $startDate->add(new DateInterval('P1D')), $boundaryType); |
|
264 | } |
||
265 | |||
266 | /** |
||
267 | * Creates new instance for Datepoint. |
||
268 | */ |
||
269 | 3 | public static function fromDatepoint(DateTimeInterface $startDate, DateTimeInterface $endDate, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self |
|
270 | { |
||
271 | 3 | return new self($startDate, $endDate, $boundaryType); |
|
272 | } |
||
273 | |||
274 | /************************************************** |
||
275 | * Basic getters |
||
276 | **************************************************/ |
||
277 | |||
278 | /** |
||
279 | * Returns the starting datepoint. |
||
280 | */ |
||
281 | 243 | public function getStartDate(): DateTimeImmutable |
|
282 | { |
||
283 | 243 | return $this->startDate; |
|
284 | } |
||
285 | |||
286 | /** |
||
287 | * Returns the ending datepoint. |
||
288 | */ |
||
289 | 219 | public function getEndDate(): DateTimeImmutable |
|
290 | { |
||
291 | 219 | return $this->endDate; |
|
292 | } |
||
293 | |||
294 | /** |
||
295 | * Returns the instance boundary type. |
||
296 | */ |
||
297 | 159 | public function getBoundaryType(): string |
|
298 | { |
||
299 | 159 | return $this->boundaryType; |
|
300 | } |
||
301 | |||
302 | /** |
||
303 | * Returns the instance duration as expressed in seconds. |
||
304 | */ |
||
305 | 36 | public function getTimestampInterval(): float |
|
306 | { |
||
307 | 36 | return $this->endDate->getTimestamp() - $this->startDate->getTimestamp(); |
|
308 | } |
||
309 | |||
310 | /** |
||
311 | * Returns the instance duration as a DateInterval object. |
||
312 | */ |
||
313 | 129 | public function getDateInterval(): DateInterval |
|
314 | { |
||
315 | 129 | return $this->startDate->diff($this->endDate); |
|
316 | } |
||
317 | |||
318 | /************************************************** |
||
319 | * String representation |
||
320 | **************************************************/ |
||
321 | |||
322 | /** |
||
323 | * Returns the string representation as a ISO8601 interval format. |
||
324 | * |
||
325 | * @deprecated since version 4.10 |
||
326 | * @see ::toIso8601() |
||
327 | */ |
||
328 | 6 | public function __toString() |
|
329 | { |
||
330 | 6 | return $this->toIso8601(); |
|
331 | } |
||
332 | |||
333 | /** |
||
334 | * Returns the string representation as a ISO8601 interval format. |
||
335 | * |
||
336 | * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals |
||
337 | * @param ?string $format |
||
338 | */ |
||
339 | 18 | public function toIso8601(?string $format = null): string |
|
340 | { |
||
341 | 18 | $utc = new DateTimeZone('UTC'); |
|
342 | 18 | $format = $format ?? self::ISO8601_FORMAT; |
|
343 | |||
344 | 18 | $startDate = $this->startDate->setTimezone($utc)->format($format); |
|
345 | 18 | $endDate = $this->endDate->setTimezone($utc)->format($format); |
|
346 | |||
347 | 18 | return $startDate.'/'.$endDate; |
|
348 | } |
||
349 | |||
350 | /** |
||
351 | * Returns the JSON representation of an instance. |
||
352 | * |
||
353 | * Based on the JSON representation of dates as |
||
354 | * returned by Javascript Date.toJSON() method. |
||
355 | * |
||
356 | * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toJSON |
||
357 | * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString |
||
358 | * |
||
359 | * @return array<string> |
||
360 | */ |
||
361 | 12 | public function jsonSerialize() |
|
362 | { |
||
363 | 12 | [$startDate, $endDate] = explode('/', $this->toIso8601(), 2); |
|
364 | |||
365 | 12 | return ['startDate' => $startDate, 'endDate' => $endDate]; |
|
366 | } |
||
367 | |||
368 | /** |
||
369 | * Returns the mathematical representation of an instance as a left close, right open interval. |
||
370 | * |
||
371 | * @see https://en.wikipedia.org/wiki/Interval_(mathematics)#Notations_for_intervals |
||
372 | * @see https://php.net/manual/en/function.date.php |
||
373 | * @see https://www.postgresql.org/docs/9.3/static/rangetypes.html |
||
374 | * |
||
375 | * @param string $format the format of the outputted date string |
||
376 | */ |
||
377 | 24 | public function format(string $format): string |
|
378 | { |
||
379 | 24 | return $this->boundaryType[0] |
|
380 | 24 | .$this->startDate->format($format) |
|
381 | 24 | .', ' |
|
382 | 24 | .$this->endDate->format($format) |
|
383 | 24 | .$this->boundaryType[1]; |
|
384 | } |
||
385 | |||
386 | /************************************************** |
||
387 | * Boundary related methods |
||
388 | **************************************************/ |
||
389 | |||
390 | /** |
||
391 | * Tells whether the start datepoint is included in the boundary. |
||
392 | */ |
||
393 | 12 | public function isStartIncluded(): bool |
|
394 | { |
||
395 | 12 | return '[' === $this->boundaryType[0]; |
|
396 | } |
||
397 | |||
398 | /** |
||
399 | * Tells whether the start datepoint is excluded from the boundary. |
||
400 | */ |
||
401 | 81 | public function isStartExcluded(): bool |
|
402 | { |
||
403 | 81 | return '(' === $this->boundaryType[0]; |
|
404 | } |
||
405 | |||
406 | /** |
||
407 | * Tells whether the end datepoint is included in the boundary. |
||
408 | */ |
||
409 | 12 | public function isEndIncluded(): bool |
|
410 | { |
||
411 | 12 | return ']' === $this->boundaryType[1]; |
|
412 | } |
||
413 | |||
414 | /** |
||
415 | * Tells whether the end datepoint is excluded from the boundary. |
||
416 | */ |
||
417 | 81 | public function isEndExcluded(): bool |
|
418 | { |
||
419 | 81 | return ')' === $this->boundaryType[1]; |
|
420 | } |
||
421 | |||
422 | /************************************************** |
||
423 | * Duration comparison methods |
||
424 | **************************************************/ |
||
425 | |||
426 | /** |
||
427 | * Compares two instances according to their duration. |
||
428 | * |
||
429 | * Returns: |
||
430 | * <ul> |
||
431 | * <li> -1 if the current Interval is lesser than the submitted Interval object</li> |
||
432 | * <li> 1 if the current Interval is greater than the submitted Interval object</li> |
||
433 | * <li> 0 if both Interval objects have the same duration</li> |
||
434 | * </ul> |
||
435 | */ |
||
436 | 60 | public function durationCompare(self $interval): int |
|
437 | { |
||
438 | 60 | return $this->startDate->add($this->getDateInterval()) |
|
439 | 60 | <=> $this->startDate->add($interval->getDateInterval()); |
|
440 | } |
||
441 | |||
442 | /** |
||
443 | * Tells whether the current instance duration is equal to the submitted one. |
||
444 | */ |
||
445 | 6 | public function durationEquals(self $interval): bool |
|
446 | { |
||
447 | 6 | return 0 === $this->durationCompare($interval); |
|
448 | } |
||
449 | |||
450 | /** |
||
451 | * Tells whether the current instance duration is greater than the submitted one. |
||
452 | */ |
||
453 | 18 | public function durationGreaterThan(self $interval): bool |
|
454 | { |
||
455 | 18 | return 1 === $this->durationCompare($interval); |
|
456 | } |
||
457 | |||
458 | /** |
||
459 | * Tells whether the current instance duration is less than the submitted one. |
||
460 | */ |
||
461 | 12 | public function durationLessThan(self $interval): bool |
|
462 | { |
||
463 | 12 | return -1 === $this->durationCompare($interval); |
|
464 | } |
||
465 | |||
466 | /************************************************** |
||
467 | * Relation methods |
||
468 | **************************************************/ |
||
469 | |||
470 | /** |
||
471 | * Tells whether an instance is entirely before the specified index. |
||
472 | * |
||
473 | * The index can be a DateTimeInterface object or another Period object. |
||
474 | * |
||
475 | * [--------------------) |
||
476 | * [--------------------) |
||
477 | * |
||
478 | * @param mixed $index a datepoint or a Period object |
||
479 | */ |
||
480 | 90 | public function isBefore($index): bool |
|
481 | { |
||
482 | 90 | if ($index instanceof self) { |
|
483 | 48 | return $this->endDate < $index->startDate |
|
484 | 48 | || ($this->endDate == $index->startDate && $this->boundaryType[1] !== $index->boundaryType[0]); |
|
485 | } |
||
486 | |||
487 | 42 | $datepoint = self::filterDatepoint($index); |
|
488 | |||
489 | 42 | return $this->endDate < $datepoint |
|
490 | 42 | || ($this->endDate == $datepoint && ')' === $this->boundaryType[1]); |
|
491 | } |
||
492 | |||
493 | /** |
||
494 | * Tells whether the current instance end date meets the interval start date. |
||
495 | * |
||
496 | * [--------------------) |
||
497 | * [--------------------) |
||
498 | */ |
||
499 | 309 | public function bordersOnStart(self $interval): bool |
|
500 | { |
||
501 | 309 | return $this->endDate == $interval->startDate |
|
502 | 309 | && '][' !== $this->boundaryType[1].$interval->boundaryType[0]; |
|
503 | } |
||
504 | |||
505 | /** |
||
506 | * Tells whether two intervals share the same start datepoint |
||
507 | * and the same starting boundary type. |
||
508 | * |
||
509 | * [----------) |
||
510 | * [--------------------) |
||
511 | * |
||
512 | * or |
||
513 | * |
||
514 | * [--------------------) |
||
515 | * [---------) |
||
516 | * |
||
517 | * @param mixed $index a datepoint or a Period object |
||
518 | */ |
||
519 | 27 | public function isStartedBy($index): bool |
|
520 | { |
||
521 | 27 | if ($index instanceof self) { |
|
522 | 15 | return $this->startDate == $index->startDate |
|
523 | 15 | && $this->boundaryType[0] === $index->boundaryType[0]; |
|
524 | } |
||
525 | |||
526 | 12 | $index = self::filterDatepoint($index); |
|
527 | |||
528 | 12 | return $index == $this->startDate && '[' === $this->boundaryType[0]; |
|
529 | } |
||
530 | |||
531 | /** |
||
532 | * Tells whether an instance is fully contained in the specified interval. |
||
533 | * |
||
534 | * [----------) |
||
535 | * [--------------------) |
||
536 | */ |
||
537 | 39 | public function isDuring(self $interval): bool |
|
538 | { |
||
539 | 39 | return $interval->containsInterval($this); |
|
540 | } |
||
541 | |||
542 | /** |
||
543 | * Tells whether an instance fully contains the specified index. |
||
544 | * |
||
545 | * The index can be a DateTimeInterface object or another Period object. |
||
546 | * |
||
547 | * @param mixed $index a datepoint or a Period object |
||
548 | */ |
||
549 | 144 | public function contains($index): bool |
|
550 | { |
||
551 | 144 | if ($index instanceof self) { |
|
552 | 66 | return $this->containsInterval($index); |
|
553 | } |
||
554 | |||
555 | 78 | return $this->containsDatepoint(self::filterDatepoint($index), $this->boundaryType); |
|
556 | } |
||
557 | |||
558 | /** |
||
559 | * Tells whether an instance fully contains another instance. |
||
560 | * |
||
561 | * [--------------------) |
||
562 | * [----------) |
||
563 | */ |
||
564 | 66 | private function containsInterval(self $interval): bool |
|
565 | { |
||
566 | 66 | if ($this->startDate < $interval->startDate && $this->endDate > $interval->endDate) { |
|
567 | 18 | return true; |
|
568 | } |
||
569 | |||
570 | 63 | if ($this->startDate == $interval->startDate && $this->endDate == $interval->endDate) { |
|
571 | 21 | return $this->boundaryType === $interval->boundaryType || '[]' === $this->boundaryType; |
|
572 | } |
||
573 | |||
574 | 42 | if ($this->startDate == $interval->startDate) { |
|
575 | 12 | return ($this->boundaryType[0] === $interval->boundaryType[0] || '[' === $this->boundaryType[0]) |
|
576 | 12 | && $this->containsDatepoint($this->startDate->add($interval->getDateInterval()), $this->boundaryType); |
|
577 | } |
||
578 | |||
579 | 30 | if ($this->endDate == $interval->endDate) { |
|
580 | 18 | return ($this->boundaryType[1] === $interval->boundaryType[1] || ']' === $this->boundaryType[1]) |
|
581 | 18 | && $this->containsDatepoint($this->endDate->sub($interval->getDateInterval()), $this->boundaryType); |
|
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
582 | } |
||
583 | |||
584 | 12 | return false; |
|
585 | } |
||
586 | |||
587 | /** |
||
588 | * Tells whether an instance contains a datepoint. |
||
589 | * |
||
590 | * [------|------------) |
||
591 | */ |
||
592 | 108 | private function containsDatepoint(DateTimeInterface $datepoint, string $boundaryType): bool |
|
593 | { |
||
594 | switch ($boundaryType) { |
||
595 | 108 | case self::EXCLUDE_ALL: |
|
596 | 9 | return $datepoint > $this->startDate && $datepoint < $this->endDate; |
|
597 | 99 | case self::INCLUDE_ALL: |
|
598 | 3 | return $datepoint >= $this->startDate && $datepoint <= $this->endDate; |
|
599 | 96 | case self::EXCLUDE_START_INCLUDE_END: |
|
600 | 9 | return $datepoint > $this->startDate && $datepoint <= $this->endDate; |
|
601 | 87 | case self::INCLUDE_START_EXCLUDE_END: |
|
602 | default: |
||
603 | 87 | return $datepoint >= $this->startDate && $datepoint < $this->endDate; |
|
604 | } |
||
605 | } |
||
606 | |||
607 | /** |
||
608 | * Tells whether two intervals share the same datepoints. |
||
609 | * |
||
610 | * [--------------------) |
||
611 | * [--------------------) |
||
612 | */ |
||
613 | 288 | public function equals(self $interval): bool |
|
614 | { |
||
615 | 288 | return $this->startDate == $interval->startDate |
|
616 | 288 | && $this->endDate == $interval->endDate |
|
617 | 288 | && $this->boundaryType === $interval->boundaryType; |
|
618 | } |
||
619 | |||
620 | /** |
||
621 | * Tells whether two intervals share the same end datepoint |
||
622 | * and the same ending boundary type. |
||
623 | * |
||
624 | * [----------) |
||
625 | * [--------------------) |
||
626 | * |
||
627 | * or |
||
628 | * |
||
629 | * [--------------------) |
||
630 | * [---------) |
||
631 | * |
||
632 | * @param mixed $index a datepoint or a Period object |
||
633 | */ |
||
634 | 24 | public function isEndedBy($index): bool |
|
635 | { |
||
636 | 24 | if ($index instanceof self) { |
|
637 | 12 | return $this->endDate == $index->endDate |
|
638 | 12 | && $this->boundaryType[1] === $index->boundaryType[1]; |
|
639 | } |
||
640 | |||
641 | 12 | $index = self::filterDatepoint($index); |
|
642 | |||
643 | 12 | return $index == $this->endDate && ']' === $this->boundaryType[1]; |
|
644 | } |
||
645 | |||
646 | /** |
||
647 | * Tells whether the current instance start date meets the interval end date. |
||
648 | * |
||
649 | * [--------------------) |
||
650 | * [--------------------) |
||
651 | */ |
||
652 | 291 | public function bordersOnEnd(self $interval): bool |
|
653 | { |
||
654 | 291 | return $interval->bordersOnStart($this); |
|
655 | } |
||
656 | |||
657 | /** |
||
658 | * Tells whether an interval is entirely after the specified index. |
||
659 | * The index can be a DateTimeInterface object or another Period object. |
||
660 | * |
||
661 | * [--------------------) |
||
662 | * [--------------------) |
||
663 | * |
||
664 | * @param mixed $index a datepoint or a Period object |
||
665 | */ |
||
666 | 54 | public function isAfter($index): bool |
|
667 | { |
||
668 | 54 | if ($index instanceof self) { |
|
669 | 24 | return $index->isBefore($this); |
|
670 | } |
||
671 | |||
672 | 30 | $datepoint = self::filterDatepoint($index); |
|
673 | 30 | return $this->startDate > $datepoint |
|
674 | 30 | || ($this->startDate == $datepoint && '(' === $this->boundaryType[0]); |
|
675 | } |
||
676 | |||
677 | /** |
||
678 | * Tells whether two intervals abuts. |
||
679 | * |
||
680 | * [--------------------) |
||
681 | * [--------------------) |
||
682 | * or |
||
683 | * [--------------------) |
||
684 | * [--------------------) |
||
685 | */ |
||
686 | 309 | public function abuts(self $interval): bool |
|
687 | { |
||
688 | 309 | return $this->bordersOnStart($interval) || $this->bordersOnEnd($interval); |
|
689 | } |
||
690 | |||
691 | /** |
||
692 | * Tells whether two intervals overlaps. |
||
693 | * |
||
694 | * [--------------------) |
||
695 | * [--------------------) |
||
696 | */ |
||
697 | 291 | public function overlaps(self $interval): bool |
|
698 | { |
||
699 | 291 | return !$this->abuts($interval) |
|
700 | 291 | && $this->startDate < $interval->endDate |
|
701 | 291 | && $this->endDate > $interval->startDate; |
|
702 | } |
||
703 | |||
704 | /************************************************** |
||
705 | * Manipulating instance duration |
||
706 | **************************************************/ |
||
707 | |||
708 | /** |
||
709 | * Returns the difference between two instances expressed in seconds. |
||
710 | */ |
||
711 | 6 | public function timestampIntervalDiff(self $interval): float |
|
712 | { |
||
713 | 6 | return $this->getTimestampInterval() - $interval->getTimestampInterval(); |
|
714 | } |
||
715 | |||
716 | /** |
||
717 | * Returns the difference between two instances expressed with a DateInterval object. |
||
718 | */ |
||
719 | 12 | public function dateIntervalDiff(self $interval): DateInterval |
|
720 | { |
||
721 | 12 | return $this->endDate->diff($this->startDate->add($interval->getDateInterval())); |
|
722 | } |
||
723 | |||
724 | /** |
||
725 | * Allows iteration over a set of dates and times, |
||
726 | * recurring at regular intervals, over the instance. |
||
727 | * |
||
728 | * @see http://php.net/manual/en/dateperiod.construct.php |
||
729 | * |
||
730 | * @param mixed $duration a Duration |
||
731 | */ |
||
732 | 54 | public function getDatePeriod($duration, int $option = 0): DatePeriod |
|
733 | { |
||
734 | 54 | return new DatePeriod($this->startDate, self::filterDuration($duration), $this->endDate, $option); |
|
735 | } |
||
736 | |||
737 | /** |
||
738 | * Allows iteration over a set of dates and times, |
||
739 | * recurring at regular intervals, over the instance backwards starting from |
||
740 | * the instance ending datepoint. |
||
741 | * |
||
742 | * @param mixed $duration a Duration |
||
743 | */ |
||
744 | 24 | public function getDatePeriodBackwards($duration, int $option = 0): iterable |
|
745 | { |
||
746 | 24 | $duration = self::filterDuration($duration); |
|
747 | 24 | $date = $this->endDate; |
|
748 | 24 | if ((bool) ($option & DatePeriod::EXCLUDE_START_DATE)) { |
|
749 | 12 | $date = $this->endDate->sub($duration); |
|
750 | } |
||
751 | |||
752 | 24 | while ($date > $this->startDate) { |
|
753 | 24 | yield $date; |
|
754 | 24 | $date = $date->sub($duration); |
|
755 | } |
||
756 | 24 | } |
|
757 | |||
758 | /** |
||
759 | * Allows splitting an instance in smaller Period objects according to a given interval. |
||
760 | * |
||
761 | * The returned iterable Interval set is ordered so that: |
||
762 | * <ul> |
||
763 | * <li>The first returned object MUST share the starting datepoint of the parent object.</li> |
||
764 | * <li>The last returned object MUST share the ending datepoint of the parent object.</li> |
||
765 | * <li>The last returned object MUST have a duration equal or lesser than the submitted interval.</li> |
||
766 | * <li>All returned objects except for the first one MUST start immediately after the previously returned object</li> |
||
767 | * </ul> |
||
768 | * |
||
769 | * @param mixed $duration a Duration |
||
770 | * |
||
771 | * @return iterable<Period> |
||
772 | */ |
||
773 | 30 | public function split($duration): iterable |
|
774 | { |
||
775 | 30 | $duration = self::filterDuration($duration); |
|
776 | 30 | foreach ($this->getDatePeriod($duration) as $startDate) { |
|
777 | 30 | $endDate = $startDate->add($duration); |
|
778 | 30 | if ($endDate > $this->endDate) { |
|
779 | 12 | $endDate = $this->endDate; |
|
780 | } |
||
781 | |||
782 | 30 | yield new self($startDate, $endDate, $this->boundaryType); |
|
783 | } |
||
784 | 30 | } |
|
785 | |||
786 | /** |
||
787 | * Allows splitting an instance in smaller Period objects according to a given interval. |
||
788 | * |
||
789 | * The returned iterable Period set is ordered so that: |
||
790 | * <ul> |
||
791 | * <li>The first returned object MUST share the ending datepoint of the parent object.</li> |
||
792 | * <li>The last returned object MUST share the starting datepoint of the parent object.</li> |
||
793 | * <li>The last returned object MUST have a duration equal or lesser than the submitted interval.</li> |
||
794 | * <li>All returned objects except for the first one MUST end immediately before the previously returned object</li> |
||
795 | * </ul> |
||
796 | * |
||
797 | * @param mixed $duration a Duration |
||
798 | * |
||
799 | * @return iterable<Period> |
||
800 | */ |
||
801 | 18 | public function splitBackwards($duration): iterable |
|
802 | { |
||
803 | 18 | $endDate = $this->endDate; |
|
804 | 18 | $duration = self::filterDuration($duration); |
|
805 | do { |
||
806 | 18 | $startDate = $endDate->sub($duration); |
|
807 | 18 | if ($startDate < $this->startDate) { |
|
808 | 6 | $startDate = $this->startDate; |
|
809 | } |
||
810 | 18 | yield new self($startDate, $endDate, $this->boundaryType); |
|
811 | |||
812 | 18 | $endDate = $startDate; |
|
813 | 18 | } while ($endDate > $this->startDate); |
|
814 | 18 | } |
|
815 | |||
816 | /************************************************** |
||
817 | * Manipulation instance endpoints and boundaries |
||
818 | **************************************************/ |
||
819 | |||
820 | /** |
||
821 | * Returns the computed intersection between two instances as a new instance. |
||
822 | * |
||
823 | * [--------------------) |
||
824 | * ∩ |
||
825 | * [----------) |
||
826 | * = |
||
827 | * [----) |
||
828 | * |
||
829 | * @throws Exception If both objects do not overlaps |
||
830 | */ |
||
831 | 153 | public function intersect(self $interval): self |
|
832 | { |
||
833 | 153 | if (!$this->overlaps($interval)) { |
|
834 | 12 | throw new Exception('Both '.self::class.' objects should overlaps'); |
|
835 | } |
||
836 | |||
837 | 141 | $startDate = $this->startDate; |
|
838 | 141 | $endDate = $this->endDate; |
|
839 | 141 | $boundaryType = $this->boundaryType; |
|
840 | 141 | if ($interval->startDate > $this->startDate) { |
|
841 | 132 | $boundaryType[0] = $interval->boundaryType[0]; |
|
842 | 132 | $startDate = $interval->startDate; |
|
843 | } |
||
844 | |||
845 | 141 | if ($interval->endDate < $this->endDate) { |
|
846 | 33 | $boundaryType[1] = $interval->boundaryType[1]; |
|
847 | 33 | $endDate = $interval->endDate; |
|
848 | } |
||
849 | |||
850 | 141 | $intersect = new self($startDate, $endDate, $boundaryType); |
|
851 | 141 | if ($intersect->equals($this)) { |
|
852 | 21 | return $this; |
|
853 | } |
||
854 | |||
855 | 138 | return $intersect; |
|
856 | } |
||
857 | |||
858 | /** |
||
859 | * Returns the computed difference between two overlapping instances as |
||
860 | * an array containing Period objects or the null value. |
||
861 | * |
||
862 | * The array will always contains 2 elements: |
||
863 | * |
||
864 | * <ul> |
||
865 | * <li>an NULL filled array if both objects have the same datepoints</li> |
||
866 | * <li>one Period object and NULL if both objects share one datepoint</li> |
||
867 | * <li>two Period objects if both objects share no datepoint</li> |
||
868 | * </ul> |
||
869 | * |
||
870 | * [--------------------) |
||
871 | * \ |
||
872 | * [-----------) |
||
873 | * = |
||
874 | * [--------------) + [-----) |
||
875 | * |
||
876 | * @return array<null|Period> |
||
877 | */ |
||
878 | 99 | public function diff(self $interval): array |
|
879 | { |
||
880 | 99 | if ($interval->equals($this)) { |
|
881 | 12 | return [null, null]; |
|
882 | } |
||
883 | |||
884 | 87 | $intersect = $this->intersect($interval); |
|
885 | 81 | $merge = $this->merge($interval); |
|
886 | 81 | if ($merge->startDate == $intersect->startDate) { |
|
887 | 9 | $first = ')' === $intersect->boundaryType[1] ? '[' : '('; |
|
888 | 9 | $boundary = $first.$merge->boundaryType[1]; |
|
889 | |||
890 | 9 | return [$merge->startingOn($intersect->endDate)->withBoundaryType($boundary), null]; |
|
891 | } |
||
892 | |||
893 | 75 | if ($merge->endDate == $intersect->endDate) { |
|
894 | 6 | $last = '(' === $intersect->boundaryType[0] ? ']' : ')'; |
|
895 | 6 | $boundary = $merge->boundaryType[0].$last; |
|
896 | |||
897 | 6 | return [$merge->endingOn($intersect->startDate)->withBoundaryType($boundary), null]; |
|
898 | } |
||
899 | |||
900 | 69 | $last = '(' === $intersect->boundaryType[0] ? ']' : ')'; |
|
901 | 69 | $lastBoundary = $merge->boundaryType[0].$last; |
|
902 | |||
903 | 69 | $first = ')' === $intersect->boundaryType[1] ? '[' : '('; |
|
904 | 69 | $firstBoundary = $first.$merge->boundaryType[1]; |
|
905 | |||
906 | return [ |
||
907 | 69 | $merge->endingOn($intersect->startDate)->withBoundaryType($lastBoundary), |
|
908 | 69 | $merge->startingOn($intersect->endDate)->withBoundaryType($firstBoundary), |
|
909 | ]; |
||
910 | } |
||
911 | |||
912 | /** |
||
913 | * DEPRECATION WARNING! This method will be removed in the next major point release. |
||
914 | * |
||
915 | * @deprecated since version 4.9.0 |
||
916 | * @see ::subtract |
||
917 | */ |
||
918 | 3 | public function substract(self $interval): Sequence |
|
919 | { |
||
920 | 3 | return $this->subtract($interval); |
|
921 | } |
||
922 | |||
923 | /** |
||
924 | * Returns the difference set operation between two intervals as a Sequence. |
||
925 | * The Sequence can contain from 0 to 2 Periods depending on the result of |
||
926 | * the operation. |
||
927 | * |
||
928 | * [--------------------) |
||
929 | * - |
||
930 | * [-----------) |
||
931 | * = |
||
932 | * [--------------) |
||
933 | */ |
||
934 | 24 | public function subtract(self $interval): Sequence |
|
935 | { |
||
936 | 24 | if (!$this->overlaps($interval)) { |
|
937 | 9 | return new Sequence($this); |
|
938 | } |
||
939 | |||
940 | $filter = function ($item): bool { |
||
941 | 18 | return null !== $item && $this->overlaps($item); |
|
942 | 18 | }; |
|
943 | |||
944 | 18 | return new Sequence(...array_filter($this->diff($interval), $filter)); |
|
945 | } |
||
946 | |||
947 | /** |
||
948 | * Returns the computed gap between two instances as a new instance. |
||
949 | * |
||
950 | * [--------------------) |
||
951 | * + |
||
952 | * [----------) |
||
953 | * = |
||
954 | * [---) |
||
955 | * |
||
956 | * @throws Exception If both instance overlaps |
||
957 | */ |
||
958 | 84 | public function gap(self $interval): self |
|
959 | { |
||
960 | 84 | if ($this->overlaps($interval)) { |
|
961 | 18 | throw new Exception('Both '.self::class.' objects must not overlaps'); |
|
962 | } |
||
963 | |||
964 | 66 | $boundaryType = $this->isEndExcluded() ? '[' : '('; |
|
965 | 66 | $boundaryType .= $interval->isStartExcluded() ? ']' : ')'; |
|
966 | 66 | if ($interval->startDate > $this->startDate) { |
|
967 | 66 | return new self($this->endDate, $interval->startDate, $boundaryType); |
|
968 | } |
||
969 | |||
970 | 6 | return new self($interval->endDate, $this->startDate, $this->boundaryType); |
|
971 | } |
||
972 | |||
973 | /** |
||
974 | * Merges one or more instances to return a new instance. |
||
975 | * The resulting instance represents the largest duration possible. |
||
976 | * |
||
977 | * This method MUST retain the state of the current instance, and return |
||
978 | * an instance that contains the specified new datepoints. |
||
979 | * |
||
980 | * [--------------------) |
||
981 | * + |
||
982 | * [----------) |
||
983 | * = |
||
984 | * [--------------------------) |
||
985 | * |
||
986 | * |
||
987 | * @param Period ...$intervals |
||
988 | */ |
||
989 | 132 | public function merge(self ...$intervals): self |
|
990 | { |
||
991 | 132 | $carry = $this; |
|
992 | 132 | foreach ($intervals as $period) { |
|
993 | 126 | if ($carry->startDate > $period->startDate) { |
|
994 | 33 | $carry = new self( |
|
995 | 33 | $period->startDate, |
|
996 | 33 | $carry->endDate, |
|
997 | 33 | $period->boundaryType[0].$carry->boundaryType[1] |
|
998 | ); |
||
999 | } |
||
1000 | |||
1001 | 126 | if ($carry->endDate < $period->endDate) { |
|
1002 | 108 | $carry = new self( |
|
1003 | 108 | $carry->startDate, |
|
1004 | 108 | $period->endDate, |
|
1005 | 108 | $carry->boundaryType[0].$period->boundaryType[1] |
|
1006 | ); |
||
1007 | } |
||
1008 | } |
||
1009 | |||
1010 | 132 | return $carry; |
|
1011 | } |
||
1012 | |||
1013 | |||
1014 | /************************************************** |
||
1015 | * Mutation methods |
||
1016 | **************************************************/ |
||
1017 | |||
1018 | /** |
||
1019 | * Returns an instance with the specified starting datepoint. |
||
1020 | * |
||
1021 | * This method MUST retain the state of the current instance, and return |
||
1022 | * an instance that contains the specified starting datepoint. |
||
1023 | * |
||
1024 | * @param mixed $startDate the new starting datepoint |
||
1025 | */ |
||
1026 | 120 | public function startingOn($startDate): self |
|
1027 | { |
||
1028 | 120 | $startDate = self::filterDatepoint($startDate); |
|
1029 | 120 | if ($startDate == $this->startDate) { |
|
1030 | 6 | return $this; |
|
1031 | } |
||
1032 | |||
1033 | 120 | return new self($startDate, $this->endDate, $this->boundaryType); |
|
1034 | } |
||
1035 | |||
1036 | /** |
||
1037 | * Returns an instance with the specified ending datepoint. |
||
1038 | * |
||
1039 | * This method MUST retain the state of the current instance, and return |
||
1040 | * an instance that contains the specified ending datepoint. |
||
1041 | * |
||
1042 | * @param mixed $endDate the new ending datepoint |
||
1043 | */ |
||
1044 | 120 | public function endingOn($endDate): self |
|
1045 | { |
||
1046 | 120 | $endDate = self::filterDatepoint($endDate); |
|
1047 | 120 | if ($endDate == $this->endDate) { |
|
1048 | 6 | return $this; |
|
1049 | } |
||
1050 | |||
1051 | 120 | return new self($this->startDate, $endDate, $this->boundaryType); |
|
1052 | } |
||
1053 | |||
1054 | /** |
||
1055 | * Returns an instance with the specified boundary type. |
||
1056 | * |
||
1057 | * This method MUST retain the state of the current instance, and return |
||
1058 | * an instance with the specified range type. |
||
1059 | */ |
||
1060 | 87 | public function withBoundaryType(string $boundaryType): self |
|
1061 | { |
||
1062 | 87 | if ($boundaryType === $this->boundaryType) { |
|
1063 | 72 | return $this; |
|
1064 | } |
||
1065 | |||
1066 | 45 | return new self($this->startDate, $this->endDate, $boundaryType); |
|
1067 | } |
||
1068 | |||
1069 | /** |
||
1070 | * Returns a new instance with a new ending datepoint. |
||
1071 | * |
||
1072 | * This method MUST retain the state of the current instance, and return |
||
1073 | * an instance that contains the specified ending datepoint. |
||
1074 | * |
||
1075 | * @param mixed $duration a Duration |
||
1076 | */ |
||
1077 | 12 | public function withDurationAfterStart($duration): self |
|
1078 | { |
||
1079 | 12 | return $this->endingOn($this->startDate->add(self::filterDuration($duration))); |
|
1080 | } |
||
1081 | |||
1082 | /** |
||
1083 | * Returns a new instance with a new starting datepoint. |
||
1084 | * |
||
1085 | * This method MUST retain the state of the current instance, and return |
||
1086 | * an instance that contains the specified starting datepoint. |
||
1087 | * |
||
1088 | * @param mixed $duration a Duration |
||
1089 | */ |
||
1090 | 12 | public function withDurationBeforeEnd($duration): self |
|
1091 | { |
||
1092 | 12 | return $this->startingOn($this->endDate->sub(self::filterDuration($duration))); |
|
1093 | } |
||
1094 | |||
1095 | /** |
||
1096 | * Returns a new instance with a new starting datepoint |
||
1097 | * moved forward or backward by the given interval. |
||
1098 | * |
||
1099 | * This method MUST retain the state of the current instance, and return |
||
1100 | * an instance that contains the specified starting datepoint. |
||
1101 | * |
||
1102 | * @param mixed $duration a Duration |
||
1103 | */ |
||
1104 | 18 | public function moveStartDate($duration): self |
|
1105 | { |
||
1106 | 18 | return $this->startingOn($this->startDate->add(self::filterDuration($duration))); |
|
1107 | } |
||
1108 | |||
1109 | /** |
||
1110 | * Returns a new instance with a new ending datepoint |
||
1111 | * moved forward or backward by the given interval. |
||
1112 | * |
||
1113 | * This method MUST retain the state of the current instance, and return |
||
1114 | * an instance that contains the specified ending datepoint. |
||
1115 | * |
||
1116 | * @param mixed $duration a Duration |
||
1117 | */ |
||
1118 | 15 | public function moveEndDate($duration): self |
|
1119 | { |
||
1120 | 15 | return $this->endingOn($this->endDate->add(self::filterDuration($duration))); |
|
1121 | } |
||
1122 | |||
1123 | /** |
||
1124 | * Returns a new instance where the datepoints |
||
1125 | * are moved forwards or backward simultaneously by the given DateInterval. |
||
1126 | * |
||
1127 | * This method MUST retain the state of the current instance, and return |
||
1128 | * an instance that contains the specified new datepoints. |
||
1129 | * |
||
1130 | * @param mixed $duration a Duration |
||
1131 | */ |
||
1132 | 24 | public function move($duration): self |
|
1133 | { |
||
1134 | 24 | $duration = self::filterDuration($duration); |
|
1135 | 24 | $interval = new self($this->startDate->add($duration), $this->endDate->add($duration), $this->boundaryType); |
|
1136 | 24 | if ($this->equals($interval)) { |
|
1137 | 6 | return $this; |
|
1138 | } |
||
1139 | |||
1140 | 24 | return $interval; |
|
1141 | } |
||
1142 | |||
1143 | /** |
||
1144 | * Returns an instance where the given DateInterval is simultaneously |
||
1145 | * subtracted from the starting datepoint and added to the ending datepoint. |
||
1146 | * |
||
1147 | * Depending on the duration value, the resulting instance duration will be expanded or shrinked. |
||
1148 | * |
||
1149 | * This method MUST retain the state of the current instance, and return |
||
1150 | * an instance that contains the specified new datepoints. |
||
1151 | * |
||
1152 | * @param mixed $duration a Duration |
||
1153 | */ |
||
1154 | 24 | public function expand($duration): self |
|
1155 | { |
||
1156 | 24 | $duration = self::filterDuration($duration); |
|
1157 | 24 | $interval = new self($this->startDate->sub($duration), $this->endDate->add($duration), $this->boundaryType); |
|
1158 | 18 | if ($this->equals($interval)) { |
|
1159 | 6 | return $this; |
|
1160 | } |
||
1161 | |||
1162 | 12 | return $interval; |
|
1163 | } |
||
1164 | } |
||
1165 |