diff --git a/src/PeriodTraits/PeriodGetters.php b/src/PeriodTraits/PeriodGetters.php index 4c3808f..7ec6a7e 100644 --- a/src/PeriodTraits/PeriodGetters.php +++ b/src/PeriodTraits/PeriodGetters.php @@ -66,13 +66,6 @@ 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())) { @@ -80,7 +73,7 @@ trait PeriodGetters } if ($this->precision->equals(Precision::HOUR(), Precision::MINUTE(), Precision::SECOND())) { - $length = $this->includedEnd()->getTimestamp() - $this->includedStart()->getTimestamp(); + $length = abs($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 new file mode 100644 index 0000000..2536d28 --- /dev/null +++ b/tests/Datasets/Periods.php @@ -0,0 +1,90 @@ + [ // P1Y, but P365T vs P366T (2020 is a leap year) + Period::make('2019-01-01', '2019-12-31', Precision::YEAR()), + Period::make('2020-01-01', '2020-12-31', Precision::YEAR()), + ]; + + // todo: I don't see how this one could ever have worked. TBI… + // yield '1 month' => [ // P1M , but P30D vs P28D + // Period::make('2019-04-01', '2019-04-30', Precision::MONTH()), + // Period::make('2019-02-01', '2019-02-28', Precision::MONTH()), + // ]; + + yield '1 day' => [ // P1D , but but different days + Period::make('2019-04-23', '2019-04-23', Precision::DAY()), + Period::make('2019-04-24', '2019-04-24', Precision::DAY()), + ]; + + yield '12 hours' => [ // PT12H, but on different halves of the day + Period::make('2019-04-23 00:00:00', '2019-04-23 11:59:00', Precision::HOUR()), + Period::make('2019-04-23 12:00:00', '2019-04-23 23:59:00', Precision::HOUR()), + ]; + + yield '30 minutes' => [ // PT30M, but on different halves of the hour + Period::make('2019-04-23 11:45:00', '2019-04-23 12:14:59', Precision::MINUTE()), + Period::make('2019-04-23 12:15:00', '2019-04-23 12:44:59', Precision::MINUTE()), + ]; + + yield '30 seconds' => [ // PT30S, but on different halves of the minute + Period::make('2019-04-23 11:59:45', '2019-04-23 12:00:14', Precision::SECOND()), + Period::make('2019-04-23 12:00:15', '2019-04-23 12:00:44', Precision::SECOND()), + ]; + + yield 'same amount of days within a month' => [ // P4D == P4D + Period::make('2019-02-01', '2019-02-28', Precision::DAY()), + Period::make('2019-04-01', '2019-04-28', Precision::DAY()), + ]; + + yield 'same amount of days spanning two months' => [ // P4D is still P4D + Period::make('2019-02-27', '2019-03-02', Precision::DAY()), + Period::make('2019-03-30', '2019-04-02', Precision::DAY()), + ]; + + yield 'same period, different precision' => [ // in this case P1D == P24H + Period::make('2019-04-23', '2019-04-23', Precision::DAY()), + Period::make('2019-04-23', '2019-04-23', Precision::HOUR()), + ]; + + yield 'different boundaries, but same amount of days, ' => [ // P1D == P1D + Period::make('2019-04-23', '2019-04-23', Precision::DAY(), Boundaries::EXCLUDE_NONE()), + Period::make('2019-04-23', '2019-04-24', Precision::DAY(), Boundaries::EXCLUDE_END()), + ]; +}); + +dataset('differents', function () { + return [ + 'a is smaller than b' => [ + Period::make('2019-04-23', '2019-04-24', Precision::DAY()), + Period::make('2019-04-23', '2019-04-25', Precision::DAY()), + ], + 'b is smaller than a' => [ + Period::make('2019-04-23', '2019-04-25', Precision::DAY()), + Period::make('2019-04-23', '2019-04-24', Precision::DAY()), + ], + ]; +}); + +dataset('expected_period_lengths', function () { + yield [1, Period::make('2018-01-01', '2018-01-01')]; + yield [15, Period::make('2018-01-01', '2018-01-15')]; + yield [14, Period::make('2018-01-01', '2018-01-15', boundaries: Boundaries::EXCLUDE_START())]; + yield [14, Period::make('2018-01-01', '2018-01-15', boundaries: Boundaries::EXCLUDE_END())]; + yield [13, Period::make('2018-01-01', '2018-01-15', boundaries: Boundaries::EXCLUDE_ALL())]; + 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())]; +}); diff --git a/tests/PeriodTest.php b/tests/PeriodTest.php new file mode 100644 index 0000000..6e59ec0 --- /dev/null +++ b/tests/PeriodTest.php @@ -0,0 +1,88 @@ +length())->toEqual(15); +}); + +it('can renew a period', function () { + $period = Period::make('2018-01-01', '2018-01-15'); + + $renewal = $period->renew(); + + expect($renewal->touchesWith($period))->toBeTrue(); + expect($period->length())->toEqual($renewal->length()); +}); + +it('has a duration', function () { + $a = Period::make('2018-01-01', '2018-01-15'); + $b = Period::make('2018-02-01', '2018-02-15'); + + expect($a->duration()->equals($b->duration()))->toBeTrue(); +}); + +it('accepts carbon instances', function () { + $a = Period::make(Carbon::make('2018-01-01'), Carbon::make('2018-01-02')); + + expect($a->equals(Period::make('2018-01-01', '2018-01-02')))->toBeTrue(); +}); + +it('will preserve the time', function () { + $period = Period::make('2018-01-01 01:02:03', '2018-01-02 04:05:06'); + + expect($period->equals(Period::make(DateTimeImmutable::createFromFormat('Y-m-d H:i:s', '2018-01-01 01:02:03'), DateTimeImmutable::createFromFormat('Y-m-d H:i:s', '2018-01-02 04:05:06'))))->toBeTrue(); +}); + +it('will use the start of day when passing strings to a period', function () { + $period = Period::make('2018-01-01', '2018-01-02'); + + expect($period->equals(Period::make(DateTimeImmutable::createFromFormat('Y-m-d H:i:s', '2018-01-01 00:00:00'), DateTimeImmutable::createFromFormat('Y-m-d H:i:s', '2018-01-02 00:00:00'))))->toBeTrue(); +}); + +it('is iterable', function (int $expectedCount, Period $period) { + expect(iterator_count($period))->toBe($expectedCount); +})->with('expected_period_lengths'); + +it('its iterator returns immutable dates', function () { + $period = Period::make('2018-01-01', '2018-01-15'); + + $current = $period->getIterator()->start; + + expect($current)->toBeInstanceOf(DateTimeImmutable::class); +}); + +it('keeps timezone when boundaries are timezoned', function () { + $timeZone = new DateTimeZone('Europe/London'); + $start = new DateTimeImmutable('2000-01-01', $timeZone); + $end = new DateTimeImmutable('2000-02-01', $timeZone); + $period = Period::make($start, $end); + expect($timeZone)->toEqual($period->start()->getTimezone()); + expect($timeZone)->toEqual($period->end()->getTimezone()); +}); + +it('gets the correct ceiling of a precision', function (Period $period, ?Precision $precision, Carbon $expected) { + expect($period->ceilingEnd($precision))->toEqual($expected->startOfSecond()); +})->with('ceiling_dates'); + +it('throws if trying to get a ceiling of a lower precision', function () { + $seconds = Period::make('2018-01-01 11:30:15', '2018-01-15 11:30:15', Precision::HOUR()); + + $seconds->ceilingEnd(Precision::SECOND()); +})->throws(CannotCeilLowerPrecision::class, "Cannot get the latest hour of a second."); + +it('test from string', function () { + expect(Period::fromString('(2021-01-01 00:00:00 , 2021-01-22 12:12:12]')->asString())->toEqual('(2021-01-01 00:00:00,2021-01-22 12:12:12]'); + expect(Period::fromString('(2021-01-01 00:00 , 2021-01-22 12:12]')->asString())->toEqual('(2021-01-01 00:00,2021-01-22 12:12]'); + expect(Period::fromString('(2021-01-01 00 , 2021-01-22 12]')->asString())->toEqual('(2021-01-01 00,2021-01-22 12]'); + expect(Period::fromString('(2021-01-01 , 2021-01-22]')->asString())->toEqual('(2021-01-01,2021-01-22]'); + expect(Period::fromString('(2021-01 , 2021-01]')->asString())->toEqual('(2021-01,2021-01]'); + expect(Period::fromString('(2021 , 2021]')->asString())->toEqual('(2021,2021]'); + expect(Period::fromString('( 2021 , 2021 )')->asString())->toEqual('(2021,2021)'); + expect(Period::fromString('[ 2021 , 2021 ]')->asString())->toEqual('[2021,2021]'); +});