<?php
/**
* League.Period (https://period.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Period;
use DateInterval;
use DatePeriod;
use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
use JsonSerializable;
use function array_filter;
use function array_keys;
use function implode;
use function sprintf;
/**
* A immutable value object class to manipulate Time interval.
*
* @package League.period
* @author Ignace Nyamagana Butera <nyamsprod@gmail.com>
* @since 1.0.0
*/
final class Period implements JsonSerializable
{
private const ISO8601_FORMAT = 'Y-m-d\TH:i:s.u\Z';
private const BOUNDARY_TYPE = [
self::INCLUDE_START_EXCLUDE_END => 1,
self::INCLUDE_ALL => 1,
self::EXCLUDE_START_INCLUDE_END => 1,
self::EXCLUDE_ALL => 1,
];
public const INCLUDE_START_EXCLUDE_END = '[)';
public const EXCLUDE_START_INCLUDE_END = '(]';
public const EXCLUDE_ALL = '()';
public const INCLUDE_ALL = '[]';
/** @var DateTimeImmutable */
private $startDate;
/** @var DateTimeImmutable */
private $endDate;
/** @var string */
private $boundaryType;
/**
* Creates a new instance.
*
* @param Datepoint|DateTimeInterface|int|string $startDate the starting datepoint
* @param Datepoint|DateTimeInterface|int|string $endDate the ending datepoint
*
* @throws Exception If $startDate is greater than $endDate
*/
public function __construct($startDate, $endDate, string $boundaryType = self::INCLUDE_START_EXCLUDE_END)
{
$startDate = self::filterDatepoint($startDate);
$endDate = self::filterDatepoint($endDate);
if ($startDate > $endDate) {
throw new Exception('The ending datepoint must be greater or equal to the starting datepoint');
}
if (!isset(self::BOUNDARY_TYPE[$boundaryType])) {
throw new Exception(sprintf(
'The boundary type `%s` is invalid. The only valid values are %s',
$boundaryType,
'`'.implode('`, `', array_keys(self::BOUNDARY_TYPE)).'`'
));
}
$this->startDate = $startDate;
$this->endDate = $endDate;
$this->boundaryType = $boundaryType;
}
/**
* Returns a DateTimeImmutable instance.
*
* @param Datepoint|DateTimeInterface|int|string $datepoint a Datepoint
*/
private static function filterDatepoint($datepoint): DateTimeImmutable
{
if ($datepoint instanceof DateTimeImmutable) {
return $datepoint;
}
if ($datepoint instanceof DateTimeInterface) {
return new DateTimeImmutable($datepoint->format('Y-m-d H:i:s.u'), $datepoint->getTimezone());
}
$timestamp = $datepoint;
if (is_int($timestamp) || false !== ($timestamp = filter_var($datepoint, FILTER_VALIDATE_INT))) {
return new DateTimeImmutable('@'.$timestamp);
}
return new DateTimeImmutable($datepoint);
}
/**
* Returns a DateInterval instance.
*
* @param Period|DateInterval|Duration|string|int $duration a Duration
*/
private static function filterDuration($duration): DateInterval
{
if ($duration instanceof DateInterval) {
return $duration;
}
if ($duration instanceof self) {
return $duration->dateInterval();
}
return Duration::create($duration);
}
/**************************************************
* Named constructors
**************************************************/
/**
* @inheritDoc
*/
public static function __set_state(array $interval)
{
return new self(
$interval['startDate'],
$interval['endDate'],
$interval['boundaryType'] ?? self::INCLUDE_START_EXCLUDE_END
);
}
/**
* Creates new instance from a starting date endpoint and a duration.
*
* @param Datepoint|DateTimeInterface|int|string $startDate the starting date endpoint
* @param DateInterval|Duration|string|int $duration a Duration
*/
public static function after($startDate, $duration, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
{
$startDate = self::filterDatepoint($startDate);
return new self($startDate, $startDate->add(self::filterDuration($duration)), $boundaryType);
}
/**
* Creates new instance from a ending date endpoint and a duration.
*
* @param Datepoint|DateTimeInterface|int|string $endDate the ending date endpoint
* @param DateInterval|Duration|string|int $duration a Duration
*/
public static function before($endDate, $duration, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
{
$endDate = self::filterDatepoint($endDate);
return new self($endDate->sub(self::filterDuration($duration)), $endDate, $boundaryType);
}
/**
* Creates new instance where the given duration is simultaneously
* subtracted from and added to the date endpoint.
*
* @param Datepoint|DateTimeInterface|int|string $datepoint a Date endpoint
* @param DateInterval|Duration|string|int $duration a Duration
*/
public static function around($datepoint, $duration, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
{
$datepoint = self::filterDatepoint($datepoint);
$duration = self::filterDuration($duration);
return new self($datepoint->sub($duration), $datepoint->add($duration), $boundaryType);
}
/**
* Creates new instance from a DatePeriod.
*/
public static function fromDatePeriod(DatePeriod $datePeriod, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
{
return new self($datePeriod->getStartDate(), $datePeriod->getEndDate(), $boundaryType);
}
/**
* Creates new instance for a specific year.
*/
public static function fromYear(int $year, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
{
$startDate = (new DateTimeImmutable())->setDate($year, 1, 1)->setTime(0, 0);
return new self($startDate, $startDate->add(new DateInterval('P1Y')), $boundaryType);
}
/**
* Creates new instance for a specific ISO year.
*/
public static function fromIsoYear(int $year, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
{
return new self(
(new DateTimeImmutable())->setISODate($year, 1)->setTime(0, 0),
(new DateTimeImmutable())->setISODate(++$year, 1)->setTime(0, 0),
$boundaryType
);
}
/**
* Creates new instance for a specific year and semester.
*/
public static function fromSemester(int $year, int $semester = 1, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
{
$month = (($semester - 1) * 6) + 1;
$startDate = (new DateTimeImmutable())->setDate($year, $month, 1)->setTime(0, 0);
return new self($startDate, $startDate->add(new DateInterval('P6M')), $boundaryType);
}
/**
* Creates new instance for a specific year and quarter.
*/
public static function fromQuarter(int $year, int $quarter = 1, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
{
$month = (($quarter - 1) * 3) + 1;
$startDate = (new DateTimeImmutable())->setDate($year, $month, 1)->setTime(0, 0);
return new self($startDate, $startDate->add(new DateInterval('P3M')), $boundaryType);
}
/**
* Creates new instance for a specific year and month.
*/
public static function fromMonth(int $year, int $month = 1, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
{
$startDate = (new DateTimeImmutable())->setDate($year, $month, 1)->setTime(0, 0);
return new self($startDate, $startDate->add(new DateInterval('P1M')), $boundaryType);
}
/**
* Creates new instance for a specific ISO8601 week.
*/
public static function fromIsoWeek(int $year, int $week = 1, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
{
$startDate = (new DateTimeImmutable())->setISODate($year, $week, 1)->setTime(0, 0);
return new self($startDate, $startDate->add(new DateInterval('P7D')), $boundaryType);
}
/**
* Creates new instance for a specific year, month and day.
*/
public static function fromDay(int $year, int $month = 1, int $day = 1, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
{
$startDate = (new DateTimeImmutable())->setDate($year, $month, $day)->setTime(0, 0);
return new self($startDate, $startDate->add(new DateInterval('P1D')), $boundaryType);
}
/**
* Creates new instance for Datepoint.
*/
public static function fromDatepoint(DateTimeInterface $startDate, DateTimeInterface $endDate, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
{
return new self($startDate, $endDate, $boundaryType);
}
/**************************************************
* Basic getters
**************************************************/
/**
* Returns the starting date endpoint.
*/
public function getStartDate(): DateTimeImmutable
{
return $this->startDate;
}
/**
* Returns the ending date endpoint.
*/
public function getEndDate(): DateTimeImmutable
{
return $this->endDate;
}
/**
* Returns the instance boundary type.
*/
public function getBoundaryType(): string
{
return $this->boundaryType;
}
/**
* Returns the instance duration as expressed in seconds.
*/
public function timeDuration(): float
{
return $this->endDate->getTimestamp() - $this->startDate->getTimestamp();
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated 4.12.0 This method will be removed in the next major point release
* @see Period::timeDuration()
*
* Returns the instance duration as expressed in seconds.
*/
public function getTimestampInterval(): float
{
return $this->timeDuration();
}
/**
* Returns the instance duration as a DateInterval object.
*/
public function dateInterval(): DateInterval
{
return $this->startDate->diff($this->endDate);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated 4.12.0 This method will be removed in the next major point release
* @see Period::dateInterval()
*
* Returns the instance duration as a DateInterval object.
*/
public function getDateInterval(): DateInterval
{
return $this->dateInterval();
}
/**************************************************
* String representation
**************************************************/
/**
* Returns the string representation as a ISO8601 interval format.
*
* @deprecated 4.10.0 This method will be removed in the next major point release
* @see ::toIso8601()
*/
public function __toString()
{
return $this->toIso8601();
}
/**
* Returns the string representation as a ISO8601 interval format.
*
* @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
* @param ?string $format
*/
public function toIso8601(?string $format = null): string
{
$utc = new DateTimeZone('UTC');
$format = $format ?? self::ISO8601_FORMAT;
$startDate = $this->startDate->setTimezone($utc)->format($format);
$endDate = $this->endDate->setTimezone($utc)->format($format);
return $startDate.'/'.$endDate;
}
/**
* Returns the JSON representation of an instance.
*
* Based on the JSON representation of dates as
* returned by Javascript Date.toJSON() method.
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toJSON
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
*
* @return array<string>
*/
public function jsonSerialize(): array
{
[$startDate, $endDate] = explode('/', $this->toIso8601(), 2);
return ['startDate' => $startDate, 'endDate' => $endDate];
}
/**
* Returns the mathematical representation of an instance as a left close, right open interval.
*
* @see https://en.wikipedia.org/wiki/Interval_(mathematics)#Notations_for_intervals
* @see https://php.net/manual/en/function.date.php
* @see https://www.postgresql.org/docs/9.3/static/rangetypes.html
*
* @param string $format the format of the outputted date string
*/
public function toIso80000(string $format): string
{
return $this->boundaryType[0]
.$this->startDate->format($format)
.', '
.$this->endDate->format($format)
.$this->boundaryType[1];
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated 4.12.0 This method will be removed in the next major point release
* @see Period::toIso80000()
*
* @param string $format the format of the outputted date string
*
* Returns the mathematical representation of an instance as a left close, right open interval.
*
* @see https://en.wikipedia.org/wiki/Interval_(mathematics)#Notations_for_intervals
* @see https://php.net/manual/en/function.date.php
* @see https://www.postgresql.org/docs/9.3/static/rangetypes.html
*/
public function format(string $format): string
{
return $this->toIso80000($format);
}
/**************************************************
* Boundary related methods
**************************************************/
/**
* Tells whether the start datepoint is included in the boundary.
*/
public function isStartIncluded(): bool
{
return '[' === $this->boundaryType[0];
}
/**
* Tells whether the start datepoint is excluded from the boundary.
*/
public function isStartExcluded(): bool
{
return '(' === $this->boundaryType[0];
}
/**
* Tells whether the end datepoint is included in the boundary.
*/
public function isEndIncluded(): bool
{
return ']' === $this->boundaryType[1];
}
/**
* Tells whether the end datepoint is excluded from the boundary.
*/
public function isEndExcluded(): bool
{
return ')' === $this->boundaryType[1];
}
/**************************************************
* Duration comparison methods
**************************************************/
/**
* Compares two instances according to their duration.
*
* Returns:
* <ul>
* <li> -1 if the current Interval is lesser than the submitted Interval object</li>
* <li> 1 if the current Interval is greater than the submitted Interval object</li>
* <li> 0 if both Interval objects have the same duration</li>
* </ul>
*/
public function durationCompare(self $interval): int
{
return $this->startDate->add($this->dateInterval())
<=> $this->startDate->add($interval->dateInterval());
}
/**
* Tells whether the current instance duration is equal to the submitted one.
*/
public function durationEquals(self $interval): bool
{
return 0 === $this->durationCompare($interval);
}
/**
* Tells whether the current instance duration is greater than the submitted one.
*/
public function durationGreaterThan(self $interval): bool
{
return 1 === $this->durationCompare($interval);
}
/**
* Tells whether the current instance duration is less than the submitted one.
*/
public function durationLessThan(self $interval): bool
{
return -1 === $this->durationCompare($interval);
}
/**************************************************
* Relation methods
**************************************************/
/**
* Tells whether an instance is entirely before the specified index.
*
* The index can be a DateTimeInterface object or another Period object.
*
* [--------------------)
* [--------------------)
*
* @param Period|Datepoint|\DateTimeInterface|int|string $index a datepoint or a Period object
*/
public function isBefore($index): bool
{
if ($index instanceof self) {
return $this->endDate < $index->startDate
|| ($this->endDate == $index->startDate && $this->boundaryType[1] !== $index->boundaryType[0]);
}
$datepoint = self::filterDatepoint($index);
return $this->endDate < $datepoint
|| ($this->endDate == $datepoint && ')' === $this->boundaryType[1]);
}
/**
* Tells whether the current instance end date meets the interval start date.
*
* [--------------------)
* [--------------------)
*/
public function bordersOnStart(self $interval): bool
{
return $this->endDate == $interval->startDate
&& '][' !== $this->boundaryType[1].$interval->boundaryType[0];
}
/**
* Tells whether two intervals share the same start datepoint
* and the same starting boundary type.
*
* [----------)
* [--------------------)
*
* or
*
* [--------------------)
* [---------)
*
* @param Period|Datepoint|\DateTimeInterface|int|string $index a datepoint or a Period object
*/
public function isStartedBy($index): bool
{
if ($index instanceof self) {
return $this->startDate == $index->startDate
&& $this->boundaryType[0] === $index->boundaryType[0];
}
$index = self::filterDatepoint($index);
return $index == $this->startDate && '[' === $this->boundaryType[0];
}
/**
* Tells whether an instance is fully contained in the specified interval.
*
* [----------)
* [--------------------)
*/
public function isDuring(self $interval): bool
{
return $interval->containsInterval($this);
}
/**
* Tells whether an instance fully contains the specified index.
*
* The index can be a DateTimeInterface object or another Period object.
*
* @param Period|Datepoint|\DateTimeInterface|int|string $index a datepoint or a Period object
*/
public function contains($index): bool
{
if ($index instanceof self) {
return $this->containsInterval($index);
}
return $this->containsDatepoint(self::filterDatepoint($index), $this->boundaryType);
}
/**
* Tells whether an instance fully contains another instance.
*
* [--------------------)
* [----------)
*/
private function containsInterval(self $interval): bool
{
if ($this->startDate < $interval->startDate && $this->endDate > $interval->endDate) {
return true;
}
if ($this->startDate == $interval->startDate && $this->endDate == $interval->endDate) {
return $this->boundaryType === $interval->boundaryType || '[]' === $this->boundaryType;
}
if ($this->startDate == $interval->startDate) {
return ($this->boundaryType[0] === $interval->boundaryType[0] || '[' === $this->boundaryType[0])
&& $this->containsDatepoint($this->startDate->add($interval->getDateInterval()), $this->boundaryType);
}
if ($this->endDate == $interval->endDate) {
return ($this->boundaryType[1] === $interval->boundaryType[1] || ']' === $this->boundaryType[1])
&& $this->containsDatepoint($this->endDate->sub($interval->getDateInterval()), $this->boundaryType);
}
return false;
}
/**
* Tells whether an instance contains a date endpoint.
*
* [------|------------)
*/
private function containsDatepoint(DateTimeInterface $datepoint, string $boundaryType): bool
{
switch ($boundaryType) {
case self::EXCLUDE_ALL:
return $datepoint > $this->startDate && $datepoint < $this->endDate;
case self::INCLUDE_ALL:
return $datepoint >= $this->startDate && $datepoint <= $this->endDate;
case self::EXCLUDE_START_INCLUDE_END:
return $datepoint > $this->startDate && $datepoint <= $this->endDate;
case self::INCLUDE_START_EXCLUDE_END:
default:
return $datepoint >= $this->startDate && $datepoint < $this->endDate;
}
}
/**
* Tells whether two intervals share the same datepoints.
*
* [--------------------)
* [--------------------)
*/
public function equals(self $interval): bool
{
return $this->startDate == $interval->startDate
&& $this->endDate == $interval->endDate
&& $this->boundaryType === $interval->boundaryType;
}
/**
* Tells whether two intervals share the same end datepoint
* and the same ending boundary type.
*
* [----------)
* [--------------------)
*
* or
*
* [--------------------)
* [---------)
*
* @param Period|Datepoint|\DateTimeInterface|int|string $index a datepoint or a Period object
*/
public function isEndedBy($index): bool
{
if ($index instanceof self) {
return $this->endDate == $index->endDate
&& $this->boundaryType[1] === $index->boundaryType[1];
}
$index = self::filterDatepoint($index);
return $index == $this->endDate && ']' === $this->boundaryType[1];
}
/**
* Tells whether the current instance start date meets the interval end date.
*
* [--------------------)
* [--------------------)
*/
public function bordersOnEnd(self $interval): bool
{
return $interval->bordersOnStart($this);
}
/**
* Tells whether an interval is entirely after the specified index.
* The index can be a DateTimeInterface object or another Period object.
*
* [--------------------)
* [--------------------)
*
* @param Period|Datepoint|\DateTimeInterface|int|string $index a datepoint or a Period object
*/
public function isAfter($index): bool
{
if ($index instanceof self) {
return $index->isBefore($this);
}
$datepoint = self::filterDatepoint($index);
return $this->startDate > $datepoint
|| ($this->startDate == $datepoint && '(' === $this->boundaryType[0]);
}
/**
* Tells whether two intervals abuts.
*
* [--------------------)
* [--------------------)
* or
* [--------------------)
* [--------------------)
*/
public function abuts(self $interval): bool
{
return $this->bordersOnStart($interval) || $this->bordersOnEnd($interval);
}
/**
* Tells whether two intervals overlaps.
*
* [--------------------)
* [--------------------)
*/
public function overlaps(self $interval): bool
{
return !$this->abuts($interval)
&& $this->startDate < $interval->endDate
&& $this->endDate > $interval->startDate;
}
/**************************************************
* Manipulating instance duration
**************************************************/
/**
* Returns the difference between two instances expressed in seconds.
*/
public function timeDurationDiff(self $interval): float
{
return $this->timeDuration() - $interval->timeDuration();
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated 4.12.0 This method will be removed in the next major point release
* @see Period::timeDurationDiff()
*
* Returns the difference between two instances expressed in seconds.
*/
public function timestampIntervalDiff(self $interval): float
{
return $this->timeDurationDiff($interval);
}
/**
* Returns the difference between two instances expressed with a DateInterval object.
*/
public function dateIntervalDiff(self $interval): DateInterval
{
return $this->endDate->diff($this->startDate->add($interval->dateInterval()));
}
/**
* Allows iteration over a set of dates and times,
* recurring at regular intervals, over the instance.
*
* @see http://php.net/manual/en/dateperiod.construct.php
*
* @param Period|DateInterval|Duration|string|int $duration a Duration
*/
public function dateRangeForward($duration, int $option = 0): DatePeriod
{
return new DatePeriod($this->startDate, self::filterDuration($duration), $this->endDate, $option);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated 4.12.0 This method will be removed in the next major point release
* @see Period::dateRangeForward()
*
* @param Period|DateInterval|Duration|string|int $duration a Duration
*
* Allows iteration over a set of dates and times,
* recurring at regular intervals, over the instance.
*
* @see http://php.net/manual/en/dateperiod.construct.php
*/
public function getDatePeriod($duration, int $option = 0): DatePeriod
{
return $this->dateRangeForward($duration, $option);
}
/**
* Allows iteration over a set of dates and times,
* recurring at regular intervals, over the instance backwards starting from
* the instance ending date endpoint.
*
* @param Period|DateInterval|Duration|string|int $duration a Duration
*/
public function dateRangeBackwards($duration, int $option = 0): iterable
{
$duration = self::filterDuration($duration);
$date = $this->endDate;
if ((bool) ($option & DatePeriod::EXCLUDE_START_DATE)) {
$date = $this->endDate->sub($duration);
}
while ($date > $this->startDate) {
yield $date;
$date = $date->sub($duration);
}
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated 4.12.0 This method will be removed in the next major point release
* @see Period::dateRangeBackwards()
*
* @param Period|DateInterval|Duration|string|int $duration a Duration
*
* Allows iteration over a set of dates and times,
* recurring at regular intervals, over the instance backwards starting from
* the instance ending date endpoint.
*/
public function getDatePeriodBackwards($duration, int $option = 0): iterable
{
return $this->dateRangeBackwards($duration, $option);
}
/**
* Allows splitting an instance in smaller Period objects according to a given interval.
*
* The returned iterable Interval set is ordered so that:
* <ul>
* <li>The first returned object MUST share the starting datepoint of the parent object.</li>
* <li>The last returned object MUST share the ending datepoint of the parent object.</li>
* <li>The last returned object MUST have a duration equal or lesser than the submitted interval.</li>
* <li>All returned objects except for the first one MUST start immediately after the previously returned object</li>
* </ul>
*
* @param Period|DateInterval|Duration|string|int $duration a Duration
*
* @return iterable<Period>
*/
public function splitForward($duration): iterable
{
$duration = self::filterDuration($duration);
/** @var DateTimeImmutable $startDate */
foreach ($this->dateRangeForward($duration) as $startDate) {
$endDate = $startDate->add($duration);
if ($endDate > $this->endDate) {
$endDate = $this->endDate;
}
yield new self($startDate, $endDate, $this->boundaryType);
}
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @see Period::splitForward()
*
* Allows splitting an instance in smaller Period objects according to a given interval.
*
* The returned iterable Interval set is ordered so that:
* <ul>
* <li>The first returned object MUST share the starting datepoint of the parent object.</li>
* <li>The last returned object MUST share the ending datepoint of the parent object.</li>
* <li>The last returned object MUST have a duration equal or lesser than the submitted interval.</li>
* <li>All returned objects except for the first one MUST start immediately after the previously returned object</li>
* </ul>
*
* @param Period|DateInterval|Duration|string|int $duration a Duration
*
* @return iterable<Period>
*/
public function split($duration): iterable
{
return $this->splitForward($duration);
}
/**
* Allows splitting an instance in smaller Period objects according to a given interval.
*
* The returned iterable Period set is ordered so that:
* <ul>
* <li>The first returned object MUST share the ending datepoint of the parent object.</li>
* <li>The last returned object MUST share the starting datepoint of the parent object.</li>
* <li>The last returned object MUST have a duration equal or lesser than the submitted interval.</li>
* <li>All returned objects except for the first one MUST end immediately before the previously returned object</li>
* </ul>
*
* @param Period|DateInterval|Duration|string|int $duration a Duration
*
* @return iterable<Period>
*/
public function splitBackwards($duration): iterable
{
$endDate = $this->endDate;
$duration = self::filterDuration($duration);
do {
$startDate = $endDate->sub($duration);
if ($startDate < $this->startDate) {
$startDate = $this->startDate;
}
yield new self($startDate, $endDate, $this->boundaryType);
$endDate = $startDate;
} while ($endDate > $this->startDate);
}
/**************************************************
* Manipulation instance endpoints and bounds
**************************************************/
/**
* Returns the computed intersection between two instances as a new instance.
*
* [--------------------)
* ∩
* [----------)
* =
* [----)
*
* @throws Exception If both objects do not overlaps
*/
public function intersect(self $interval): self
{
if (!$this->overlaps($interval)) {
throw new Exception('Both '.self::class.' objects should overlaps');
}
$startDate = $this->startDate;
$endDate = $this->endDate;
$boundaryType = $this->boundaryType;
if ($interval->startDate > $this->startDate) {
$boundaryType[0] = $interval->boundaryType[0];
$startDate = $interval->startDate;
}
if ($interval->endDate < $this->endDate) {
$boundaryType[1] = $interval->boundaryType[1];
$endDate = $interval->endDate;
}
$intersect = new self($startDate, $endDate, $boundaryType);
if ($intersect->equals($this)) {
return $this;
}
return $intersect;
}
/**
* Returns the computed difference between two overlapping instances as
* an array containing Period objects or the null value.
*
* The array will always contains 2 elements:
*
* <ul>
* <li>an NULL filled array if both objects have the same datepoints</li>
* <li>one Period object and NULL if both objects share one datepoint</li>
* <li>two Period objects if both objects share no datepoint</li>
* </ul>
*
* [--------------------)
* \
* [-----------)
* =
* [--------------) + [-----)
*
* @return array<null|Period>
*/
public function diff(self $interval): array
{
if ($interval->equals($this)) {
return [null, null];
}
$intersect = $this->intersect($interval);
$merge = $this->merge($interval);
if ($merge->startDate == $intersect->startDate) {
$first = ')' === $intersect->boundaryType[1] ? '[' : '(';
$boundary = $first.$merge->boundaryType[1];
return [$merge->startingOn($intersect->endDate)->boundedBy($boundary), null];
}
if ($merge->endDate == $intersect->endDate) {
$last = '(' === $intersect->boundaryType[0] ? ']' : ')';
$boundary = $merge->boundaryType[0].$last;
return [$merge->endingOn($intersect->startDate)->boundedBy($boundary), null];
}
$last = '(' === $intersect->boundaryType[0] ? ']' : ')';
$lastBoundary = $merge->boundaryType[0].$last;
$first = ')' === $intersect->boundaryType[1] ? '[' : '(';
$firstBoundary = $first.$merge->boundaryType[1];
return [
$merge->endingOn($intersect->startDate)->boundedBy($lastBoundary),
$merge->startingOn($intersect->endDate)->boundedBy($firstBoundary),
];
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated 4.9.0 This method will be removed in the next major point release
* @see Period::subtract
*/
public function substract(self $interval): Sequence
{
return $this->subtract($interval);
}
/**
* Returns the difference set operation between two intervals as a Sequence.
* The Sequence can contain from 0 to 2 Periods depending on the result of
* the operation.
*
* [--------------------)
* -
* [-----------)
* =
* [--------------)
*/
public function subtract(self $interval): Sequence
{
if (!$this->overlaps($interval)) {
return new Sequence($this);
}
$filter = function ($item): bool {
return null !== $item && $this->overlaps($item);
};
return new Sequence(...array_filter($this->diff($interval), $filter));
}
/**
* Returns the computed gap between two instances as a new instance.
*
* [--------------------)
* +
* [----------)
* =
* [---)
*
* @throws Exception If both instance overlaps
*/
public function gap(self $interval): self
{
if ($this->overlaps($interval)) {
throw new Exception('Both '.self::class.' objects must not overlaps');
}
$bounds = $this->isEndIncluded() ? '(' : '[';
$bounds .= $interval->isStartIncluded() ? ')' : ']';
if ($interval->startDate > $this->startDate) {
return new self($this->endDate, $interval->startDate, $bounds);
}
return new self($interval->endDate, $this->startDate, $this->boundaryType);
}
/**
* Merges one or more instances to return a new instance.
* The resulting instance represents the largest duration possible.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified new datepoints.
*
* [--------------------)
* +
* [----------)
* =
* [--------------------------)
*
*
* @param Period ...$intervals
*/
public function merge(self ...$intervals): self
{
$carry = $this;
foreach ($intervals as $period) {
if ($carry->startDate > $period->startDate) {
$carry = new self(
$period->startDate,
$carry->endDate,
$period->boundaryType[0].$carry->boundaryType[1]
);
}
if ($carry->endDate < $period->endDate) {
$carry = new self(
$carry->startDate,
$period->endDate,
$carry->boundaryType[0].$period->boundaryType[1]
);
}
}
return $carry;
}
/**************************************************
* Mutation methods
**************************************************/
/**
* Returns an instance with the specified starting date endpoint.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified starting date endpoint.
*
* @param Datepoint|DateTimeInterface|int|string $startDate the new starting date endpoint
*/
public function startingOn($startDate): self
{
$startDate = self::filterDatepoint($startDate);
if ($startDate == $this->startDate) {
return $this;
}
return new self($startDate, $this->endDate, $this->boundaryType);
}
/**
* Returns an instance with the specified ending date endpoint.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified ending date endpoint.
*
* @param Datepoint|DateTimeInterface|int|string $endDate the new ending date endpoint
*/
public function endingOn($endDate): self
{
$endDate = self::filterDatepoint($endDate);
if ($endDate == $this->endDate) {
return $this;
}
return new self($this->startDate, $endDate, $this->boundaryType);
}
/**
* Returns an instance with the specified boundary type.
*
* This method MUST retain the state of the current instance, and return
* an instance with the specified range type.
*/
public function boundedBy(string $bounds): self
{
if ($bounds === $this->boundaryType) {
return $this;
}
return new self($this->startDate, $this->endDate, $bounds);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated 4.12.0 This method will be removed in the next major point release
* @see Period::boundedBy()
*
* Returns an instance with the specified boundary type.
*
* This method MUST retain the state of the current instance, and return
* an instance with the specified range type.
*/
public function withBoundaryType(string $boundaryType): self
{
return $this->boundedBy($boundaryType);
}
/**
* Returns a new instance with a new ending date endpoint.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified ending date endpoint.
*
* @param Period|DateInterval|Duration|string|int $duration a Duration
*/
public function withDurationAfterStart($duration): self
{
return $this->endingOn($this->startDate->add(self::filterDuration($duration)));
}
/**
* Returns a new instance with a new starting date endpoint.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified starting date endpoint.
*
* @param Period|DateInterval|Duration|string|int $duration a Duration
*/
public function withDurationBeforeEnd($duration): self
{
return $this->startingOn($this->endDate->sub(self::filterDuration($duration)));
}
/**
* Returns a new instance with a new starting datepoint
* moved forward or backward by the given interval.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified starting date endpoint.
*
* @param Period|DateInterval|Duration|string|int $duration a Duration
*/
public function moveStartDate($duration): self
{
return $this->startingOn($this->startDate->add(self::filterDuration($duration)));
}
/**
* Returns a new instance with a new ending datepoint
* moved forward or backward by the given interval.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified ending date endpoint.
*
* @param Period|DateInterval|Duration|string|int $duration a Duration
*/
public function moveEndDate($duration): self
{
return $this->endingOn($this->endDate->add(self::filterDuration($duration)));
}
/**
* Returns a new instance where the date endpoints
* are moved forwards or backward simultaneously by the given DateInterval.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified new date endpoints.
*
* @param Period|DateInterval|Duration|string|int $duration a Duration
*/
public function move($duration): self
{
$duration = self::filterDuration($duration);
$interval = new self($this->startDate->add($duration), $this->endDate->add($duration), $this->boundaryType);
if ($this->equals($interval)) {
return $this;
}
return $interval;
}
/**
* Returns an instance where the given DateInterval is simultaneously
* subtracted from the starting date endpoint and added to the ending date endpoint.
*
* Depending on the duration value, the resulting instance duration will be expanded or shrinked.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified new date endpoints.
*
* @param Period|DateInterval|Duration|string|int $duration a Duration
*/
public function expand($duration): self
{
$duration = self::filterDuration($duration);
$interval = new self($this->startDate->sub($duration), $this->endDate->add($duration), $this->boundaryType);
if ($this->equals($interval)) {
return $this;
}
return $interval;
}
}