Subversion-Projekte lars-tiefland.laravel_shop

Revision

Zur aktuellen Revision | Details | Letzte Änderung | Log anzeigen | RSS feed

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