Subversion-Projekte lars-tiefland.laravel_shop

Revision

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