fix: return 0 from length() when excluded boundaries leave a period empty

Excluding the boundaries of a one or two unit long period pushes the
included start past the included end. diff()->days is unsigned and the
HOUR/MINUTE/SECOND branch applied abs() outright, so that backwards range
was measured as a positive distance and the unconditional +1 inflated it
further: a one day period reported 1, 2, 2 and 3 for EXCLUDE_NONE, START,
END and ALL, while iterating the same objects yielded nothing.

Return 0 up front when the included start is past the included end. This
matches what the MONTH and YEAR precisions already returned through
iterator_count(), and lets the now unreachable abs() go.
This commit is contained in:
dualfroz committed 2026-09-04 22:00:22 +02:00
1 parent 5e077c8688
commit 9086ebcaab
3 files changed
+45 -1

No files matched your search

+8 -1
View File
@@ -66,6 +66,13 @@ trait PeriodGetters
public function length(): int public function length(): int
{ {
// Excluding the boundaries of a period that is only a few units long can push the
// included start past the included end. Such a period contains nothing, which is what
// iterating over it yields, so its length is 0 rather than a distance measured backwards.
if ($this->includedStart() > $this->includedEnd()) {
return 0;
}
// Length of month and year are not fixed, so we can't predict the length without iterate // 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 ? // TODO: maybe we can use cal_days_in_month ?
if ($this->precision->equals(Precision::MONTH(), Precision::YEAR())) { if ($this->precision->equals(Precision::MONTH(), Precision::YEAR())) {
@@ -73,7 +80,7 @@ trait PeriodGetters
} }
if ($this->precision->equals(Precision::HOUR(), Precision::MINUTE(), Precision::SECOND())) { if ($this->precision->equals(Precision::HOUR(), Precision::MINUTE(), Precision::SECOND())) {
$length = abs($this->includedEnd()->getTimestamp() - $this->includedStart()->getTimestamp()); $length = $this->includedEnd()->getTimestamp() - $this->includedStart()->getTimestamp();
if ($this->precision->equals(Precision::SECOND())) { if ($this->precision->equals(Precision::SECOND())) {
return $length + 1; return $length + 1;
+5
View File
@@ -35,6 +35,11 @@ it('length with boundaries', function ($expectedAmount, Period $period) {
expect($period->length())->toEqual($expectedAmount); expect($period->length())->toEqual($expectedAmount);
})->with('periods_with_amounts_of_included_dates'); })->with('periods_with_amounts_of_included_dates');
it('length is zero when excluding boundaries leaves nothing', function (Period $period) {
expect(iterator_count($period))->toEqual(0);
expect($period->length())->toEqual(0);
})->with('periods_left_empty_by_excluded_boundaries');
it('overlap with excluded boundaries', function () { it('overlap with excluded boundaries', function () {
$a = Period::make('2018-01-01', '2018-01-05', boundaries: Boundaries::EXCLUDE_END()); $a = Period::make('2018-01-01', '2018-01-05', boundaries: Boundaries::EXCLUDE_END());
$b = Period::make('2018-01-05', '2018-01-10'); $b = Period::make('2018-01-05', '2018-01-10');
+32
View File
@@ -81,6 +81,38 @@ dataset('periods_with_amounts_of_included_dates', function () {
yield [86362, Period::make('2018-01-01 00:00:00', '2018-01-01 23:59:22', Precision::SECOND(), Boundaries::EXCLUDE_START())]; yield [86362, Period::make('2018-01-01 00:00:00', '2018-01-01 23:59:22', Precision::SECOND(), Boundaries::EXCLUDE_START())];
yield [86362, Period::make('2018-01-01 00:00:00', '2018-01-01 23:59:22', Precision::SECOND(), Boundaries::EXCLUDE_END())]; yield [86362, Period::make('2018-01-01 00:00:00', '2018-01-01 23:59:22', Precision::SECOND(), Boundaries::EXCLUDE_END())];
yield [86361, Period::make('2018-01-01 00:00:00', '2018-01-01 23:59:22', Precision::SECOND(), Boundaries::EXCLUDE_ALL())]; yield [86361, Period::make('2018-01-01 00:00:00', '2018-01-01 23:59:22', Precision::SECOND(), Boundaries::EXCLUDE_ALL())];
// Excluding a boundary of a one unit long period leaves nothing behind.
yield [1, Period::make('2021-01-01', '2021-01-01', Precision::DAY(), Boundaries::EXCLUDE_NONE())];
yield [0, Period::make('2021-01-01', '2021-01-01', Precision::DAY(), Boundaries::EXCLUDE_START())];
yield [0, Period::make('2021-01-01', '2021-01-01', Precision::DAY(), Boundaries::EXCLUDE_END())];
yield [0, Period::make('2021-01-01', '2021-01-01', Precision::DAY(), Boundaries::EXCLUDE_ALL())];
yield [1, Period::make('2021-01-01 00:00:00', '2021-01-01 00:00:00', Precision::HOUR(), Boundaries::EXCLUDE_NONE())];
yield [0, Period::make('2021-01-01 00:00:00', '2021-01-01 00:00:00', Precision::HOUR(), Boundaries::EXCLUDE_START())];
yield [0, Period::make('2021-01-01 00:00:00', '2021-01-01 00:00:00', Precision::HOUR(), Boundaries::EXCLUDE_END())];
yield [0, Period::make('2021-01-01 00:00:00', '2021-01-01 00:00:00', Precision::HOUR(), Boundaries::EXCLUDE_ALL())];
yield [0, Period::make('2021-01-01 00:00:00', '2021-01-01 00:00:00', Precision::MINUTE(), Boundaries::EXCLUDE_ALL())];
yield [0, Period::make('2021-01-01 00:00:00', '2021-01-01 00:00:00', Precision::SECOND(), Boundaries::EXCLUDE_ALL())];
// A two unit long period keeps exactly one unit when one of its boundaries is excluded.
yield [2, Period::make('2021-01-01', '2021-01-02', Precision::DAY(), Boundaries::EXCLUDE_NONE())];
yield [1, Period::make('2021-01-01', '2021-01-02', Precision::DAY(), Boundaries::EXCLUDE_START())];
yield [1, Period::make('2021-01-01', '2021-01-02', Precision::DAY(), Boundaries::EXCLUDE_END())];
yield [0, Period::make('2021-01-01', '2021-01-02', Precision::DAY(), Boundaries::EXCLUDE_ALL())];
});
dataset('periods_left_empty_by_excluded_boundaries', function () {
yield 'day' => [Period::make('2021-01-01', '2021-01-01', Precision::DAY(), Boundaries::EXCLUDE_START())];
yield 'day, end excluded' => [Period::make('2021-01-01', '2021-01-01', Precision::DAY(), Boundaries::EXCLUDE_END())];
yield 'day, all excluded' => [Period::make('2021-01-01', '2021-01-01', Precision::DAY(), Boundaries::EXCLUDE_ALL())];
yield 'two days, all excluded' => [Period::make('2021-01-01', '2021-01-02', Precision::DAY(), Boundaries::EXCLUDE_ALL())];
yield 'hour' => [Period::make('2021-01-01 00:00:00', '2021-01-01 00:00:00', Precision::HOUR(), Boundaries::EXCLUDE_ALL())];
yield 'minute' => [Period::make('2021-01-01 00:00:00', '2021-01-01 00:00:00', Precision::MINUTE(), Boundaries::EXCLUDE_ALL())];
yield 'second' => [Period::make('2021-01-01 00:00:00', '2021-01-01 00:00:00', Precision::SECOND(), Boundaries::EXCLUDE_ALL())];
yield 'month' => [Period::make('2021-01-01', '2021-01-01', Precision::MONTH(), Boundaries::EXCLUDE_ALL())];
yield 'year' => [Period::make('2021-01-01', '2021-01-01', Precision::YEAR(), Boundaries::EXCLUDE_ALL())];
}); });
dataset('ceiling_dates', function () { dataset('ceiling_dates', function () {