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