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