briannesbitt / Carbon

@@ -37,7 +37,7 @@
Loading
37 37
/**
38 38
 * Substitution of DatePeriod with some modifications and many more features.
39 39
 *
40 -
 * @property-read int $recurrences number of recurrences (if end not set).
40 +
 * @property-read int|float $recurrences number of recurrences (if end not set).
41 41
 * @property-read bool $include_start_date rather the start date is included in the iteration.
42 42
 * @property-read bool $include_end_date rather the end date is included in the iteration (if recurrences not set).
43 43
 * @property-read CarbonInterface $start Period start date.
@@ -212,6 +212,13 @@
Loading
212 212
     */
213 213
    public const NEXT_MAX_ATTEMPTS = 1000;
214 214
215 +
    /**
216 +
     * Number of maximum attempts before giving up on finding end date.
217 +
     *
218 +
     * @var int
219 +
     */
220 +
    public const END_MAX_ATTEMPTS = 10000;
221 +
215 222
    /**
216 223
     * The registered macros.
217 224
     *
@@ -981,7 +988,7 @@
Loading
981 988
    /**
982 989
     * Get number of recurrences.
983 990
     *
984 -
     * @return int|null
991 +
     * @return int|float|null
985 992
     */
986 993
    public function getRecurrences()
987 994
    {
@@ -1205,9 +1212,9 @@
Loading
1205 1212
    /**
1206 1213
     * Add a recurrences filter (set maximum number of recurrences).
1207 1214
     *
1208 -
     * @param int|null $recurrences
1215 +
     * @param int|float|null $recurrences
1209 1216
     *
1210 -
     * @throws \InvalidArgumentException
1217 +
     * @throws InvalidArgumentException
1211 1218
     *
1212 1219
     * @return $this
1213 1220
     */
@@ -1221,7 +1228,7 @@
Loading
1221 1228
            return $this->removeFilter(static::RECURRENCES_FILTER);
1222 1229
        }
1223 1230
1224 -
        $this->recurrences = (int) $recurrences;
1231 +
        $this->recurrences = $recurrences === INF ? INF : (int) $recurrences;
1225 1232
1226 1233
        if (!$this->hasFilter(static::RECURRENCES_FILTER)) {
1227 1234
            return $this->addFilter(static::RECURRENCES_FILTER);
@@ -1708,9 +1715,11 @@
Loading
1708 1715
            return $end;
1709 1716
        }
1710 1717
1711 -
        $dates = iterator_to_array($this);
1718 +
        if ($this->dateInterval->isEmpty()) {
1719 +
            return $this->getStartDate($rounding);
1720 +
        }
1712 1721
1713 -
        $date = end($dates);
1722 +
        $date = $this->getEndFromRecurrences() ?? $this->iterateUntilEnd();
1714 1723
1715 1724
        if ($date && $rounding) {
1716 1725
            $date = $date->copy()->round($this->getDateInterval(), $rounding);
@@ -1719,6 +1728,56 @@
Loading
1719 1728
        return $date;
1720 1729
    }
1721 1730
1731 +
    /**
1732 +
     * @return CarbonInterface|null
1733 +
     */
1734 +
    private function getEndFromRecurrences()
1735 +
    {
1736 +
        if ($this->recurrences === null) {
1737 +
            throw new UnreachableException(
1738 +
                "Could not calculate period end without either explicit end or recurrences.\n".
1739 +
                "If you're looking for a forever-period, use ->setRecurrences(INF)."
1740 +
            );
1741 +
        }
1742 +
1743 +
        if ($this->recurrences === INF) {
1744 +
            $start = $this->getStartDate();
1745 +
1746 +
            return $start < $start->copy()->add($this->getDateInterval())
1747 +
                ? CarbonImmutable::endOfTime()
1748 +
                : CarbonImmutable::startOfTime();
1749 +
        }
1750 +
1751 +
        if ($this->filters === [[static::RECURRENCES_FILTER, null]]) {
1752 +
            return $this->getStartDate()->copy()->add(
1753 +
                $this->getDateInterval()->times(
1754 +
                    $this->recurrences - ($this->isStartExcluded() ? 0 : 1)
1755 +
                )
1756 +
            );
1757 +
        }
1758 +
1759 +
        return null;
1760 +
    }
1761 +
1762 +
    /**
1763 +
     * @return CarbonInterface|null
1764 +
     */
1765 +
    private function iterateUntilEnd()
1766 +
    {
1767 +
        $attempts = 0;
1768 +
        $date = null;
1769 +
1770 +
        foreach ($this as $date) {
1771 +
            if (++$attempts > static::END_MAX_ATTEMPTS) {
1772 +
                throw new UnreachableException(
1773 +
                    'Could not calculate period end after iterating '.static::END_MAX_ATTEMPTS.' times.'
1774 +
                );
1775 +
            }
1776 +
        }
1777 +
1778 +
        return $date;
1779 +
    }
