blob: 0e81e7573a31728f6262649bdaadaf33688eb762 [file] [log] [blame]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001<?php
2
3/**
4 * This file is part of the Carbon package.
5 *
6 * (c) Brian Nesbitt <brian@nesbot.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010011
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020012namespace Carbon;
13
14use Carbon\Exceptions\InvalidCastException;
15use Carbon\Exceptions\InvalidIntervalException;
16use Carbon\Exceptions\InvalidPeriodDateException;
17use Carbon\Exceptions\InvalidPeriodParameterException;
18use Carbon\Exceptions\NotACarbonClassException;
19use Carbon\Exceptions\NotAPeriodException;
20use Carbon\Exceptions\UnknownGetterException;
21use Carbon\Exceptions\UnknownMethodException;
22use Carbon\Exceptions\UnreachableException;
23use Carbon\Traits\IntervalRounding;
24use Carbon\Traits\Mixin;
25use Carbon\Traits\Options;
26use Closure;
27use Countable;
28use DateInterval;
29use DatePeriod;
30use DateTime;
31use DateTimeInterface;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010032use DateTimeZone;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020033use InvalidArgumentException;
34use Iterator;
35use JsonSerializable;
36use ReflectionException;
37use ReturnTypeWillChange;
38use RuntimeException;
39
40/**
41 * Substitution of DatePeriod with some modifications and many more features.
42 *
43 * @property-read int|float $recurrences number of recurrences (if end not set).
44 * @property-read bool $include_start_date rather the start date is included in the iteration.
45 * @property-read bool $include_end_date rather the end date is included in the iteration (if recurrences not set).
46 * @property-read CarbonInterface $start Period start date.
47 * @property-read CarbonInterface $current Current date from the iteration.
48 * @property-read CarbonInterface $end Period end date.
49 * @property-read CarbonInterval $interval Underlying date interval instance. Always present, one day by default.
50 *
51 * @method static CarbonPeriod start($date, $inclusive = null) Create instance specifying start date or modify the start date if called on an instance.
52 * @method static CarbonPeriod since($date, $inclusive = null) Alias for start().
53 * @method static CarbonPeriod sinceNow($inclusive = null) Create instance with start date set to now or set the start date to now if called on an instance.
54 * @method static CarbonPeriod end($date = null, $inclusive = null) Create instance specifying end date or modify the end date if called on an instance.
55 * @method static CarbonPeriod until($date = null, $inclusive = null) Alias for end().
56 * @method static CarbonPeriod untilNow($inclusive = null) Create instance with end date set to now or set the end date to now if called on an instance.
57 * @method static CarbonPeriod dates($start, $end = null) Create instance with start and end dates or modify the start and end dates if called on an instance.
58 * @method static CarbonPeriod between($start, $end = null) Create instance with start and end dates or modify the start and end dates if called on an instance.
59 * @method static CarbonPeriod recurrences($recurrences = null) Create instance with maximum number of recurrences or modify the number of recurrences if called on an instance.
60 * @method static CarbonPeriod times($recurrences = null) Alias for recurrences().
61 * @method static CarbonPeriod options($options = null) Create instance with options or modify the options if called on an instance.
62 * @method static CarbonPeriod toggle($options, $state = null) Create instance with options toggled on or off, or toggle options if called on an instance.
63 * @method static CarbonPeriod filter($callback, $name = null) Create instance with filter added to the stack or append a filter if called on an instance.
64 * @method static CarbonPeriod push($callback, $name = null) Alias for filter().
65 * @method static CarbonPeriod prepend($callback, $name = null) Create instance with filter prepended to the stack or prepend a filter if called on an instance.
66 * @method static CarbonPeriod filters(array $filters = []) Create instance with filters stack or replace the whole filters stack if called on an instance.
67 * @method static CarbonPeriod interval($interval) Create instance with given date interval or modify the interval if called on an instance.
68 * @method static CarbonPeriod each($interval) Create instance with given date interval or modify the interval if called on an instance.
69 * @method static CarbonPeriod every($interval) Create instance with given date interval or modify the interval if called on an instance.
70 * @method static CarbonPeriod step($interval) Create instance with given date interval or modify the interval if called on an instance.
71 * @method static CarbonPeriod stepBy($interval) Create instance with given date interval or modify the interval if called on an instance.
72 * @method static CarbonPeriod invert() Create instance with inverted date interval or invert the interval if called on an instance.
73 * @method static CarbonPeriod years($years = 1) Create instance specifying a number of years for date interval or replace the interval by the given a number of years if called on an instance.
74 * @method static CarbonPeriod year($years = 1) Alias for years().
75 * @method static CarbonPeriod months($months = 1) Create instance specifying a number of months for date interval or replace the interval by the given a number of months if called on an instance.
76 * @method static CarbonPeriod month($months = 1) Alias for months().
77 * @method static CarbonPeriod weeks($weeks = 1) Create instance specifying a number of weeks for date interval or replace the interval by the given a number of weeks if called on an instance.
78 * @method static CarbonPeriod week($weeks = 1) Alias for weeks().
79 * @method static CarbonPeriod days($days = 1) Create instance specifying a number of days for date interval or replace the interval by the given a number of days if called on an instance.
80 * @method static CarbonPeriod dayz($days = 1) Alias for days().
81 * @method static CarbonPeriod day($days = 1) Alias for days().
82 * @method static CarbonPeriod hours($hours = 1) Create instance specifying a number of hours for date interval or replace the interval by the given a number of hours if called on an instance.
83 * @method static CarbonPeriod hour($hours = 1) Alias for hours().
84 * @method static CarbonPeriod minutes($minutes = 1) Create instance specifying a number of minutes for date interval or replace the interval by the given a number of minutes if called on an instance.
85 * @method static CarbonPeriod minute($minutes = 1) Alias for minutes().
86 * @method static CarbonPeriod seconds($seconds = 1) Create instance specifying a number of seconds for date interval or replace the interval by the given a number of seconds if called on an instance.
87 * @method static CarbonPeriod second($seconds = 1) Alias for seconds().
88 * @method $this roundYear(float $precision = 1, string $function = "round") Round the current instance year with given precision using the given function.
89 * @method $this roundYears(float $precision = 1, string $function = "round") Round the current instance year with given precision using the given function.
90 * @method $this floorYear(float $precision = 1) Truncate the current instance year with given precision.
91 * @method $this floorYears(float $precision = 1) Truncate the current instance year with given precision.
92 * @method $this ceilYear(float $precision = 1) Ceil the current instance year with given precision.
93 * @method $this ceilYears(float $precision = 1) Ceil the current instance year with given precision.
94 * @method $this roundMonth(float $precision = 1, string $function = "round") Round the current instance month with given precision using the given function.
95 * @method $this roundMonths(float $precision = 1, string $function = "round") Round the current instance month with given precision using the given function.
96 * @method $this floorMonth(float $precision = 1) Truncate the current instance month with given precision.
97 * @method $this floorMonths(float $precision = 1) Truncate the current instance month with given precision.
98 * @method $this ceilMonth(float $precision = 1) Ceil the current instance month with given precision.
99 * @method $this ceilMonths(float $precision = 1) Ceil the current instance month with given precision.
100 * @method $this roundWeek(float $precision = 1, string $function = "round") Round the current instance day with given precision using the given function.
101 * @method $this roundWeeks(float $precision = 1, string $function = "round") Round the current instance day with given precision using the given function.
102 * @method $this floorWeek(float $precision = 1) Truncate the current instance day with given precision.
103 * @method $this floorWeeks(float $precision = 1) Truncate the current instance day with given precision.
104 * @method $this ceilWeek(float $precision = 1) Ceil the current instance day with given precision.
105 * @method $this ceilWeeks(float $precision = 1) Ceil the current instance day with given precision.
106 * @method $this roundDay(float $precision = 1, string $function = "round") Round the current instance day with given precision using the given function.
107 * @method $this roundDays(float $precision = 1, string $function = "round") Round the current instance day with given precision using the given function.
108 * @method $this floorDay(float $precision = 1) Truncate the current instance day with given precision.
109 * @method $this floorDays(float $precision = 1) Truncate the current instance day with given precision.
110 * @method $this ceilDay(float $precision = 1) Ceil the current instance day with given precision.
111 * @method $this ceilDays(float $precision = 1) Ceil the current instance day with given precision.
112 * @method $this roundHour(float $precision = 1, string $function = "round") Round the current instance hour with given precision using the given function.
113 * @method $this roundHours(float $precision = 1, string $function = "round") Round the current instance hour with given precision using the given function.
114 * @method $this floorHour(float $precision = 1) Truncate the current instance hour with given precision.
115 * @method $this floorHours(float $precision = 1) Truncate the current instance hour with given precision.
116 * @method $this ceilHour(float $precision = 1) Ceil the current instance hour with given precision.
117 * @method $this ceilHours(float $precision = 1) Ceil the current instance hour with given precision.
118 * @method $this roundMinute(float $precision = 1, string $function = "round") Round the current instance minute with given precision using the given function.
119 * @method $this roundMinutes(float $precision = 1, string $function = "round") Round the current instance minute with given precision using the given function.
120 * @method $this floorMinute(float $precision = 1) Truncate the current instance minute with given precision.
121 * @method $this floorMinutes(float $precision = 1) Truncate the current instance minute with given precision.
122 * @method $this ceilMinute(float $precision = 1) Ceil the current instance minute with given precision.
123 * @method $this ceilMinutes(float $precision = 1) Ceil the current instance minute with given precision.
124 * @method $this roundSecond(float $precision = 1, string $function = "round") Round the current instance second with given precision using the given function.
125 * @method $this roundSeconds(float $precision = 1, string $function = "round") Round the current instance second with given precision using the given function.
126 * @method $this floorSecond(float $precision = 1) Truncate the current instance second with given precision.
127 * @method $this floorSeconds(float $precision = 1) Truncate the current instance second with given precision.
128 * @method $this ceilSecond(float $precision = 1) Ceil the current instance second with given precision.
129 * @method $this ceilSeconds(float $precision = 1) Ceil the current instance second with given precision.
130 * @method $this roundMillennium(float $precision = 1, string $function = "round") Round the current instance millennium with given precision using the given function.
131 * @method $this roundMillennia(float $precision = 1, string $function = "round") Round the current instance millennium with given precision using the given function.
132 * @method $this floorMillennium(float $precision = 1) Truncate the current instance millennium with given precision.
133 * @method $this floorMillennia(float $precision = 1) Truncate the current instance millennium with given precision.
134 * @method $this ceilMillennium(float $precision = 1) Ceil the current instance millennium with given precision.
135 * @method $this ceilMillennia(float $precision = 1) Ceil the current instance millennium with given precision.
136 * @method $this roundCentury(float $precision = 1, string $function = "round") Round the current instance century with given precision using the given function.
137 * @method $this roundCenturies(float $precision = 1, string $function = "round") Round the current instance century with given precision using the given function.
138 * @method $this floorCentury(float $precision = 1) Truncate the current instance century with given precision.
139 * @method $this floorCenturies(float $precision = 1) Truncate the current instance century with given precision.
140 * @method $this ceilCentury(float $precision = 1) Ceil the current instance century with given precision.
141 * @method $this ceilCenturies(float $precision = 1) Ceil the current instance century with given precision.
142 * @method $this roundDecade(float $precision = 1, string $function = "round") Round the current instance decade with given precision using the given function.
143 * @method $this roundDecades(float $precision = 1, string $function = "round") Round the current instance decade with given precision using the given function.
144 * @method $this floorDecade(float $precision = 1) Truncate the current instance decade with given precision.
145 * @method $this floorDecades(float $precision = 1) Truncate the current instance decade with given precision.
146 * @method $this ceilDecade(float $precision = 1) Ceil the current instance decade with given precision.
147 * @method $this ceilDecades(float $precision = 1) Ceil the current instance decade with given precision.
148 * @method $this roundQuarter(float $precision = 1, string $function = "round") Round the current instance quarter with given precision using the given function.
149 * @method $this roundQuarters(float $precision = 1, string $function = "round") Round the current instance quarter with given precision using the given function.
150 * @method $this floorQuarter(float $precision = 1) Truncate the current instance quarter with given precision.
151 * @method $this floorQuarters(float $precision = 1) Truncate the current instance quarter with given precision.
152 * @method $this ceilQuarter(float $precision = 1) Ceil the current instance quarter with given precision.
153 * @method $this ceilQuarters(float $precision = 1) Ceil the current instance quarter with given precision.
154 * @method $this roundMillisecond(float $precision = 1, string $function = "round") Round the current instance millisecond with given precision using the given function.
155 * @method $this roundMilliseconds(float $precision = 1, string $function = "round") Round the current instance millisecond with given precision using the given function.
156 * @method $this floorMillisecond(float $precision = 1) Truncate the current instance millisecond with given precision.
157 * @method $this floorMilliseconds(float $precision = 1) Truncate the current instance millisecond with given precision.
158 * @method $this ceilMillisecond(float $precision = 1) Ceil the current instance millisecond with given precision.
159 * @method $this ceilMilliseconds(float $precision = 1) Ceil the current instance millisecond with given precision.
160 * @method $this roundMicrosecond(float $precision = 1, string $function = "round") Round the current instance microsecond with given precision using the given function.
161 * @method $this roundMicroseconds(float $precision = 1, string $function = "round") Round the current instance microsecond with given precision using the given function.
162 * @method $this floorMicrosecond(float $precision = 1) Truncate the current instance microsecond with given precision.
163 * @method $this floorMicroseconds(float $precision = 1) Truncate the current instance microsecond with given precision.
164 * @method $this ceilMicrosecond(float $precision = 1) Ceil the current instance microsecond with given precision.
165 * @method $this ceilMicroseconds(float $precision = 1) Ceil the current instance microsecond with given precision.
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100166 *
167 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200168 */
169class CarbonPeriod implements Iterator, Countable, JsonSerializable
170{
171 use IntervalRounding;
172 use Mixin {
173 Mixin::mixin as baseMixin;
174 }
175 use Options;
176
177 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100178 * Built-in filter for limit by recurrences.
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200179 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100180 * @var callable
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200181 */
182 public const RECURRENCES_FILTER = [self::class, 'filterRecurrences'];
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100183
184 /**
185 * Built-in filter for limit to an end.
186 *
187 * @var callable
188 */
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200189 public const END_DATE_FILTER = [self::class, 'filterEndDate'];
190
191 /**
192 * Special value which can be returned by filters to end iteration. Also a filter.
193 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100194 * @var callable
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200195 */
196 public const END_ITERATION = [self::class, 'endIteration'];
197
198 /**
199 * Exclude start date from iteration.
200 *
201 * @var int
202 */
203 public const EXCLUDE_START_DATE = 1;
204
205 /**
206 * Exclude end date from iteration.
207 *
208 * @var int
209 */
210 public const EXCLUDE_END_DATE = 2;
211
212 /**
213 * Yield CarbonImmutable instances.
214 *
215 * @var int
216 */
217 public const IMMUTABLE = 4;
218
219 /**
220 * Number of maximum attempts before giving up on finding next valid date.
221 *
222 * @var int
223 */
224 public const NEXT_MAX_ATTEMPTS = 1000;
225
226 /**
227 * Number of maximum attempts before giving up on finding end date.
228 *
229 * @var int
230 */
231 public const END_MAX_ATTEMPTS = 10000;
232
233 /**
234 * The registered macros.
235 *
236 * @var array
237 */
238 protected static $macros = [];
239
240 /**
241 * Date class of iteration items.
242 *
243 * @var string
244 */
245 protected $dateClass = Carbon::class;
246
247 /**
248 * Underlying date interval instance. Always present, one day by default.
249 *
250 * @var CarbonInterval
251 */
252 protected $dateInterval;
253
254 /**
255 * Whether current date interval was set by default.
256 *
257 * @var bool
258 */
259 protected $isDefaultInterval;
260
261 /**
262 * The filters stack.
263 *
264 * @var array
265 */
266 protected $filters = [];
267
268 /**
269 * Period start date. Applied on rewind. Always present, now by default.
270 *
271 * @var CarbonInterface
272 */
273 protected $startDate;
274
275 /**
276 * Period end date. For inverted interval should be before the start date. Applied via a filter.
277 *
278 * @var CarbonInterface|null
279 */
280 protected $endDate;
281
282 /**
283 * Limit for number of recurrences. Applied via a filter.
284 *
285 * @var int|null
286 */
287 protected $recurrences;
288
289 /**
290 * Iteration options.
291 *
292 * @var int
293 */
294 protected $options;
295
296 /**
297 * Index of current date. Always sequential, even if some dates are skipped by filters.
298 * Equal to null only before the first iteration.
299 *
300 * @var int
301 */
302 protected $key;
303
304 /**
305 * Current date. May temporarily hold unaccepted value when looking for a next valid date.
306 * Equal to null only before the first iteration.
307 *
308 * @var CarbonInterface
309 */
310 protected $current;
311
312 /**
313 * Timezone of current date. Taken from the start date.
314 *
315 * @var \DateTimeZone|null
316 */
317 protected $timezone;
318
319 /**
320 * The cached validation result for current date.
321 *
322 * @var bool|string|null
323 */
324 protected $validationResult;
325
326 /**
327 * Timezone handler for settings() method.
328 *
329 * @var mixed
330 */
331 protected $tzName;
332
333 /**
334 * Make a CarbonPeriod instance from given variable if possible.
335 *
336 * @param mixed $var
337 *
338 * @return static|null
339 */
340 public static function make($var)
341 {
342 try {
343 return static::instance($var);
344 } catch (NotAPeriodException $e) {
345 return static::create($var);
346 }
347 }
348
349 /**
350 * Create a new instance from a DatePeriod or CarbonPeriod object.
351 *
352 * @param CarbonPeriod|DatePeriod $period
353 *
354 * @return static
355 */
356 public static function instance($period)
357 {
358 if ($period instanceof static) {
359 return $period->copy();
360 }
361
362 if ($period instanceof self) {
363 return new static(
364 $period->getStartDate(),
365 $period->getEndDate() ?: $period->getRecurrences(),
366 $period->getDateInterval(),
367 $period->getOptions()
368 );
369 }
370
371 if ($period instanceof DatePeriod) {
372 return new static(
373 $period->start,
374 $period->end ?: ($period->recurrences - 1),
375 $period->interval,
376 $period->include_start_date ? 0 : static::EXCLUDE_START_DATE
377 );
378 }
379
380 $class = \get_called_class();
381 $type = \gettype($period);
382
383 throw new NotAPeriodException(
384 'Argument 1 passed to '.$class.'::'.__METHOD__.'() '.
385 'must be an instance of DatePeriod or '.$class.', '.
386 ($type === 'object' ? 'instance of '.\get_class($period) : $type).' given.'
387 );
388 }
389
390 /**
391 * Create a new instance.
392 *
393 * @return static
394 */
395 public static function create(...$params)
396 {
397 return static::createFromArray($params);
398 }
399
400 /**
401 * Create a new instance from an array of parameters.
402 *
403 * @param array $params
404 *
405 * @return static
406 */
407 public static function createFromArray(array $params)
408 {
409 return new static(...$params);
410 }
411
412 /**
413 * Create CarbonPeriod from ISO 8601 string.
414 *
415 * @param string $iso
416 * @param int|null $options
417 *
418 * @return static
419 */
420 public static function createFromIso($iso, $options = null)
421 {
422 $params = static::parseIso8601($iso);
423
424 $instance = static::createFromArray($params);
425
426 if ($options !== null) {
427 $instance->setOptions($options);
428 }
429
430 return $instance;
431 }
432
433 /**
434 * Return whether given interval contains non zero value of any time unit.
435 *
436 * @param \DateInterval $interval
437 *
438 * @return bool
439 */
440 protected static function intervalHasTime(DateInterval $interval)
441 {
442 return $interval->h || $interval->i || $interval->s || $interval->f;
443 }
444
445 /**
446 * Return whether given variable is an ISO 8601 specification.
447 *
448 * Note: Check is very basic, as actual validation will be done later when parsing.
449 * We just want to ensure that variable is not any other type of a valid parameter.
450 *
451 * @param mixed $var
452 *
453 * @return bool
454 */
455 protected static function isIso8601($var)
456 {
457 if (!\is_string($var)) {
458 return false;
459 }
460
461 // Match slash but not within a timezone name.
462 $part = '[a-z]+(?:[_-][a-z]+)*';
463
464 preg_match("#\b$part/$part\b|(/)#i", $var, $match);
465
466 return isset($match[1]);
467 }
468
469 /**
470 * Parse given ISO 8601 string into an array of arguments.
471 *
472 * @SuppressWarnings(PHPMD.ElseExpression)
473 *
474 * @param string $iso
475 *
476 * @return array
477 */
478 protected static function parseIso8601($iso)
479 {
480 $result = [];
481
482 $interval = null;
483 $start = null;
484 $end = null;
485
486 foreach (explode('/', $iso) as $key => $part) {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100487 if ($key === 0 && preg_match('/^R([0-9]*|INF)$/', $part, $match)) {
488 $parsed = \strlen($match[1]) ? (($match[1] !== 'INF') ? (int) $match[1] : INF) : null;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200489 } elseif ($interval === null && $parsed = CarbonInterval::make($part)) {
490 $interval = $part;
491 } elseif ($start === null && $parsed = Carbon::make($part)) {
492 $start = $part;
493 } elseif ($end === null && $parsed = Carbon::make(static::addMissingParts($start ?? '', $part))) {
494 $end = $part;
495 } else {
496 throw new InvalidPeriodParameterException("Invalid ISO 8601 specification: $iso.");
497 }
498
499 $result[] = $parsed;
500 }
501
502 return $result;
503 }
504
505 /**
506 * Add missing parts of the target date from the soure date.
507 *
508 * @param string $source
509 * @param string $target
510 *
511 * @return string
512 */
513 protected static function addMissingParts($source, $target)
514 {
515 $pattern = '/'.preg_replace('/[0-9]+/', '[0-9]+', preg_quote($target, '/')).'$/';
516
517 $result = preg_replace($pattern, $target, $source, 1, $count);
518
519 return $count ? $result : $target;
520 }
521
522 /**
523 * Register a custom macro.
524 *
525 * @example
526 * ```
527 * CarbonPeriod::macro('middle', function () {
528 * return $this->getStartDate()->average($this->getEndDate());
529 * });
530 * echo CarbonPeriod::since('2011-05-12')->until('2011-06-03')->middle();
531 * ```
532 *
533 * @param string $name
534 * @param object|callable $macro
535 *
536 * @return void
537 */
538 public static function macro($name, $macro)
539 {
540 static::$macros[$name] = $macro;
541 }
542
543 /**
544 * Register macros from a mixin object.
545 *
546 * @example
547 * ```
548 * CarbonPeriod::mixin(new class {
549 * public function addDays() {
550 * return function ($count = 1) {
551 * return $this->setStartDate(
552 * $this->getStartDate()->addDays($count)
553 * )->setEndDate(
554 * $this->getEndDate()->addDays($count)
555 * );
556 * };
557 * }
558 * public function subDays() {
559 * return function ($count = 1) {
560 * return $this->setStartDate(
561 * $this->getStartDate()->subDays($count)
562 * )->setEndDate(
563 * $this->getEndDate()->subDays($count)
564 * );
565 * };
566 * }
567 * });
568 * echo CarbonPeriod::create('2000-01-01', '2000-02-01')->addDays(5)->subDays(3);
569 * ```
570 *
571 * @param object|string $mixin
572 *
573 * @throws ReflectionException
574 *
575 * @return void
576 */
577 public static function mixin($mixin)
578 {
579 static::baseMixin($mixin);
580 }
581
582 /**
583 * Check if macro is registered.
584 *
585 * @param string $name
586 *
587 * @return bool
588 */
589 public static function hasMacro($name)
590 {
591 return isset(static::$macros[$name]);
592 }
593
594 /**
595 * Provide static proxy for instance aliases.
596 *
597 * @param string $method
598 * @param array $parameters
599 *
600 * @return mixed
601 */
602 public static function __callStatic($method, $parameters)
603 {
604 $date = new static();
605
606 if (static::hasMacro($method)) {
607 return static::bindMacroContext(null, function () use (&$method, &$parameters, &$date) {
608 return $date->callMacro($method, $parameters);
609 });
610 }
611
612 return $date->$method(...$parameters);
613 }
614
615 /**
616 * CarbonPeriod constructor.
617 *
618 * @SuppressWarnings(PHPMD.ElseExpression)
619 *
620 * @throws InvalidArgumentException
621 */
622 public function __construct(...$arguments)
623 {
624 // Parse and assign arguments one by one. First argument may be an ISO 8601 spec,
625 // which will be first parsed into parts and then processed the same way.
626
627 $argumentsCount = \count($arguments);
628
629 if ($argumentsCount && static::isIso8601($iso = $arguments[0])) {
630 array_splice($arguments, 0, 1, static::parseIso8601($iso));
631 }
632
633 if ($argumentsCount === 1) {
634 if ($arguments[0] instanceof DatePeriod) {
635 $arguments = [
636 $arguments[0]->start,
637 $arguments[0]->end ?: ($arguments[0]->recurrences - 1),
638 $arguments[0]->interval,
639 $arguments[0]->include_start_date ? 0 : static::EXCLUDE_START_DATE,
640 ];
641 } elseif ($arguments[0] instanceof self) {
642 $arguments = [
643 $arguments[0]->getStartDate(),
644 $arguments[0]->getEndDate() ?: $arguments[0]->getRecurrences(),
645 $arguments[0]->getDateInterval(),
646 $arguments[0]->getOptions(),
647 ];
648 }
649 }
650
651 foreach ($arguments as $argument) {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100652 $parsedDate = null;
653
654 if ($argument instanceof DateTimeZone) {
655 $this->setTimezone($argument);
656 } elseif ($this->dateInterval === null &&
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200657 (
658 \is_string($argument) && preg_match(
659 '/^(-?\d(\d(?![\/-])|[^\d\/-]([\/-])?)*|P[T0-9].*|(?:\h*\d+(?:\.\d+)?\h*[a-z]+)+)$/i',
660 $argument
661 ) ||
662 $argument instanceof DateInterval ||
663 $argument instanceof Closure
664 ) &&
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100665 $parsedInterval = @CarbonInterval::make($argument)
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200666 ) {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100667 $this->setDateInterval($parsedInterval);
668 } elseif ($this->startDate === null && $parsedDate = $this->makeDateTime($argument)) {
669 $this->setStartDate($parsedDate);
670 } elseif ($this->endDate === null && ($parsedDate = $parsedDate ?? $this->makeDateTime($argument))) {
671 $this->setEndDate($parsedDate);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200672 } elseif ($this->recurrences === null && $this->endDate === null && is_numeric($argument)) {
673 $this->setRecurrences($argument);
674 } elseif ($this->options === null && (\is_int($argument) || $argument === null)) {
675 $this->setOptions($argument);
676 } else {
677 throw new InvalidPeriodParameterException('Invalid constructor parameters.');
678 }
679 }
680
681 if ($this->startDate === null) {
682 $this->setStartDate(Carbon::now());
683 }
684
685 if ($this->dateInterval === null) {
686 $this->setDateInterval(CarbonInterval::day());
687
688 $this->isDefaultInterval = true;
689 }
690
691 if ($this->options === null) {
692 $this->setOptions(0);
693 }
694 }
695
696 /**
697 * Get a copy of the instance.
698 *
699 * @return static
700 */
701 public function copy()
702 {
703 return clone $this;
704 }
705
706 /**
707 * Get the getter for a property allowing both `DatePeriod` snakeCase and camelCase names.
708 *
709 * @param string $name
710 *
711 * @return callable|null
712 */
713 protected function getGetter(string $name)
714 {
715 switch (strtolower(preg_replace('/[A-Z]/', '_$0', $name))) {
716 case 'start':
717 case 'start_date':
718 return [$this, 'getStartDate'];
719 case 'end':
720 case 'end_date':
721 return [$this, 'getEndDate'];
722 case 'interval':
723 case 'date_interval':
724 return [$this, 'getDateInterval'];
725 case 'recurrences':
726 return [$this, 'getRecurrences'];
727 case 'include_start_date':
728 return [$this, 'isStartIncluded'];
729 case 'include_end_date':
730 return [$this, 'isEndIncluded'];
731 case 'current':
732 return [$this, 'current'];
733 default:
734 return null;
735 }
736 }
737
738 /**
739 * Get a property allowing both `DatePeriod` snakeCase and camelCase names.
740 *
741 * @param string $name
742 *
743 * @return bool|CarbonInterface|CarbonInterval|int|null
744 */
745 public function get(string $name)
746 {
747 $getter = $this->getGetter($name);
748
749 if ($getter) {
750 return $getter();
751 }
752
753 throw new UnknownGetterException($name);
754 }
755
756 /**
757 * Get a property allowing both `DatePeriod` snakeCase and camelCase names.
758 *
759 * @param string $name
760 *
761 * @return bool|CarbonInterface|CarbonInterval|int|null
762 */
763 public function __get(string $name)
764 {
765 return $this->get($name);
766 }
767
768 /**
769 * Check if an attribute exists on the object
770 *
771 * @param string $name
772 *
773 * @return bool
774 */
775 public function __isset(string $name): bool
776 {
777 return $this->getGetter($name) !== null;
778 }
779
780 /**
781 * @alias copy
782 *
783 * Get a copy of the instance.
784 *
785 * @return static
786 */
787 public function clone()
788 {
789 return clone $this;
790 }
791
792 /**
793 * Set the iteration item class.
794 *
795 * @param string $dateClass
796 *
797 * @return $this
798 */
799 public function setDateClass(string $dateClass)
800 {
801 if (!is_a($dateClass, CarbonInterface::class, true)) {
802 throw new NotACarbonClassException($dateClass);
803 }
804
805 $this->dateClass = $dateClass;
806
807 if (is_a($dateClass, Carbon::class, true)) {
808 $this->toggleOptions(static::IMMUTABLE, false);
809 } elseif (is_a($dateClass, CarbonImmutable::class, true)) {
810 $this->toggleOptions(static::IMMUTABLE, true);
811 }
812
813 return $this;
814 }
815
816 /**
817 * Returns iteration item date class.
818 *
819 * @return string
820 */
821 public function getDateClass(): string
822 {
823 return $this->dateClass;
824 }
825
826 /**
827 * Change the period date interval.
828 *
829 * @param DateInterval|string $interval
830 *
831 * @throws InvalidIntervalException
832 *
833 * @return $this
834 */
835 public function setDateInterval($interval)
836 {
837 if (!$interval = CarbonInterval::make($interval)) {
838 throw new InvalidIntervalException('Invalid interval.');
839 }
840
841 if ($interval->spec() === 'PT0S' && !$interval->f && !$interval->getStep()) {
842 throw new InvalidIntervalException('Empty interval is not accepted.');
843 }
844
845 $this->dateInterval = $interval;
846
847 $this->isDefaultInterval = false;
848
849 $this->handleChangedParameters();
850
851 return $this;
852 }
853
854 /**
855 * Invert the period date interval.
856 *
857 * @return $this
858 */
859 public function invertDateInterval()
860 {
861 $interval = $this->dateInterval->invert();
862
863 return $this->setDateInterval($interval);
864 }
865
866 /**
867 * Set start and end date.
868 *
869 * @param DateTime|DateTimeInterface|string $start
870 * @param DateTime|DateTimeInterface|string|null $end
871 *
872 * @return $this
873 */
874 public function setDates($start, $end)
875 {
876 $this->setStartDate($start);
877 $this->setEndDate($end);
878
879 return $this;
880 }
881
882 /**
883 * Change the period options.
884 *
885 * @param int|null $options
886 *
887 * @throws InvalidArgumentException
888 *
889 * @return $this
890 */
891 public function setOptions($options)
892 {
893 if (!\is_int($options) && $options !== null) {
894 throw new InvalidPeriodParameterException('Invalid options.');
895 }
896
897 $this->options = $options ?: 0;
898
899 $this->handleChangedParameters();
900
901 return $this;
902 }
903
904 /**
905 * Get the period options.
906 *
907 * @return int
908 */
909 public function getOptions()
910 {
911 return $this->options;
912 }
913
914 /**
915 * Toggle given options on or off.
916 *
917 * @param int $options
918 * @param bool|null $state
919 *
920 * @throws \InvalidArgumentException
921 *
922 * @return $this
923 */
924 public function toggleOptions($options, $state = null)
925 {
926 if ($state === null) {
927 $state = ($this->options & $options) !== $options;
928 }
929
930 return $this->setOptions(
931 $state ?
932 $this->options | $options :
933 $this->options & ~$options
934 );
935 }
936
937 /**
938 * Toggle EXCLUDE_START_DATE option.
939 *
940 * @param bool $state
941 *
942 * @return $this
943 */
944 public function excludeStartDate($state = true)
945 {
946 return $this->toggleOptions(static::EXCLUDE_START_DATE, $state);
947 }
948
949 /**
950 * Toggle EXCLUDE_END_DATE option.
951 *
952 * @param bool $state
953 *
954 * @return $this
955 */
956 public function excludeEndDate($state = true)
957 {
958 return $this->toggleOptions(static::EXCLUDE_END_DATE, $state);
959 }
960
961 /**
962 * Get the underlying date interval.
963 *
964 * @return CarbonInterval
965 */
966 public function getDateInterval()
967 {
968 return $this->dateInterval->copy();
969 }
970
971 /**
972 * Get start date of the period.
973 *
974 * @param string|null $rounding Optional rounding 'floor', 'ceil', 'round' using the period interval.
975 *
976 * @return CarbonInterface
977 */
978 public function getStartDate(string $rounding = null)
979 {
980 $date = $this->startDate->avoidMutation();
981
982 return $rounding ? $date->round($this->getDateInterval(), $rounding) : $date;
983 }
984
985 /**
986 * Get end date of the period.
987 *
988 * @param string|null $rounding Optional rounding 'floor', 'ceil', 'round' using the period interval.
989 *
990 * @return CarbonInterface|null
991 */
992 public function getEndDate(string $rounding = null)
993 {
994 if (!$this->endDate) {
995 return null;
996 }
997
998 $date = $this->endDate->avoidMutation();
999
1000 return $rounding ? $date->round($this->getDateInterval(), $rounding) : $date;
1001 }
1002
1003 /**
1004 * Get number of recurrences.
1005 *
1006 * @return int|float|null
1007 */
1008 public function getRecurrences()
1009 {
1010 return $this->recurrences;
1011 }
1012
1013 /**
1014 * Returns true if the start date should be excluded.
1015 *
1016 * @return bool
1017 */
1018 public function isStartExcluded()
1019 {
1020 return ($this->options & static::EXCLUDE_START_DATE) !== 0;
1021 }
1022
1023 /**
1024 * Returns true if the end date should be excluded.
1025 *
1026 * @return bool
1027 */
1028 public function isEndExcluded()
1029 {
1030 return ($this->options & static::EXCLUDE_END_DATE) !== 0;
1031 }
1032
1033 /**
1034 * Returns true if the start date should be included.
1035 *
1036 * @return bool
1037 */
1038 public function isStartIncluded()
1039 {
1040 return !$this->isStartExcluded();
1041 }
1042
1043 /**
1044 * Returns true if the end date should be included.
1045 *
1046 * @return bool
1047 */
1048 public function isEndIncluded()
1049 {
1050 return !$this->isEndExcluded();
1051 }
1052
1053 /**
1054 * Return the start if it's included by option, else return the start + 1 period interval.
1055 *
1056 * @return CarbonInterface
1057 */
1058 public function getIncludedStartDate()
1059 {
1060 $start = $this->getStartDate();
1061
1062 if ($this->isStartExcluded()) {
1063 return $start->add($this->getDateInterval());
1064 }
1065
1066 return $start;
1067 }
1068
1069 /**
1070 * Return the end if it's included by option, else return the end - 1 period interval.
1071 * Warning: if the period has no fixed end, this method will iterate the period to calculate it.
1072 *
1073 * @return CarbonInterface
1074 */
1075 public function getIncludedEndDate()
1076 {
1077 $end = $this->getEndDate();
1078
1079 if (!$end) {
1080 return $this->calculateEnd();
1081 }
1082
1083 if ($this->isEndExcluded()) {
1084 return $end->sub($this->getDateInterval());
1085 }
1086
1087 return $end;
1088 }
1089
1090 /**
1091 * Add a filter to the stack.
1092 *
1093 * @SuppressWarnings(PHPMD.UnusedFormalParameter)
1094 *
1095 * @param callable $callback
1096 * @param string $name
1097 *
1098 * @return $this
1099 */
1100 public function addFilter($callback, $name = null)
1101 {
1102 $tuple = $this->createFilterTuple(\func_get_args());
1103
1104 $this->filters[] = $tuple;
1105
1106 $this->handleChangedParameters();
1107
1108 return $this;
1109 }
1110
1111 /**
1112 * Prepend a filter to the stack.
1113 *
1114 * @SuppressWarnings(PHPMD.UnusedFormalParameter)
1115 *
1116 * @param callable $callback
1117 * @param string $name
1118 *
1119 * @return $this
1120 */
1121 public function prependFilter($callback, $name = null)
1122 {
1123 $tuple = $this->createFilterTuple(\func_get_args());
1124
1125 array_unshift($this->filters, $tuple);
1126
1127 $this->handleChangedParameters();
1128
1129 return $this;
1130 }
1131
1132 /**
1133 * Remove a filter by instance or name.
1134 *
1135 * @param callable|string $filter
1136 *
1137 * @return $this
1138 */
1139 public function removeFilter($filter)
1140 {
1141 $key = \is_callable($filter) ? 0 : 1;
1142
1143 $this->filters = array_values(array_filter(
1144 $this->filters,
1145 function ($tuple) use ($key, $filter) {
1146 return $tuple[$key] !== $filter;
1147 }
1148 ));
1149
1150 $this->updateInternalState();
1151
1152 $this->handleChangedParameters();
1153
1154 return $this;
1155 }
1156
1157 /**
1158 * Return whether given instance or name is in the filter stack.
1159 *
1160 * @param callable|string $filter
1161 *
1162 * @return bool
1163 */
1164 public function hasFilter($filter)
1165 {
1166 $key = \is_callable($filter) ? 0 : 1;
1167
1168 foreach ($this->filters as $tuple) {
1169 if ($tuple[$key] === $filter) {
1170 return true;
1171 }
1172 }
1173
1174 return false;
1175 }
1176
1177 /**
1178 * Get filters stack.
1179 *
1180 * @return array
1181 */
1182 public function getFilters()
1183 {
1184 return $this->filters;
1185 }
1186
1187 /**
1188 * Set filters stack.
1189 *
1190 * @param array $filters
1191 *
1192 * @return $this
1193 */
1194 public function setFilters(array $filters)
1195 {
1196 $this->filters = $filters;
1197
1198 $this->updateInternalState();
1199
1200 $this->handleChangedParameters();
1201
1202 return $this;
1203 }
1204
1205 /**
1206 * Reset filters stack.
1207 *
1208 * @return $this
1209 */
1210 public function resetFilters()
1211 {
1212 $this->filters = [];
1213
1214 if ($this->endDate !== null) {
1215 $this->filters[] = [static::END_DATE_FILTER, null];
1216 }
1217
1218 if ($this->recurrences !== null) {
1219 $this->filters[] = [static::RECURRENCES_FILTER, null];
1220 }
1221
1222 $this->handleChangedParameters();
1223
1224 return $this;
1225 }
1226
1227 /**
1228 * Add a recurrences filter (set maximum number of recurrences).
1229 *
1230 * @param int|float|null $recurrences
1231 *
1232 * @throws InvalidArgumentException
1233 *
1234 * @return $this
1235 */
1236 public function setRecurrences($recurrences)
1237 {
1238 if (!is_numeric($recurrences) && $recurrences !== null || $recurrences < 0) {
1239 throw new InvalidPeriodParameterException('Invalid number of recurrences.');
1240 }
1241
1242 if ($recurrences === null) {
1243 return $this->removeFilter(static::RECURRENCES_FILTER);
1244 }
1245
1246 $this->recurrences = $recurrences === INF ? INF : (int) $recurrences;
1247
1248 if (!$this->hasFilter(static::RECURRENCES_FILTER)) {
1249 return $this->addFilter(static::RECURRENCES_FILTER);
1250 }
1251
1252 $this->handleChangedParameters();
1253
1254 return $this;
1255 }
1256
1257 /**
1258 * Change the period start date.
1259 *
1260 * @param DateTime|DateTimeInterface|string $date
1261 * @param bool|null $inclusive
1262 *
1263 * @throws InvalidPeriodDateException
1264 *
1265 * @return $this
1266 */
1267 public function setStartDate($date, $inclusive = null)
1268 {
1269 if (!$date = ([$this->dateClass, 'make'])($date)) {
1270 throw new InvalidPeriodDateException('Invalid start date.');
1271 }
1272
1273 $this->startDate = $date;
1274
1275 if ($inclusive !== null) {
1276 $this->toggleOptions(static::EXCLUDE_START_DATE, !$inclusive);
1277 }
1278
1279 return $this;
1280 }
1281
1282 /**
1283 * Change the period end date.
1284 *
1285 * @param DateTime|DateTimeInterface|string|null $date
1286 * @param bool|null $inclusive
1287 *
1288 * @throws \InvalidArgumentException
1289 *
1290 * @return $this
1291 */
1292 public function setEndDate($date, $inclusive = null)
1293 {
1294 if ($date !== null && !$date = ([$this->dateClass, 'make'])($date)) {
1295 throw new InvalidPeriodDateException('Invalid end date.');
1296 }
1297
1298 if (!$date) {
1299 return $this->removeFilter(static::END_DATE_FILTER);
1300 }
1301
1302 $this->endDate = $date;
1303
1304 if ($inclusive !== null) {
1305 $this->toggleOptions(static::EXCLUDE_END_DATE, !$inclusive);
1306 }
1307
1308 if (!$this->hasFilter(static::END_DATE_FILTER)) {
1309 return $this->addFilter(static::END_DATE_FILTER);
1310 }
1311
1312 $this->handleChangedParameters();
1313
1314 return $this;
1315 }
1316
1317 /**
1318 * Check if the current position is valid.
1319 *
1320 * @return bool
1321 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001322 #[ReturnTypeWillChange]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001323 public function valid()
1324 {
1325 return $this->validateCurrentDate() === true;
1326 }
1327
1328 /**
1329 * Return the current key.
1330 *
1331 * @return int|null
1332 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001333 #[ReturnTypeWillChange]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001334 public function key()
1335 {
1336 return $this->valid()
1337 ? $this->key
1338 : null;
1339 }
1340
1341 /**
1342 * Return the current date.
1343 *
1344 * @return CarbonInterface|null
1345 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001346 #[ReturnTypeWillChange]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001347 public function current()
1348 {
1349 return $this->valid()
1350 ? $this->prepareForReturn($this->current)
1351 : null;
1352 }
1353
1354 /**
1355 * Move forward to the next date.
1356 *
1357 * @throws RuntimeException
1358 *
1359 * @return void
1360 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001361 #[ReturnTypeWillChange]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001362 public function next()
1363 {
1364 if ($this->current === null) {
1365 $this->rewind();
1366 }
1367
1368 if ($this->validationResult !== static::END_ITERATION) {
1369 $this->key++;
1370
1371 $this->incrementCurrentDateUntilValid();
1372 }
1373 }
1374
1375 /**
1376 * Rewind to the start date.
1377 *
1378 * Iterating over a date in the UTC timezone avoids bug during backward DST change.
1379 *
1380 * @see https://bugs.php.net/bug.php?id=72255
1381 * @see https://bugs.php.net/bug.php?id=74274
1382 * @see https://wiki.php.net/rfc/datetime_and_daylight_saving_time
1383 *
1384 * @throws RuntimeException
1385 *
1386 * @return void
1387 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001388 #[ReturnTypeWillChange]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001389 public function rewind()
1390 {
1391 $this->key = 0;
1392 $this->current = ([$this->dateClass, 'make'])($this->startDate);
1393 $settings = $this->getSettings();
1394
1395 if ($this->hasLocalTranslator()) {
1396 $settings['locale'] = $this->getTranslatorLocale();
1397 }
1398
1399 $this->current->settings($settings);
1400 $this->timezone = static::intervalHasTime($this->dateInterval) ? $this->current->getTimezone() : null;
1401
1402 if ($this->timezone) {
1403 $this->current = $this->current->utc();
1404 }
1405
1406 $this->validationResult = null;
1407
1408 if ($this->isStartExcluded() || $this->validateCurrentDate() === false) {
1409 $this->incrementCurrentDateUntilValid();
1410 }
1411 }
1412
1413 /**
1414 * Skip iterations and returns iteration state (false if ended, true if still valid).
1415 *
1416 * @param int $count steps number to skip (1 by default)
1417 *
1418 * @return bool
1419 */
1420 public function skip($count = 1)
1421 {
1422 for ($i = $count; $this->valid() && $i > 0; $i--) {
1423 $this->next();
1424 }
1425
1426 return $this->valid();
1427 }
1428
1429 /**
1430 * Format the date period as ISO 8601.
1431 *
1432 * @return string
1433 */
1434 public function toIso8601String()
1435 {
1436 $parts = [];
1437
1438 if ($this->recurrences !== null) {
1439 $parts[] = 'R'.$this->recurrences;
1440 }
1441
1442 $parts[] = $this->startDate->toIso8601String();
1443
1444 $parts[] = $this->dateInterval->spec();
1445
1446 if ($this->endDate !== null) {
1447 $parts[] = $this->endDate->toIso8601String();
1448 }
1449
1450 return implode('/', $parts);
1451 }
1452
1453 /**
1454 * Convert the date period into a string.
1455 *
1456 * @return string
1457 */
1458 public function toString()
1459 {
1460 $translator = ([$this->dateClass, 'getTranslator'])();
1461
1462 $parts = [];
1463
1464 $format = !$this->startDate->isStartOfDay() || $this->endDate && !$this->endDate->isStartOfDay()
1465 ? 'Y-m-d H:i:s'
1466 : 'Y-m-d';
1467
1468 if ($this->recurrences !== null) {
1469 $parts[] = $this->translate('period_recurrences', [], $this->recurrences, $translator);
1470 }
1471
1472 $parts[] = $this->translate('period_interval', [':interval' => $this->dateInterval->forHumans([
1473 'join' => true,
1474 ])], null, $translator);
1475
1476 $parts[] = $this->translate('period_start_date', [':date' => $this->startDate->rawFormat($format)], null, $translator);
1477
1478 if ($this->endDate !== null) {
1479 $parts[] = $this->translate('period_end_date', [':date' => $this->endDate->rawFormat($format)], null, $translator);
1480 }
1481
1482 $result = implode(' ', $parts);
1483
1484 return mb_strtoupper(mb_substr($result, 0, 1)).mb_substr($result, 1);
1485 }
1486
1487 /**
1488 * Format the date period as ISO 8601.
1489 *
1490 * @return string
1491 */
1492 public function spec()
1493 {
1494 return $this->toIso8601String();
1495 }
1496
1497 /**
1498 * Cast the current instance into the given class.
1499 *
1500 * @param string $className The $className::instance() method will be called to cast the current object.
1501 *
1502 * @return DatePeriod
1503 */
1504 public function cast(string $className)
1505 {
1506 if (!method_exists($className, 'instance')) {
1507 if (is_a($className, DatePeriod::class, true)) {
1508 return new $className(
1509 $this->getStartDate(),
1510 $this->getDateInterval(),
1511 $this->getEndDate() ? $this->getIncludedEndDate() : $this->getRecurrences(),
1512 $this->isStartExcluded() ? DatePeriod::EXCLUDE_START_DATE : 0
1513 );
1514 }
1515
1516 throw new InvalidCastException("$className has not the instance() method needed to cast the date.");
1517 }
1518
1519 return $className::instance($this);
1520 }
1521
1522 /**
1523 * Return native DatePeriod PHP object matching the current instance.
1524 *
1525 * @example
1526 * ```
1527 * var_dump(CarbonPeriod::create('2021-01-05', '2021-02-15')->toDatePeriod());
1528 * ```
1529 *
1530 * @return DatePeriod
1531 */
1532 public function toDatePeriod()
1533 {
1534 return $this->cast(DatePeriod::class);
1535 }
1536
1537 /**
1538 * Convert the date period into an array without changing current iteration state.
1539 *
1540 * @return CarbonInterface[]
1541 */
1542 public function toArray()
1543 {
1544 $state = [
1545 $this->key,
1546 $this->current ? $this->current->avoidMutation() : null,
1547 $this->validationResult,
1548 ];
1549
1550 $result = iterator_to_array($this);
1551
1552 [$this->key, $this->current, $this->validationResult] = $state;
1553
1554 return $result;
1555 }
1556
1557 /**
1558 * Count dates in the date period.
1559 *
1560 * @return int
1561 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001562 #[ReturnTypeWillChange]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001563 public function count()
1564 {
1565 return \count($this->toArray());
1566 }
1567
1568 /**
1569 * Return the first date in the date period.
1570 *
1571 * @return CarbonInterface|null
1572 */
1573 public function first()
1574 {
1575 return ($this->toArray() ?: [])[0] ?? null;
1576 }
1577
1578 /**
1579 * Return the last date in the date period.
1580 *
1581 * @return CarbonInterface|null
1582 */
1583 public function last()
1584 {
1585 $array = $this->toArray();
1586
1587 return $array ? $array[\count($array) - 1] : null;
1588 }
1589
1590 /**
1591 * Convert the date period into a string.
1592 *
1593 * @return string
1594 */
1595 public function __toString()
1596 {
1597 return $this->toString();
1598 }
1599
1600 /**
1601 * Add aliases for setters.
1602 *
1603 * CarbonPeriod::days(3)->hours(5)->invert()
1604 * ->sinceNow()->until('2010-01-10')
1605 * ->filter(...)
1606 * ->count()
1607 *
1608 * Note: We use magic method to let static and instance aliases with the same names.
1609 *
1610 * @param string $method
1611 * @param array $parameters
1612 *
1613 * @return mixed
1614 */
1615 public function __call($method, $parameters)
1616 {
1617 if (static::hasMacro($method)) {
1618 return static::bindMacroContext($this, function () use (&$method, &$parameters) {
1619 return $this->callMacro($method, $parameters);
1620 });
1621 }
1622
1623 $roundedValue = $this->callRoundMethod($method, $parameters);
1624
1625 if ($roundedValue !== null) {
1626 return $roundedValue;
1627 }
1628
1629 $first = \count($parameters) >= 1 ? $parameters[0] : null;
1630 $second = \count($parameters) >= 2 ? $parameters[1] : null;
1631
1632 switch ($method) {
1633 case 'start':
1634 case 'since':
1635 return $this->setStartDate($first, $second);
1636
1637 case 'sinceNow':
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001638 return $this->setStartDate(new Carbon(), $first);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001639
1640 case 'end':
1641 case 'until':
1642 return $this->setEndDate($first, $second);
1643
1644 case 'untilNow':
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001645 return $this->setEndDate(new Carbon(), $first);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001646
1647 case 'dates':
1648 case 'between':
1649 return $this->setDates($first, $second);
1650
1651 case 'recurrences':
1652 case 'times':
1653 return $this->setRecurrences($first);
1654
1655 case 'options':
1656 return $this->setOptions($first);
1657
1658 case 'toggle':
1659 return $this->toggleOptions($first, $second);
1660
1661 case 'filter':
1662 case 'push':
1663 return $this->addFilter($first, $second);
1664
1665 case 'prepend':
1666 return $this->prependFilter($first, $second);
1667
1668 case 'filters':
1669 return $this->setFilters($first ?: []);
1670
1671 case 'interval':
1672 case 'each':
1673 case 'every':
1674 case 'step':
1675 case 'stepBy':
1676 return $this->setDateInterval($first);
1677
1678 case 'invert':
1679 return $this->invertDateInterval();
1680
1681 case 'years':
1682 case 'year':
1683 case 'months':
1684 case 'month':
1685 case 'weeks':
1686 case 'week':
1687 case 'days':
1688 case 'dayz':
1689 case 'day':
1690 case 'hours':
1691 case 'hour':
1692 case 'minutes':
1693 case 'minute':
1694 case 'seconds':
1695 case 'second':
1696 return $this->setDateInterval((
1697 // Override default P1D when instantiating via fluent setters.
1698 [$this->isDefaultInterval ? new CarbonInterval('PT0S') : $this->dateInterval, $method]
1699 )(
1700 \count($parameters) === 0 ? 1 : $first
1701 ));
1702 }
1703
1704 if ($this->localStrictModeEnabled ?? Carbon::isStrictModeEnabled()) {
1705 throw new UnknownMethodException($method);
1706 }
1707
1708 return $this;
1709 }
1710
1711 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001712 * Set the instance's timezone from a string or object and apply it to start/end.
1713 *
1714 * @param \DateTimeZone|string $timezone
1715 *
1716 * @return static
1717 */
1718 public function setTimezone($timezone)
1719 {
1720 $this->tzName = $timezone;
1721 $this->timezone = $timezone;
1722
1723 if ($this->startDate) {
1724 $this->setStartDate($this->startDate->setTimezone($timezone));
1725 }
1726
1727 if ($this->endDate) {
1728 $this->setEndDate($this->endDate->setTimezone($timezone));
1729 }
1730
1731 return $this;
1732 }
1733
1734 /**
1735 * Set the instance's timezone from a string or object and add/subtract the offset difference to start/end.
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001736 *
1737 * @param \DateTimeZone|string $timezone
1738 *
1739 * @return static
1740 */
1741 public function shiftTimezone($timezone)
1742 {
1743 $this->tzName = $timezone;
1744 $this->timezone = $timezone;
1745
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001746 if ($this->startDate) {
1747 $this->setStartDate($this->startDate->shiftTimezone($timezone));
1748 }
1749
1750 if ($this->endDate) {
1751 $this->setEndDate($this->endDate->shiftTimezone($timezone));
1752 }
1753
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001754 return $this;
1755 }
1756
1757 /**
1758 * Returns the end is set, else calculated from start an recurrences.
1759 *
1760 * @param string|null $rounding Optional rounding 'floor', 'ceil', 'round' using the period interval.
1761 *
1762 * @return CarbonInterface
1763 */
1764 public function calculateEnd(string $rounding = null)
1765 {
1766 if ($end = $this->getEndDate($rounding)) {
1767 return $end;
1768 }
1769
1770 if ($this->dateInterval->isEmpty()) {
1771 return $this->getStartDate($rounding);
1772 }
1773
1774 $date = $this->getEndFromRecurrences() ?? $this->iterateUntilEnd();
1775
1776 if ($date && $rounding) {
1777 $date = $date->avoidMutation()->round($this->getDateInterval(), $rounding);
1778 }
1779
1780 return $date;
1781 }
1782
1783 /**
1784 * @return CarbonInterface|null
1785 */
1786 private function getEndFromRecurrences()
1787 {
1788 if ($this->recurrences === null) {
1789 throw new UnreachableException(
1790 "Could not calculate period end without either explicit end or recurrences.\n".
1791 "If you're looking for a forever-period, use ->setRecurrences(INF)."
1792 );
1793 }
1794
1795 if ($this->recurrences === INF) {
1796 $start = $this->getStartDate();
1797
1798 return $start < $start->avoidMutation()->add($this->getDateInterval())
1799 ? CarbonImmutable::endOfTime()
1800 : CarbonImmutable::startOfTime();
1801 }
1802
1803 if ($this->filters === [[static::RECURRENCES_FILTER, null]]) {
1804 return $this->getStartDate()->avoidMutation()->add(
1805 $this->getDateInterval()->times(
1806 $this->recurrences - ($this->isStartExcluded() ? 0 : 1)
1807 )
1808 );
1809 }
1810
1811 return null;
1812 }
1813
1814 /**
1815 * @return CarbonInterface|null
1816 */
1817 private function iterateUntilEnd()
1818 {
1819 $attempts = 0;
1820 $date = null;
1821
1822 foreach ($this as $date) {
1823 if (++$attempts > static::END_MAX_ATTEMPTS) {
1824 throw new UnreachableException(
1825 'Could not calculate period end after iterating '.static::END_MAX_ATTEMPTS.' times.'
1826 );
1827 }
1828 }
1829
1830 return $date;
1831 }
1832
1833 /**
1834 * Returns true if the current period overlaps the given one (if 1 parameter passed)
1835 * or the period between 2 dates (if 2 parameters passed).
1836 *
1837 * @param CarbonPeriod|\DateTimeInterface|Carbon|CarbonImmutable|string $rangeOrRangeStart
1838 * @param \DateTimeInterface|Carbon|CarbonImmutable|string|null $rangeEnd
1839 *
1840 * @return bool
1841 */
1842 public function overlaps($rangeOrRangeStart, $rangeEnd = null)
1843 {
1844 $range = $rangeEnd ? static::create($rangeOrRangeStart, $rangeEnd) : $rangeOrRangeStart;
1845
1846 if (!($range instanceof self)) {
1847 $range = static::create($range);
1848 }
1849
1850 [$start, $end] = $this->orderCouple($this->getStartDate(), $this->calculateEnd());
1851 [$rangeStart, $rangeEnd] = $this->orderCouple($range->getStartDate(), $range->calculateEnd());
1852
1853 return $end > $rangeStart && $rangeEnd > $start;
1854 }
1855
1856 /**
1857 * Execute a given function on each date of the period.
1858 *
1859 * @example
1860 * ```
1861 * Carbon::create('2020-11-29')->daysUntil('2020-12-24')->forEach(function (Carbon $date) {
1862 * echo $date->diffInDays('2020-12-25')." days before Christmas!\n";
1863 * });
1864 * ```
1865 *
1866 * @param callable $callback
1867 */
1868 public function forEach(callable $callback)
1869 {
1870 foreach ($this as $date) {
1871 $callback($date);
1872 }
1873 }
1874
1875 /**
1876 * Execute a given function on each date of the period and yield the result of this function.
1877 *
1878 * @example
1879 * ```
1880 * $period = Carbon::create('2020-11-29')->daysUntil('2020-12-24');
1881 * echo implode("\n", iterator_to_array($period->map(function (Carbon $date) {
1882 * return $date->diffInDays('2020-12-25').' days before Christmas!';
1883 * })));
1884 * ```
1885 *
1886 * @param callable $callback
1887 *
1888 * @return \Generator
1889 */
1890 public function map(callable $callback)
1891 {
1892 foreach ($this as $date) {
1893 yield $callback($date);
1894 }
1895 }
1896
1897 /**
1898 * Determines if the instance is equal to another.
1899 * Warning: if options differ, instances wil never be equal.
1900 *
1901 * @param mixed $period
1902 *
1903 * @see equalTo()
1904 *
1905 * @return bool
1906 */
1907 public function eq($period): bool
1908 {
1909 return $this->equalTo($period);
1910 }
1911
1912 /**
1913 * Determines if the instance is equal to another.
1914 * Warning: if options differ, instances wil never be equal.
1915 *
1916 * @param mixed $period
1917 *
1918 * @return bool
1919 */
1920 public function equalTo($period): bool
1921 {
1922 if (!($period instanceof self)) {
1923 $period = self::make($period);
1924 }
1925
1926 $end = $this->getEndDate();
1927
1928 return $period !== null
1929 && $this->getDateInterval()->eq($period->getDateInterval())
1930 && $this->getStartDate()->eq($period->getStartDate())
1931 && ($end ? $end->eq($period->getEndDate()) : $this->getRecurrences() === $period->getRecurrences())
1932 && ($this->getOptions() & (~static::IMMUTABLE)) === ($period->getOptions() & (~static::IMMUTABLE));
1933 }
1934
1935 /**
1936 * Determines if the instance is not equal to another.
1937 * Warning: if options differ, instances wil never be equal.
1938 *
1939 * @param mixed $period
1940 *
1941 * @see notEqualTo()
1942 *
1943 * @return bool
1944 */
1945 public function ne($period): bool
1946 {
1947 return $this->notEqualTo($period);
1948 }
1949
1950 /**
1951 * Determines if the instance is not equal to another.
1952 * Warning: if options differ, instances wil never be equal.
1953 *
1954 * @param mixed $period
1955 *
1956 * @return bool
1957 */
1958 public function notEqualTo($period): bool
1959 {
1960 return !$this->eq($period);
1961 }
1962
1963 /**
1964 * Determines if the start date is before an other given date.
1965 * (Rather start/end are included by options is ignored.)
1966 *
1967 * @param mixed $date
1968 *
1969 * @return bool
1970 */
1971 public function startsBefore($date = null): bool
1972 {
1973 return $this->getStartDate()->lessThan($this->resolveCarbon($date));
1974 }
1975
1976 /**
1977 * Determines if the start date is before or the same as a given date.
1978 * (Rather start/end are included by options is ignored.)
1979 *
1980 * @param mixed $date
1981 *
1982 * @return bool
1983 */
1984 public function startsBeforeOrAt($date = null): bool
1985 {
1986 return $this->getStartDate()->lessThanOrEqualTo($this->resolveCarbon($date));
1987 }
1988
1989 /**
1990 * Determines if the start date is after an other given date.
1991 * (Rather start/end are included by options is ignored.)
1992 *
1993 * @param mixed $date
1994 *
1995 * @return bool
1996 */
1997 public function startsAfter($date = null): bool
1998 {
1999 return $this->getStartDate()->greaterThan($this->resolveCarbon($date));
2000 }
2001
2002 /**
2003 * Determines if the start date is after or the same as a given date.
2004 * (Rather start/end are included by options is ignored.)
2005 *
2006 * @param mixed $date
2007 *
2008 * @return bool
2009 */
2010 public function startsAfterOrAt($date = null): bool
2011 {
2012 return $this->getStartDate()->greaterThanOrEqualTo($this->resolveCarbon($date));
2013 }
2014
2015 /**
2016 * Determines if the start date is the same as a given date.
2017 * (Rather start/end are included by options is ignored.)
2018 *
2019 * @param mixed $date
2020 *
2021 * @return bool
2022 */
2023 public function startsAt($date = null): bool
2024 {
2025 return $this->getStartDate()->equalTo($this->resolveCarbon($date));
2026 }
2027
2028 /**
2029 * Determines if the end date is before an other given date.
2030 * (Rather start/end are included by options is ignored.)
2031 *
2032 * @param mixed $date
2033 *
2034 * @return bool
2035 */
2036 public function endsBefore($date = null): bool
2037 {
2038 return $this->calculateEnd()->lessThan($this->resolveCarbon($date));
2039 }
2040
2041 /**
2042 * Determines if the end date is before or the same as a given date.
2043 * (Rather start/end are included by options is ignored.)
2044 *
2045 * @param mixed $date
2046 *
2047 * @return bool
2048 */
2049 public function endsBeforeOrAt($date = null): bool
2050 {
2051 return $this->calculateEnd()->lessThanOrEqualTo($this->resolveCarbon($date));
2052 }
2053
2054 /**
2055 * Determines if the end date is after an other given date.
2056 * (Rather start/end are included by options is ignored.)
2057 *
2058 * @param mixed $date
2059 *
2060 * @return bool
2061 */
2062 public function endsAfter($date = null): bool
2063 {
2064 return $this->calculateEnd()->greaterThan($this->resolveCarbon($date));
2065 }
2066
2067 /**
2068 * Determines if the end date is after or the same as a given date.
2069 * (Rather start/end are included by options is ignored.)
2070 *
2071 * @param mixed $date
2072 *
2073 * @return bool
2074 */
2075 public function endsAfterOrAt($date = null): bool
2076 {
2077 return $this->calculateEnd()->greaterThanOrEqualTo($this->resolveCarbon($date));
2078 }
2079
2080 /**
2081 * Determines if the end date is the same as a given date.
2082 * (Rather start/end are included by options is ignored.)
2083 *
2084 * @param mixed $date
2085 *
2086 * @return bool
2087 */
2088 public function endsAt($date = null): bool
2089 {
2090 return $this->calculateEnd()->equalTo($this->resolveCarbon($date));
2091 }
2092
2093 /**
2094 * Return true if start date is now or later.
2095 * (Rather start/end are included by options is ignored.)
2096 *
2097 * @return bool
2098 */
2099 public function isStarted(): bool
2100 {
2101 return $this->startsBeforeOrAt();
2102 }
2103
2104 /**
2105 * Return true if end date is now or later.
2106 * (Rather start/end are included by options is ignored.)
2107 *
2108 * @return bool
2109 */
2110 public function isEnded(): bool
2111 {
2112 return $this->endsBeforeOrAt();
2113 }
2114
2115 /**
2116 * Return true if now is between start date (included) and end date (excluded).
2117 * (Rather start/end are included by options is ignored.)
2118 *
2119 * @return bool
2120 */
2121 public function isInProgress(): bool
2122 {
2123 return $this->isStarted() && !$this->isEnded();
2124 }
2125
2126 /**
2127 * Round the current instance at the given unit with given precision if specified and the given function.
2128 *
2129 * @param string $unit
2130 * @param float|int|string|\DateInterval|null $precision
2131 * @param string $function
2132 *
2133 * @return $this
2134 */
2135 public function roundUnit($unit, $precision = 1, $function = 'round')
2136 {
2137 $this->setStartDate($this->getStartDate()->roundUnit($unit, $precision, $function));
2138
2139 if ($this->endDate) {
2140 $this->setEndDate($this->getEndDate()->roundUnit($unit, $precision, $function));
2141 }
2142
2143 $this->setDateInterval($this->getDateInterval()->roundUnit($unit, $precision, $function));
2144
2145 return $this;
2146 }
2147
2148 /**
2149 * Truncate the current instance at the given unit with given precision if specified.
2150 *
2151 * @param string $unit
2152 * @param float|int|string|\DateInterval|null $precision
2153 *
2154 * @return $this
2155 */
2156 public function floorUnit($unit, $precision = 1)
2157 {
2158 return $this->roundUnit($unit, $precision, 'floor');
2159 }
2160
2161 /**
2162 * Ceil the current instance at the given unit with given precision if specified.
2163 *
2164 * @param string $unit
2165 * @param float|int|string|\DateInterval|null $precision
2166 *
2167 * @return $this
2168 */
2169 public function ceilUnit($unit, $precision = 1)
2170 {
2171 return $this->roundUnit($unit, $precision, 'ceil');
2172 }
2173
2174 /**
2175 * Round the current instance second with given precision if specified (else period interval is used).
2176 *
2177 * @param float|int|string|\DateInterval|null $precision
2178 * @param string $function
2179 *
2180 * @return $this
2181 */
2182 public function round($precision = null, $function = 'round')
2183 {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01002184 return $this->roundWith(
2185 $precision ?? $this->getDateInterval()->setLocalTranslator(TranslatorImmutable::get('en'))->forHumans(),
2186 $function
2187 );
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02002188 }
2189
2190 /**
2191 * Round the current instance second with given precision if specified (else period interval is used).
2192 *
2193 * @param float|int|string|\DateInterval|null $precision
2194 *
2195 * @return $this
2196 */
2197 public function floor($precision = null)
2198 {
2199 return $this->round($precision, 'floor');
2200 }
2201
2202 /**
2203 * Ceil the current instance second with given precision if specified (else period interval is used).
2204 *
2205 * @param float|int|string|\DateInterval|null $precision
2206 *
2207 * @return $this
2208 */
2209 public function ceil($precision = null)
2210 {
2211 return $this->round($precision, 'ceil');
2212 }
2213
2214 /**
2215 * Specify data which should be serialized to JSON.
2216 *
2217 * @link https://php.net/manual/en/jsonserializable.jsonserialize.php
2218 *
2219 * @return CarbonInterface[]
2220 */
2221 #[ReturnTypeWillChange]
2222 public function jsonSerialize()
2223 {
2224 return $this->toArray();
2225 }
2226
2227 /**
2228 * Return true if the given date is between start and end.
2229 *
2230 * @param \Carbon\Carbon|\Carbon\CarbonPeriod|\Carbon\CarbonInterval|\DateInterval|\DatePeriod|\DateTimeInterface|string|null $date
2231 *
2232 * @return bool
2233 */
2234 public function contains($date = null): bool
2235 {
2236 $startMethod = 'startsBefore'.($this->isStartIncluded() ? 'OrAt' : '');
2237 $endMethod = 'endsAfter'.($this->isEndIncluded() ? 'OrAt' : '');
2238
2239 return $this->$startMethod($date) && $this->$endMethod($date);
2240 }
2241
2242 /**
2243 * Return true if the current period follows a given other period (with no overlap).
2244 * For instance, [2019-08-01 -> 2019-08-12] follows [2019-07-29 -> 2019-07-31]
2245 * Note than in this example, follows() would be false if 2019-08-01 or 2019-07-31 was excluded by options.
2246 *
2247 * @param \Carbon\CarbonPeriod|\DatePeriod|string $period
2248 *
2249 * @return bool
2250 */
2251 public function follows($period, ...$arguments): bool
2252 {
2253 $period = $this->resolveCarbonPeriod($period, ...$arguments);
2254
2255 return $this->getIncludedStartDate()->equalTo($period->getIncludedEndDate()->add($period->getDateInterval()));
2256 }
2257
2258 /**
2259 * Return true if the given other period follows the current one (with no overlap).
2260 * For instance, [2019-07-29 -> 2019-07-31] is followed by [2019-08-01 -> 2019-08-12]
2261 * Note than in this example, isFollowedBy() would be false if 2019-08-01 or 2019-07-31 was excluded by options.
2262 *
2263 * @param \Carbon\CarbonPeriod|\DatePeriod|string $period
2264 *
2265 * @return bool
2266 */
2267 public function isFollowedBy($period, ...$arguments): bool
2268 {
2269 $period = $this->resolveCarbonPeriod($period, ...$arguments);
2270
2271 return $period->follows($this);
2272 }
2273
2274 /**
2275 * Return true if the given period either follows or is followed by the current one.
2276 *
2277 * @see follows()
2278 * @see isFollowedBy()
2279 *
2280 * @param \Carbon\CarbonPeriod|\DatePeriod|string $period
2281 *
2282 * @return bool
2283 */
2284 public function isConsecutiveWith($period, ...$arguments): bool
2285 {
2286 return $this->follows($period, ...$arguments) || $this->isFollowedBy($period, ...$arguments);
2287 }
2288
2289 /**
2290 * Update properties after removing built-in filters.
2291 *
2292 * @return void
2293 */
2294 protected function updateInternalState()
2295 {
2296 if (!$this->hasFilter(static::END_DATE_FILTER)) {
2297 $this->endDate = null;
2298 }
2299
2300 if (!$this->hasFilter(static::RECURRENCES_FILTER)) {
2301 $this->recurrences = null;
2302 }
2303 }
2304
2305 /**
2306 * Create a filter tuple from raw parameters.
2307 *
2308 * Will create an automatic filter callback for one of Carbon's is* methods.
2309 *
2310 * @param array $parameters
2311 *
2312 * @return array
2313 */
2314 protected function createFilterTuple(array $parameters)
2315 {
2316 $method = array_shift($parameters);
2317
2318 if (!$this->isCarbonPredicateMethod($method)) {
2319 return [$method, array_shift($parameters)];
2320 }
2321
2322 return [function ($date) use ($method, $parameters) {
2323 return ([$date, $method])(...$parameters);
2324 }, $method];
2325 }
2326
2327 /**
2328 * Return whether given callable is a string pointing to one of Carbon's is* methods
2329 * and should be automatically converted to a filter callback.
2330 *
2331 * @param callable $callable
2332 *
2333 * @return bool
2334 */
2335 protected function isCarbonPredicateMethod($callable)
2336 {
2337 return \is_string($callable) && str_starts_with($callable, 'is') &&
2338 (method_exists($this->dateClass, $callable) || ([$this->dateClass, 'hasMacro'])($callable));
2339 }
2340
2341 /**
2342 * Recurrences filter callback (limits number of recurrences).
2343 *
2344 * @SuppressWarnings(PHPMD.UnusedFormalParameter)
2345 *
2346 * @param \Carbon\Carbon $current
2347 * @param int $key
2348 *
2349 * @return bool|string
2350 */
2351 protected function filterRecurrences($current, $key)
2352 {
2353 if ($key < $this->recurrences) {
2354 return true;
2355 }
2356
2357 return static::END_ITERATION;
2358 }
2359
2360 /**
2361 * End date filter callback.
2362 *
2363 * @param \Carbon\Carbon $current
2364 *
2365 * @return bool|string
2366 */
2367 protected function filterEndDate($current)
2368 {
2369 if (!$this->isEndExcluded() && $current == $this->endDate) {
2370 return true;
2371 }
2372
2373 if ($this->dateInterval->invert ? $current > $this->endDate : $current < $this->endDate) {
2374 return true;
2375 }
2376
2377 return static::END_ITERATION;
2378 }
2379
2380 /**
2381 * End iteration filter callback.
2382 *
2383 * @return string
2384 */
2385 protected function endIteration()
2386 {
2387 return static::END_ITERATION;
2388 }
2389
2390 /**
2391 * Handle change of the parameters.
2392 */
2393 protected function handleChangedParameters()
2394 {
2395 if (($this->getOptions() & static::IMMUTABLE) && $this->dateClass === Carbon::class) {
2396 $this->setDateClass(CarbonImmutable::class);
2397 } elseif (!($this->getOptions() & static::IMMUTABLE) && $this->dateClass === CarbonImmutable::class) {
2398 $this->setDateClass(Carbon::class);
2399 }
2400
2401 $this->validationResult = null;
2402 }
2403
2404 /**
2405 * Validate current date and stop iteration when necessary.
2406 *
2407 * Returns true when current date is valid, false if it is not, or static::END_ITERATION
2408 * when iteration should be stopped.
2409 *
2410 * @return bool|string
2411 */
2412 protected function validateCurrentDate()
2413 {
2414 if ($this->current === null) {
2415 $this->rewind();
2416 }
2417
2418 // Check after the first rewind to avoid repeating the initial validation.
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01002419 return $this->validationResult ?? ($this->validationResult = $this->checkFilters());
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02002420 }
2421
2422 /**
2423 * Check whether current value and key pass all the filters.
2424 *
2425 * @return bool|string
2426 */
2427 protected function checkFilters()
2428 {
2429 $current = $this->prepareForReturn($this->current);
2430
2431 foreach ($this->filters as $tuple) {
2432 $result = \call_user_func(
2433 $tuple[0],
2434 $current->avoidMutation(),
2435 $this->key,
2436 $this
2437 );
2438
2439 if ($result === static::END_ITERATION) {
2440 return static::END_ITERATION;
2441 }
2442
2443 if (!$result) {
2444 return false;
2445 }
2446 }
2447
2448 return true;
2449 }
2450
2451 /**
2452 * Prepare given date to be returned to the external logic.
2453 *
2454 * @param CarbonInterface $date
2455 *
2456 * @return CarbonInterface
2457 */
2458 protected function prepareForReturn(CarbonInterface $date)
2459 {
2460 $date = ([$this->dateClass, 'make'])($date);
2461
2462 if ($this->timezone) {
2463 $date = $date->setTimezone($this->timezone);
2464 }
2465
2466 return $date;
2467 }
2468
2469 /**
2470 * Keep incrementing the current date until a valid date is found or the iteration is ended.
2471 *
2472 * @throws RuntimeException
2473 *
2474 * @return void
2475 */
2476 protected function incrementCurrentDateUntilValid()
2477 {
2478 $attempts = 0;
2479
2480 do {
2481 $this->current = $this->current->add($this->dateInterval);
2482
2483 $this->validationResult = null;
2484
2485 if (++$attempts > static::NEXT_MAX_ATTEMPTS) {
2486 throw new UnreachableException('Could not find next valid date.');
2487 }
2488 } while ($this->validateCurrentDate() === false);
2489 }
2490
2491 /**
2492 * Call given macro.
2493 *
2494 * @param string $name
2495 * @param array $parameters
2496 *
2497 * @return mixed
2498 */
2499 protected function callMacro($name, $parameters)
2500 {
2501 $macro = static::$macros[$name];
2502
2503 if ($macro instanceof Closure) {
2504 $boundMacro = @$macro->bindTo($this, static::class) ?: @$macro->bindTo(null, static::class);
2505
2506 return ($boundMacro ?: $macro)(...$parameters);
2507 }
2508
2509 return $macro(...$parameters);
2510 }
2511
2512 /**
2513 * Return the Carbon instance passed through, a now instance in the same timezone
2514 * if null given or parse the input if string given.
2515 *
2516 * @param \Carbon\Carbon|\Carbon\CarbonPeriod|\Carbon\CarbonInterval|\DateInterval|\DatePeriod|\DateTimeInterface|string|null $date
2517 *
2518 * @return \Carbon\CarbonInterface
2519 */
2520 protected function resolveCarbon($date = null)
2521 {
2522 return $this->getStartDate()->nowWithSameTz()->carbonize($date);
2523 }
2524
2525 /**
2526 * Resolve passed arguments or DatePeriod to a CarbonPeriod object.
2527 *
2528 * @param mixed $period
2529 * @param mixed ...$arguments
2530 *
2531 * @return static
2532 */
2533 protected function resolveCarbonPeriod($period, ...$arguments)
2534 {
2535 if ($period instanceof self) {
2536 return $period;
2537 }
2538
2539 return $period instanceof DatePeriod
2540 ? static::instance($period)
2541 : static::create($period, ...$arguments);
2542 }
2543
2544 private function orderCouple($first, $second): array
2545 {
2546 return $first > $second ? [$second, $first] : [$first, $second];
2547 }
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01002548
2549 private function makeDateTime($value): ?DateTimeInterface
2550 {
2551 if ($value instanceof DateTimeInterface) {
2552 return $value;
2553 }
2554
2555 if (\is_string($value)) {
2556 $value = trim($value);
2557
2558 if (!preg_match('/^P[0-9T]/', $value) &&
2559 !preg_match('/^R[0-9]/', $value) &&
2560 preg_match('/[a-z0-9]/i', $value)
2561 ) {
2562 return Carbon::parse($value, $this->tzName);
2563 }
2564 }
2565
2566 return null;
2567 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02002568}