diff --git a/src/PeriodTraits/PeriodGetters.php b/src/PeriodTraits/PeriodGetters.php index 7ec6a7e..4c3808f 100644 --- a/src/PeriodTraits/PeriodGetters.php +++ b/src/PeriodTraits/PeriodGetters.php @@ -66,6 +66,13 @@ trait PeriodGetters 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 // TODO: maybe we can use cal_days_in_month ? if ($this->precision->equals(Precision::MONTH(), Precision::YEAR())) { @@ -73,7 +80,7 @@ trait PeriodGetters } 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())) { return $length + 1; diff --git a/tests/BoundaryTest.php b/tests/BoundaryTest.php index 2160dfd..d55889d 100644 --- a/tests/BoundaryTest.php +++ b/tests/BoundaryTest.php @@ -35,6 +35,11 @@ it('length with boundaries', function ($expectedAmount, Period $period) { expect($period->length())->toEqual($expectedAmount); })->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 () { $a = Period::make('2018-01-01', '2018-01-05', boundaries: Boundaries::EXCLUDE_END()); $b = Period::make('2018-01-05', '2018-01-10'); diff --git a/tests/Datasets/Dates.php b/tests/Datasets/Dates.php index ef9154a..ee7bd17 100644 --- a/tests/Datasets/Dates.php +++ b/tests/Datasets/Dates.php @@ -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_END())]; 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 () {