1780 +
1722 1781
    /**
1723 1782
     * Returns true if the current period overlaps the given one (if 1 parameter passed)
1724 1783
     * or the period between 2 dates (if 2 parameters passed).
@@ -1736,7 +1795,15 @@
Loading
1736 1795
            $range = static::create($range);
1737 1796
        }
1738 1797
1739 -
        return $this->calculateEnd() > $range->getStartDate() && $range->calculateEnd() > $this->getStartDate();
1798 +
        $thisDates = [$this->getStartDate(), $this->calculateEnd()];
1799 +
        sort($thisDates);
1800 +
        [$start, $end] = $thisDates;
1801 +
1802 +
        $rangeDates = [$range->getStartDate(), $range->calculateEnd()];
1803 +
        sort($rangeDates);
1804 +
        [$rangeStart, $rangeEnd] = $rangeDates;
1805 +
1806 +
        return $end > $rangeStart && $rangeEnd > $start;
1740 1807
    }
1741 1808
1742 1809
    /**

@@ -509,5 +509,64 @@
Loading
509 509
 */
510 510
class CarbonImmutable extends DateTimeImmutable implements CarbonInterface
511 511
{
512 -
    use Date;
512 +
    use Date {
513 +
        __clone as dateTraitClone;
514 +
    }
515 +
516 +
    public function __clone()
517 +
    {
518 +
        $this->dateTraitClone();
519 +
        $this->endOfTime = false;
520 +
        $this->startOfTime = false;
521 +
    }
522 +
523 +
    /**
524 +
     * Create a very old date representing start of time.
525 +
     *
526 +
     * @return static
527 +
     */
528 +
    public static function startOfTime(): self
529 +
    {
530 +
        $date = static::parse('0001-01-01')->years(self::getStartOfTimeYear());
531 +
        $date->startOfTime = true;
532 +
533 +
        return $date;
534 +
    }
535 +
536 +
    /**
537 +
     * Create a very far date representing end of time.
538 +
     *
539 +
     * @return static
540 +
     */
541 +
    public static function endOfTime(): self
542 +
    {
543 +
        $date = static::parse('9999-12-31 23:59:59.999999')->years(self::getEndOfTimeYear());
544 +
        $date->endOfTime = true;
545 +
546 +
        return $date;
547 +
    }
548 +
549 +
    /**
550 +
     * @codeCoverageIgnore
551 +
     */
552 +
    private static function getEndOfTimeYear(): int
553 +
    {
554 +
        if (version_compare(PHP_VERSION, '7.3.0-dev', '<')) {
555 +
            return 145261681241552;
556 +
        }
557 +
558 +
        return PHP_INT_MAX;
559 +
    }
560 +
561 +
    /**
562 +
     * @codeCoverageIgnore
563 +
     */
564 +
    private static function getStartOfTimeYear(): int
565 +
    {
566 +
        if (version_compare(PHP_VERSION, '7.3.0-dev', '<')) {
567 +
            return -135908816449551;
568 +
        }
569 +
570 +
        return max(PHP_INT_MIN, -9223372036854773760);
571 +
    }
513 572
}

@@ -31,6 +31,12 @@
Loading
31 31
 */
32 32
trait Comparison
33 33
{
34 +
    /** @var bool */
35 +
    protected $endOfTime = false;
36 +
37 +
    /** @var bool */
38 +
    protected $startOfTime = false;
39 +
34 40
    /**
35 41
     * Determines if the instance is equal to another
36 42
     *
@@ -1040,4 +1046,24 @@
Loading
1040 1046
1041 1047
        return (bool) @preg_match('/^'.$regex.'$/', $date);
1042 1048
    }
1049 +
1050 +
    /**
1051 +
     * Returns true if the date was created using CarbonImmutable::startOfTime()
1052 +
     *
1053 +
     * @return bool
1054 +
     */
1055 +
    public function isStartOfTime(): bool
1056 +
    {
1057 +
        return $this->startOfTime ?? false;
1058 +
    }
1059 +
1060 +
    /**
1061 +
     * Returns true if the date was created using CarbonImmutable::endOfTime()
1062 +
     *
1063 +
     * @return bool
1064 +
     */
1065 +
    public function isEndOfTime(): bool
1066 +
    {
1067 +
        return $this->endOfTime ?? false;
1068 +
    }
1043 1069
}
Files Coverage
src/Carbon 100.00%
Project Totals (886 files) 100.00%

No yaml found.

Create your codecov.yml to customize your Codecov experience

Sunburst
The inner-most circle is the entire project, moving away from the center are folders then, finally, a single file. The size and color of each slice is representing the number of statements and the coverage, respectively.
Icicle
The top section represents the entire project. Proceeding with folders and finally individual files. The size and color of each slice is representing the number of statements and the coverage, respectively.
Grid
Each block represents a single file in the project. The size and color of each block is represented by the number of statements and the coverage, respectively.
Loading