diff --git a/src/PeriodTraits/PeriodGetters.php b/src/PeriodTraits/PeriodGetters.php index 7ec6a7e..0922b2a 100644 --- a/src/PeriodTraits/PeriodGetters.php +++ b/src/PeriodTraits/PeriodGetters.php @@ -66,6 +66,13 @@ trait PeriodGetters public function length(): int { + // When the boundary exclusions push the included start past the included + // end, the period is empty: it contains no units and iterating over it + // (see Period::getIterator()) yields nothing. + if ($this->includedStart() > $this->includedEnd()) { + return 0; + } + // Length of month and year are not fixed, so we can't predict the length without iterate // TODO: maybe we can use cal_days_in_month ? if ($this->precision->equals(Precision::MONTH(), Precision::YEAR())) { @@ -73,7 +80,9 @@ trait PeriodGetters } if ($this->precision->equals(Precision::HOUR(), Precision::MINUTE(), Precision::SECOND())) { - $length = abs($this->includedEnd()->getTimestamp() - $this->includedStart()->getTimestamp()); + // includedEnd is guaranteed to be >= includedStart here, so the + // difference is already non-negative. + $length = $this->includedEnd()->getTimestamp() - $this->includedStart()->getTimestamp(); if ($this->precision->equals(Precision::SECOND())) { return $length + 1; diff --git a/tests/Datasets/Periods.php b/tests/Datasets/Periods.php index 2536d28..cbf7a41 100644 --- a/tests/Datasets/Periods.php +++ b/tests/Datasets/Periods.php @@ -88,3 +88,20 @@ dataset('expected_period_lengths', function () { yield [24, Period::make('2018-01-01 00:00:00', '2018-01-01 23:59:59', Precision::HOUR())]; yield [24, Period::make('2018-01-01 00:00:00', '2018-01-02 00:00:00', Precision::HOUR(), Boundaries::EXCLUDE_END())]; }); + +// Single-unit periods (start === end) whose boundary exclusions push the +// included start past the included end, making them empty. The iterator +// (Period::getIterator) yields nothing for these, so length() must return 0 +// too instead of a spurious positive count. +dataset('empty_period_lengths', function () { + yield 'day, exclude start' => [Period::make('2018-01-01', '2018-01-01', Precision::DAY(), Boundaries::EXCLUDE_START())]; + yield 'day, exclude end' => [Period::make('2018-01-01', '2018-01-01', Precision::DAY(), Boundaries::EXCLUDE_END())]; + yield 'day, exclude all' => [Period::make('2018-01-01', '2018-01-01', Precision::DAY(), Boundaries::EXCLUDE_ALL())]; + + yield 'hour, exclude all' => [Period::make('2018-01-01 10:00:00', '2018-01-01 10:00:00', Precision::HOUR(), Boundaries::EXCLUDE_ALL())]; + yield 'minute, exclude all' => [Period::make('2018-01-01 10:00:00', '2018-01-01 10:00:00', Precision::MINUTE(), Boundaries::EXCLUDE_ALL())]; + yield 'second, exclude all' => [Period::make('2018-01-01 10:00:00', '2018-01-01 10:00:00', Precision::SECOND(), Boundaries::EXCLUDE_ALL())]; + + yield 'month, exclude all' => [Period::make('2018-01-01', '2018-01-01', Precision::MONTH(), Boundaries::EXCLUDE_ALL())]; + yield 'year, exclude all' => [Period::make('2018-01-01', '2018-01-01', Precision::YEAR(), Boundaries::EXCLUDE_ALL())]; +}); diff --git a/tests/PeriodTest.php b/tests/PeriodTest.php index 6e59ec0..a06022f 100644 --- a/tests/PeriodTest.php +++ b/tests/PeriodTest.php @@ -11,6 +11,12 @@ it('can determine the period length', function () { expect($period->length())->toEqual(15); }); +it('returns a length of 0 for an empty period, consistent with its iterator', function (Period $period) { + expect($period->includedStart())->toBeGreaterThan($period->includedEnd()); + expect($period->length())->toEqual(0); + expect($period->length())->toEqual(iterator_count($period)); +})->with('empty_period_lengths'); + it('can renew a period', function () { $period = Period::make('2018-01-01', '2018-01-15');