From b1fe1328f1b568aebfff160026769a21786a8f27 Mon Sep 17 00:00:00 2001 From: "o.yakubenko2014@yandex.ua" Date: Sun, 29 Sep 2019 10:22:01 +0300 Subject: [PATCH 1/2] add draft for EndlessPeriod --- src/EndlessPeriod.php | 532 +++++++++++++++++++++++++++++++ src/Exceptions/InvalidDate.php | 5 + src/Period.php | 24 +- src/PeriodInterface.php | 84 +++++ tests/EndlessPeriodTest.php | 553 +++++++++++++++++++++++++++++++++ 5 files changed, 1186 insertions(+), 12 deletions(-) create mode 100644 src/EndlessPeriod.php create mode 100644 src/PeriodInterface.php create mode 100644 tests/EndlessPeriodTest.php diff --git a/src/EndlessPeriod.php b/src/EndlessPeriod.php new file mode 100644 index 0000000..3247ee5 --- /dev/null +++ b/src/EndlessPeriod.php @@ -0,0 +1,532 @@ + $end) { +// throw InvalidPeriod::endBeforeStart($start, $end); +// } + + $this->boundaryExclusionMask = $boundaryExclusionMask ?? Boundaries::EXCLUDE_NONE; + $this->precisionMask = $precisionMask ?? Precision::DAY; + + $this->start = $this->roundDate($start, $this->precisionMask); + $this->end = null;//$this->roundDate($end, $this->precisionMask); + $this->interval = $this->createDateInterval($this->precisionMask); + + $this->includedStart = $this->startIncluded() + ? $this->start + : $this->start->add($this->interval); + + $this->includedEnd = $this->endIncluded() + ? $this->end + : $this->end->sub($this->interval); + } + + /** + * @param string|DateTimeInterface $start + * @param string|DateTimeInterface $end + * @param int|null $precisionMask + * @param int|null $boundaryExclusionMask + * @param string|null $format + * + * @return static + */ + public static function make( + $start, + $end = null, + ?int $precisionMask = null, + ?int $boundaryExclusionMask = null, + ?string $format = null + ): PeriodInterface { + if ($start === null) { + throw InvalidDate::cannotBeNull('Start date'); + } + +// if ($end !== null) { +// throw InvalidDate::shouldBeNull('End date'); +// } + + return new static( + static::resolveDate($start, $format), + null, + $precisionMask, + $boundaryExclusionMask + ); + } + + public function startIncluded(): bool + { + return ! $this->startExcluded(); + } + + public function startExcluded(): bool + { + return Boundaries::EXCLUDE_START & $this->boundaryExclusionMask; + } + + public function endIncluded(): bool + { + return ! $this->endExcluded(); + } + + public function endExcluded(): bool + { + return Boundaries::EXCLUDE_END & $this->boundaryExclusionMask; + } + + public function getStart(): DateTimeImmutable + { + return $this->start; + } + + public function getIncludedStart(): DateTimeImmutable + { + return $this->includedStart; + } + + public function getEnd(): DateTimeImmutable + { + return $this->end; + } + + public function getIncludedEnd(): ?DateTimeImmutable + { + return $this->includedEnd; + } + + public function length(): ?int + { + return null; + } + + public function overlapsWith(PeriodInterface $period): bool + { + $this->ensurePrecisionMatches($period); + + if ($this->getIncludedStart() > $period->getIncludedEnd()) { + return false; + } + + if ($period->getIncludedStart() > $this->getIncludedEnd()) { + return false; + } + + return true; + } + + public function touchesWith(PeriodInterface $period): bool + { + $this->ensurePrecisionMatches($period); + + if ($period instanceof EndlessPeriod) { + return true; + } + + if ($this->getIncludedStart()->diff($period->getIncludedEnd())->days <= 1) { + return true; + } + + return false; + } + + public function startsBefore(DateTimeInterface $date): bool + { + return $this->getIncludedStart() < $date; + } + + public function startsBeforeOrAt(DateTimeInterface $date): bool + { + return $this->getIncludedStart() <= $date; + } + + public function startsAfter(DateTimeInterface $date): bool + { + return $this->getIncludedStart() > $date; + } + + public function startsAfterOrAt(DateTimeInterface $date): bool + { + return $this->getIncludedStart() >= $date; + } + + public function startsAt(DateTimeInterface $date): bool + { + return $this->getIncludedStart()->getTimestamp() === $this->roundDate( + $date, + $this->precisionMask + )->getTimestamp(); + } + + public function endsBefore(DateTimeInterface $date): bool + { + return $this->getIncludedEnd() < $this->roundDate( + $date, + $this->precisionMask + ); + } + + public function endsBeforeOrAt(DateTimeInterface $date): bool + { + return $this->getIncludedEnd() <= $this->roundDate( + $date, + $this->precisionMask + ); + } + + public function endsAfter(DateTimeInterface $date): bool + { + return $this->getIncludedEnd() > $this->roundDate( + $date, + $this->precisionMask + ); + } + + public function endsAfterOrAt(DateTimeInterface $date): bool + { + return $this->getIncludedEnd() >= $this->roundDate( + $date, + $this->precisionMask + ); + } + + public function endsAt(DateTimeInterface $date): bool + { + return $this->getIncludedEnd()->getTimestamp() === $this->roundDate( + $date, + $this->precisionMask + )->getTimestamp(); + } + + public function contains(DateTimeInterface $date): bool + { + if ($this->roundDate($date, $this->precisionMask) < $this->getIncludedStart()) { + return false; + } + + if ($this->roundDate($date, $this->precisionMask) > $this->getIncludedEnd()) { + return false; + } + + return true; + } + + public function equals(PeriodInterface $period): bool + { + $this->ensurePrecisionMatches($period); + + if ($period->getIncludedStart()->getTimestamp() !== $this->getIncludedStart()->getTimestamp()) { + return false; + } + + if ($period->getIncludedEnd()->getTimestamp() !== $this->getIncludedEnd()->getTimestamp()) { + return false; + } + + return true; + } + + /** + * @param \Spatie\Period\Period $period + * + * @return static|null + * @throws \Exception + */ + public function gap(PeriodInterface $period): ?Period + { + $this->ensurePrecisionMatches($period); + + if ($this->overlapsWith($period)) { + return null; + } + + if ($this->touchesWith($period)) { + return null; + } + + if ($this->getIncludedStart() >= $period->getIncludedEnd()) { + return static::make( + $period->getIncludedEnd()->add($this->interval), + $this->getIncludedStart()->sub($this->interval), + $this->getPrecisionMask() + ); + } + + return static::make( + $this->getIncludedEnd()->add($this->interval), + $period->getIncludedStart()->sub($this->interval), + $this->getPrecisionMask() + ); + } + + /** + * @param \Spatie\Period\Period $period + * + * @return static|null + */ + public function overlapSingle(PeriodInterface $period): ?Period + { + $this->ensurePrecisionMatches($period); + + $start = $this->getIncludedStart() > $period->getIncludedStart() + ? $this->getIncludedStart() + : $period->getIncludedStart(); + + $end = $this->getIncludedEnd() < $period->getIncludedEnd() + ? $this->getIncludedEnd() + : $period->getIncludedEnd(); + + if ($start > $end) { + return null; + } + + return static::make($start, $end, $this->getPrecisionMask()); + } + + /** + * @param \Spatie\Period\Period ...$periods + * + * @return \Spatie\Period\PeriodCollection|static[] + */ + public function overlap(PeriodInterface ...$periods): PeriodCollection + { + $overlapCollection = new PeriodCollection(); + + foreach ($periods as $period) { + $overlap = $this->overlapSingle($period); + + if ($overlap === null) { + continue; + } + + $overlapCollection[] = $overlap; + } + + return $overlapCollection; + } + + /** + * @param \Spatie\Period\Period ...$periods + * + * @return static + */ + public function overlapAll(PeriodInterface ...$periods): Period + { + $overlap = clone $this; + + if (! count($periods)) { + return $overlap; + } + + foreach ($periods as $period) { + $overlap = $overlap->overlapSingle($period); + } + + return $overlap; + } + + public function diffSingle(PeriodInterface $period): PeriodCollection + { + $this->ensurePrecisionMatches($period); + + $periodCollection = new PeriodCollection(); + + if (! $this->overlapsWith($period)) { + $periodCollection[] = clone $this; + $periodCollection[] = clone $period; + + return $periodCollection; + } + + $overlap = $this->overlapSingle($period); + + $start = $this->getIncludedStart() < $period->getIncludedStart() + ? $this->getIncludedStart() + : $period->getIncludedStart(); + + $end = $this->getIncludedEnd() > $period->getIncludedEnd() + ? $this->getIncludedEnd() + : $period->getIncludedEnd(); + + if ($overlap->getIncludedStart() > $start) { + $periodCollection[] = static::make( + $start, + $overlap->getIncludedStart()->sub($this->interval), + $this->getPrecisionMask() + ); + } + + if ($overlap->getIncludedEnd() < $end) { + $periodCollection[] = static::make( + $overlap->getIncludedEnd()->add($this->interval), + $end, + $this->getPrecisionMask() + ); + } + + return $periodCollection; + } + + /** + * @param \Spatie\Period\Period ...$periods + * + * @return \Spatie\Period\PeriodCollection|static[] + */ + public function diff(PeriodInterface ...$periods): PeriodCollection + { + if (count($periods) === 1 && ! $this->overlapsWith($periods[0])) { + $collection = new PeriodCollection(); + + $gap = $this->gap($periods[0]); + + if ($gap !== null) { + $collection[] = $gap; + } + + return $collection; + } + + $diffs = []; + + foreach ($periods as $period) { + $diffs[] = $this->diffSingle($period); + } + + $collection = (new PeriodCollection($this))->overlap(...$diffs); + + return $collection; + } + + public function getPrecisionMask(): int + { + return $this->precisionMask; + } + + public function getIterator() + { + return new DatePeriod( + $this->getIncludedStart(), + $this->interval, + $this->getIncludedEnd()->add($this->interval) + ); + } + + protected static function resolveDate($date, ?string $format): DateTimeImmutable + { + if ($date instanceof DateTimeImmutable) { + return $date; + } + + if ($date instanceof DateTime) { + return DateTimeImmutable::createFromMutable($date); + } + + $format = static::resolveFormat($date, $format); + + if (! is_string($date)) { + throw InvalidDate::forFormat($date, $format); + } + + $dateTime = DateTimeImmutable::createFromFormat($format, $date); + + if ($dateTime === false) { + throw InvalidDate::forFormat($date, $format); + } + + if (strpos($format, ' ') === false) { + $dateTime = $dateTime->setTime(0, 0, 0); + } + + return $dateTime; + } + + protected static function resolveFormat($date, ?string $format): string + { + if ($format !== null) { + return $format; + } + + if (strpos($format, ' ') === false && strpos($date, ' ') !== false) { + return 'Y-m-d H:i:s'; + } + + return 'Y-m-d'; + } + + protected function roundDate(DateTimeInterface $date, int $precision): DateTimeImmutable + { + [$year, $month, $day, $hour, $minute, $second] = explode(' ', $date->format('Y m d H i s')); + + $month = (Precision::MONTH & $precision) === Precision::MONTH ? $month : '01'; + $day = (Precision::DAY & $precision) === Precision::DAY ? $day : '01'; + $hour = (Precision::HOUR & $precision) === Precision::HOUR ? $hour : '00'; + $minute = (Precision::MINUTE & $precision) === Precision::MINUTE ? $minute : '00'; + $second = (Precision::SECOND & $precision) === Precision::SECOND ? $second : '00'; + + return DateTimeImmutable::createFromFormat( + 'Y m d H i s', + implode(' ', [$year, $month, $day, $hour, $minute, $second]) + ); + } + + protected function createDateInterval(int $precision): DateInterval + { + $interval = [ + Precision::SECOND => 'PT1S', + Precision::MINUTE => 'PT1M', + Precision::HOUR => 'PT1H', + Precision::DAY => 'P1D', + Precision::MONTH => 'P1M', + Precision::YEAR => 'P1Y', + ][$precision]; + + return new DateInterval($interval); + } + + protected function ensurePrecisionMatches(PeriodInterface $period): void + { + if ($this->precisionMask === $period->precisionMask) { + return; + } + + throw CannotComparePeriods::precisionDoesNotMatch(); + } +} diff --git a/src/Exceptions/InvalidDate.php b/src/Exceptions/InvalidDate.php index da1f626..d1fe574 100644 --- a/src/Exceptions/InvalidDate.php +++ b/src/Exceptions/InvalidDate.php @@ -11,6 +11,11 @@ public static function cannotBeNull(string $parameter): InvalidDate return new static("{$parameter} cannot be null"); } + public static function shouldBeNull(string $parameter): InvalidDate + { + return new static("{$parameter} should be null"); + } + public static function forFormat(string $date, ?string $format): InvalidDate { $message = "Could not construct a date from `{$date}`"; diff --git a/src/Period.php b/src/Period.php index c60dec7..8686a2a 100644 --- a/src/Period.php +++ b/src/Period.php @@ -12,7 +12,7 @@ use Spatie\Period\Exceptions\InvalidPeriod; use Spatie\Period\Exceptions\CannotComparePeriods; -class Period implements IteratorAggregate +class Period implements IteratorAggregate, PeriodInterface { /** @var \DateTimeImmutable */ protected $start; @@ -76,7 +76,7 @@ public static function make( ?int $precisionMask = null, ?int $boundaryExclusionMask = null, ?string $format = null - ): Period { + ): PeriodInterface { if ($start === null) { throw InvalidDate::cannotBeNull('Start date'); } @@ -140,7 +140,7 @@ public function length(): int return $length; } - public function overlapsWith(Period $period): bool + public function overlapsWith(PeriodInterface $period): bool { $this->ensurePrecisionMatches($period); @@ -155,7 +155,7 @@ public function overlapsWith(Period $period): bool return true; } - public function touchesWith(Period $period): bool + public function touchesWith(PeriodInterface $period): bool { $this->ensurePrecisionMatches($period); @@ -251,7 +251,7 @@ public function contains(DateTimeInterface $date): bool return true; } - public function equals(Period $period): bool + public function equals(PeriodInterface $period): bool { $this->ensurePrecisionMatches($period); @@ -272,7 +272,7 @@ public function equals(Period $period): bool * @return static|null * @throws \Exception */ - public function gap(Period $period): ?Period + public function gap(PeriodInterface $period): ?Period { $this->ensurePrecisionMatches($period); @@ -304,7 +304,7 @@ public function gap(Period $period): ?Period * * @return static|null */ - public function overlapSingle(Period $period): ?Period + public function overlapSingle(PeriodInterface $period): ?Period { $this->ensurePrecisionMatches($period); @@ -328,7 +328,7 @@ public function overlapSingle(Period $period): ?Period * * @return \Spatie\Period\PeriodCollection|static[] */ - public function overlap(Period ...$periods): PeriodCollection + public function overlap(PeriodInterface ...$periods): PeriodCollection { $overlapCollection = new PeriodCollection(); @@ -350,7 +350,7 @@ public function overlap(Period ...$periods): PeriodCollection * * @return static */ - public function overlapAll(Period ...$periods): Period + public function overlapAll(PeriodInterface ...$periods): Period { $overlap = clone $this; @@ -365,7 +365,7 @@ public function overlapAll(Period ...$periods): Period return $overlap; } - public function diffSingle(Period $period): PeriodCollection + public function diffSingle(PeriodInterface $period): PeriodCollection { $this->ensurePrecisionMatches($period); @@ -412,7 +412,7 @@ public function diffSingle(Period $period): PeriodCollection * * @return \Spatie\Period\PeriodCollection|static[] */ - public function diff(Period ...$periods): PeriodCollection + public function diff(PeriodInterface ...$periods): PeriodCollection { if (count($periods) === 1 && ! $this->overlapsWith($periods[0])) { $collection = new PeriodCollection(); @@ -523,7 +523,7 @@ protected function createDateInterval(int $precision): DateInterval return new DateInterval($interval); } - protected function ensurePrecisionMatches(Period $period): void + protected function ensurePrecisionMatches(PeriodInterface $period): void { if ($this->precisionMask === $period->precisionMask) { return; diff --git a/src/PeriodInterface.php b/src/PeriodInterface.php new file mode 100644 index 0000000..0fbe84e --- /dev/null +++ b/src/PeriodInterface.php @@ -0,0 +1,84 @@ +assertEquals(null, $period->length()); + } + + /** + * @test + * @dataProvider overlappingDates + */ + public function it_can_determine_if_two_periods_overlap_with_each_other(Period $a, Period $b) + { + $this->assertTrue($a->overlapsWith($b)); + } + + /** @test */ + public function it_can_determine_if_two_periods_touch_each_other() + { + $this->assertTrue( + EndlessPeriod::make('2018-01-01') + ->touchesWith(EndlessPeriod::make('2018-01-02')) + ); + + $this->assertTrue( + EndlessPeriod::make('2018-01-02', '2018-01-02') + ->touchesWith(EndlessPeriod::make('2018-01-01', '2018-01-01')) + ); + + $this->assertTrue( + EndlessPeriod::make('2018-01-01') + ->touchesWith(EndlessPeriod::make('2018-01-03')) + ); + + $this->assertTrue( + EndlessPeriod::make('2018-01-03') + ->touchesWith(EndlessPeriod::make('2018-01-01')) + ); + } + + /** + * @test + * @dataProvider noOverlappingDates + */ + public function it_can_determine_that_two_periods_do_not_overlap_with_each_other(Period $a, Period $b) + { + $this->assertFalse($a->overlapsWith($b)); + } + + public function overlappingDates(): array + { + return [ + /* + * A [=====] + * B [=====] + */ + [Period::make('2018-01-01', '2018-02-01'), Period::make('2018-01-15', '2018-02-15')], + + /* + * A [=====] + * B [=============] + */ + [Period::make('2018-01-01', '2018-02-01'), Period::make('2017-01-01', '2019-01-01')], + + /* + * A [=====] + * B [=====] + */ + [Period::make('2018-01-01', '2018-02-01'), Period::make('2017-12-01', '2018-01-15')], + + /* + * A [=============] + * B [=====] + */ + [Period::make('2017-01-01', '2019-01-01'), Period::make('2018-01-01', '2018-02-01')], + + /* + * A [====] + * B [====] + */ + [Period::make('2018-01-01', '2018-02-01'), Period::make('2018-01-01', '2018-02-01')], + ]; + } + + public function noOverlappingDates() + { + return [ + /* + * A [===] + * B [===] + */ + [Period::make('2018-01-01', '2018-01-31'), Period::make('2018-02-01', '2018-02-28')], + + /* + * A [===] + * B [===] + */ + [Period::make('2018-02-01', '2018-02-28'), Period::make('2018-01-01', '2018-01-31')], + ]; + } + + /** + * @test + * + * A [===========] + * B [============] + * + * OVERLAP [=======] + */ + public function it_can_determine_an_overlap_period_between_two_other_periods() + { + $a = Period::make('2018-01-01', '2018-01-15'); + + $b = Period::make('2018-01-10', '2018-01-30'); + + $overlapPeriod = Period::make('2018-01-10', '2018-01-15'); + + $this->assertTrue($a->overlapSingle($b)->equals($overlapPeriod)); + } + + /** + * @test + * + * A [========] + * B [==] + * C [=====] + * D [===============] + * + * OVERLAP [=] [==] [==] + */ + public function it_can_determine_multiple_overlap_periods_between_two_other_periods() + { + $a = Period::make('2018-01-01', '2018-01-31'); + $b = Period::make('2018-02-10', '2018-02-20'); + $c = Period::make('2018-03-01', '2018-03-31'); + $d = Period::make('2018-01-20', '2018-03-10'); + + $overlapPeriods = $d->overlap($a, $b, $c); + + $this->assertCount(3, $overlapPeriods); + + $this->assertTrue($overlapPeriods[0]->equals(Period::make('2018-01-20', '2018-01-31'))); + $this->assertTrue($overlapPeriods[1]->equals(Period::make('2018-02-10', '2018-02-20'))); + $this->assertTrue($overlapPeriods[2]->equals(Period::make('2018-03-01', '2018-03-10'))); + } + + /** + * @test + * + * A [============] + * B [==] + * C [=======] + * + * OVERLAP [==] + */ + public function it_can_determine_the_overlap_between_multiple_periods() + { + $a = Period::make('2018-01-01', '2018-01-31'); + $b = Period::make('2018-01-10', '2018-01-15'); + $c = Period::make('2018-01-10', '2018-01-31'); + + $overlap = $a->overlapAll($b, $c); + + $this->assertTrue($overlap->equals(Period::make('2018-01-10', '2018-01-15'))); + } + + /** @test */ + public function non_overlapping_dates_return_an_empty_collection() + { + $a = Period::make('2019-01-01', '2019-01-31'); + $b = Period::make('2019-02-01', '2019-02-28'); + + $this->assertTrue($a->overlap($b)->isEmpty()); + } + + /** @test */ + public function it_can_determine_that_two_periods_do_not_overlap() + { + $a = Period::make('2018-01-05', '2018-01-10'); + $b = Period::make('2018-01-22', '2018-01-30'); + + $overlap = $a->overlapSingle($b); + + $this->assertNull($overlap); + } + + /** + * @test + * + * A [===] + * B [===] + * + * GAP [=] + */ + public function it_can_determine_the_gap_between_two_periods() + { + $a = Period::make('2018-01-01', '2018-01-10'); + + $b = Period::make('2018-01-15', '2018-01-31'); + + $gap = $a->gap($b); + + $this->assertTrue($gap->equals(Period::make('2018-01-11', '2018-01-14'))); + } + + /** + * @test + * + * A [===] + * B [===] + * + * GAP [=] + */ + public function it_can_still_determine_the_gap_between_two_periods_even_when_the_periods_are_not_in_order() + { + $a = Period::make('2018-01-15', '2018-01-31'); + + $b = Period::make('2018-01-01', '2018-01-10'); + + $gap = $a->gap($b); + + $this->assertTrue($gap->equals(Period::make('2018-01-11', '2018-01-14'))); + } + + /** + * @test + * + * A [=====] + * B [=====] + * + * GAP + */ + public function if_will_determine_that_there_is_no_gap_if_the_periods_only_touch_but_do_not_overlap() + { + $a = Period::make('2018-01-15', '2018-01-31'); + + $b = Period::make('2018-02-01', '2018-02-01'); + + $gap = $a->gap($b); + + $this->assertNull($gap); + } + + /** + * @test + * + * A [=====] + * B [=====] + * + * GAP + */ + public function if_will_determine_that_there_is_no_gap_when_periods_overlap() + { + $a = Period::make('2018-01-15', '2018-01-31'); + + $b = Period::make('2018-01-28', '2018-02-01'); + + $gap = $a->gap($b); + + $this->assertNull($gap); + } + + /** + * @test + * + * A [===========] + * B [===========] + * + * DIFF [==] [==] + */ + public function if_can_create_a_diff_for_two_periods() + { + $a = Period::make('2018-01-01', '2018-01-15'); + + $b = Period::make('2018-01-10', '2018-01-30'); + + $diffs = $a->diffSingle($b); + + $this->assertTrue($diffs[0]->equals(Period::make('2018-01-01', '2018-01-09'))); + $this->assertTrue($diffs[1]->equals(Period::make('2018-01-16', '2018-01-30'))); + } + + /** + * @test + * + * A [==========] + * B [============] + * + * DIFF [==] [==] + */ + public function if_can_still_create_a_diff_for_two_periods_even_if_there_are_not_ordered() + { + $a = Period::make('2018-01-10', '2018-01-30'); + + $b = Period::make('2018-01-01', '2018-01-15'); + + $diffs = $a->diffSingle($b); + + $this->assertTrue($diffs[0]->equals(Period::make('2018-01-01', '2018-01-09'))); + $this->assertTrue($diffs[1]->equals(Period::make('2018-01-16', '2018-01-30'))); + } + + /** + * @test + * + * A [=====] + * B [=====] + * + * DIFF [=====] [=====] + */ + public function it_can_determine_the_diff_if_periods_do_not_overlap_at_all() + { + $a = Period::make('2018-01-10', '2018-01-15'); + + $b = Period::make('2018-02-10', '2018-02-15'); + + $diffs = $a->diffSingle($b); + + $this->assertTrue($diffs[0]->equals(Period::make('2018-01-10', '2018-01-15'))); + $this->assertTrue($diffs[1]->equals(Period::make('2018-02-10', '2018-02-15'))); + } + + /** + * @test + * + * A [=========] + * B [==] + * C [=========] + * CURRENT [===========] + * + * OVERLAP [=] [====] + * DIFF [=] + */ + public function it_can_determine_the_diff_for_periods_with_multiple_overlaps() + { + $a = Period::make('2018-01-01', '2018-01-31'); + $b = Period::make('2018-02-10', '2018-02-20'); + $c = Period::make('2018-02-11', '2018-03-31'); + + $current = Period::make('2018-01-20', '2018-03-15'); + + $diff = $current->diff($a, $b, $c); + + $this->assertCount(1, $diff); + + $this->assertTrue($diff[0]->equals(Period::make('2018-02-01', '2018-02-09'))); + } + + /** + * @test + * + * A [========] + * B [=========] + * CURRENT [============] + * + * OVERLAP [============] + * DIFF + */ + public function if_all_periods_overlap_it_will_determine_that_there_is_no_diff() + { + $a = Period::make('2018-01-15', '2018-02-10'); + $b = Period::make('2017-12-20', '2018-01-15'); + + $current = Period::make('2018-01-01', '2018-01-31'); + + $diff = $current->diff($a, $b); + + $this->assertCount(0, $diff); + } + + /** + * @test + * + * A [========] + * CURRENT [=======] + * + * DIFF [==] + */ + public function it_can_determine_that_there_is_a_diff() + { + $a = Period::make('2018-02-15', '2018-02-20'); + + $current = Period::make('2018-01-01', '2018-01-31'); + + $diff = $current->diff($a); + + $this->assertCount(1, $diff); + + $this->assertTrue($diff[0]->equals(Period::make('2018-02-01', '2018-02-14'))); + } + + /** + * @test + * + * A [====] + * B [========] + * C [=====] + * CURRENT [========================] + * + * DIFF [=] [====] + */ + public function if_can_determine_multiple_diffs() + { + $a = Period::make('2018-01-05', '2018-01-10'); + $b = Period::make('2018-01-15', '2018-03-01'); + $c = Period::make('2017-01-01', '2018-01-02'); + + $current = Period::make('2018-01-01', '2018-01-31'); + + $diff = $current->diff($a, $b, $c); + + $this->assertCount(2, $diff); + + $this->assertTrue($diff[0]->equals(Period::make('2018-01-03', '2018-01-04'))); + $this->assertTrue($diff[1]->equals(Period::make('2018-01-11', '2018-01-14'))); + } + + /** + * @test + * + * A [====] + * B [====] + * CURRENT [=============================] + * + * DIFF [======] [====] [===] + */ + public function if_can_determine_multiple_diffs_for_sure() + { + $a = Period::make('2018-01-15', '2018-01-20'); + $b = Period::make('2018-01-05', '2018-01-10'); + + $current = Period::make('2018-01-01', '2018-01-31'); + + $diff = $current->diff($a, $b); + + $this->assertCount(3, $diff); + + $this->assertTrue($diff[0]->equals(Period::make('2018-01-01', '2018-01-04'))); + $this->assertTrue($diff[1]->equals(Period::make('2018-01-11', '2018-01-14'))); + $this->assertTrue($diff[2]->equals(Period::make('2018-01-21', '2018-01-31'))); + } + + /** @test */ + public function it_accepts_carbon_instances() + { + $a = Period::make(Carbon::make('2018-01-01'), Carbon::make('2018-01-02')); + + $this->assertTrue($a->equals(Period::make('2018-01-01', '2018-01-02'))); + } + + /** @test */ + public function it_will_preserve_the_time() + { + $period = Period::make('2018-01-01 01:02:03', '2018-01-02 04:05:06'); + + $this->assertTrue($period->equals( + new Period( + 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') + ) + )); + } + + /** @test */ + public function if_will_use_the_start_of_day_when_passing_strings_to_a_period() + { + $period = Period::make('2018-01-01', '2018-01-02'); + + $this->assertTrue($period->equals( + new Period( + 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') + ) + )); + } + + /** + * @test + * + * A [=============================] + * B [========] + * + * DIFF [==] [===============] + */ + public function diff_with_one_period_within() + { + $a = Period::make('2018-01-01', '2018-01-31'); + $b = Period::make('2018-01-10', '2018-01-15'); + + $diff = $a->diff($b); + + $this->assertCount(2, $diff); + } + + /** + * @test + * @dataProvider expectedPeriodLengths + */ + public function it_is_iterable(int $expectedCount, Period $period) + { + $this->assertSame($expectedCount, iterator_count($period)); + } + + /** @test */ + public function its_iterator_returns_immutable_dates() + { + $period = Period::make('2018-01-01', '2018-01-15'); + + $this->assertInstanceOf(DateTimeImmutable::class, current($period)); + } + + /** @test */ + public function diff_filters_out_null_object_if_no_gap() + { + $a = Period::make('2019-02-01', '2019-02-01'); + + $b = Period::make('2019-02-02', '2019-02-02'); + + $diff = $a->diff($b); + + $this->assertEmpty($diff); + } + + public function expectedPeriodLengths() + { + return [ + [1, Period::make('2018-01-01', '2018-01-01')], + + [15, Period::make('2018-01-01', '2018-01-15')], + [14, Period::make('2018-01-01', '2018-01-15', null, Boundaries::EXCLUDE_START)], + [14, Period::make('2018-01-01', '2018-01-15', null, Boundaries::EXCLUDE_END)], + [13, Period::make('2018-01-01', '2018-01-15', null, Boundaries::EXCLUDE_ALL)], + + [24, Period::make('2018-01-01 00:00:00', '2018-01-01 23:59:59', Precision::HOUR)], + [24, Period::make('2018-01-01 00:00:00', '2018-01-02 00:00:00', Precision::HOUR, Boundaries::EXCLUDE_END)], + ]; + } +} From 74d134bf09ddaf75efee916ff19f06c548cc6b9e Mon Sep 17 00:00:00 2001 From: andreybolonin Date: Fri, 4 Oct 2019 18:07:20 +0300 Subject: [PATCH 2/2] fix tests --- src/EndlessPeriod.php | 11 +++++++---- src/Period.php | 6 +++++- tests/DateTimeExtensionTest.php | 2 +- tests/EndlessPeriodTest.php | 6 +++--- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/EndlessPeriod.php b/src/EndlessPeriod.php index 3247ee5..e5e7bed 100644 --- a/src/EndlessPeriod.php +++ b/src/EndlessPeriod.php @@ -33,7 +33,7 @@ class EndlessPeriod implements IteratorAggregate, PeriodInterface private $boundaryExclusionMask; /** @var int */ - private $precisionMask; + public $precisionMask; public function __construct( DateTimeImmutable $start, @@ -49,7 +49,7 @@ public function __construct( $this->precisionMask = $precisionMask ?? Precision::DAY; $this->start = $this->roundDate($start, $this->precisionMask); - $this->end = null;//$this->roundDate($end, $this->precisionMask); + $this->end = $end ? $this->roundDate($end, $this->precisionMask) : null; $this->interval = $this->createDateInterval($this->precisionMask); $this->includedStart = $this->startIncluded() @@ -87,7 +87,7 @@ public static function make( return new static( static::resolveDate($start, $format), - null, + $end ? static::resolveDate($end, $format) : null, $precisionMask, $boundaryExclusionMask ); @@ -157,8 +157,11 @@ public function touchesWith(PeriodInterface $period): bool { $this->ensurePrecisionMatches($period); - if ($period instanceof EndlessPeriod) { + if ($period instanceof EndlessPeriod + && (null == $this->getIncludedEnd() && null == $period->getIncludedEnd())) { return true; + } else { + return false; } if ($this->getIncludedStart()->diff($period->getIncludedEnd())->days <= 1) { diff --git a/src/Period.php b/src/Period.php index 8686a2a..aaafe2f 100644 --- a/src/Period.php +++ b/src/Period.php @@ -33,7 +33,7 @@ class Period implements IteratorAggregate, PeriodInterface private $boundaryExclusionMask; /** @var int */ - private $precisionMask; + public $precisionMask; public function __construct( DateTimeImmutable $start, @@ -159,6 +159,10 @@ public function touchesWith(PeriodInterface $period): bool { $this->ensurePrecisionMatches($period); + if ($period instanceof EndlessPeriod && $this->getIncludedEnd()->diff($period->getIncludedStart())->days <= 1) { + + } + if ($this->getIncludedEnd()->diff($period->getIncludedStart())->days <= 1) { return true; } diff --git a/tests/DateTimeExtensionTest.php b/tests/DateTimeExtensionTest.php index 1ed8e74..827562c 100644 --- a/tests/DateTimeExtensionTest.php +++ b/tests/DateTimeExtensionTest.php @@ -57,7 +57,7 @@ class TestPeriod extends Period /** @var DateTimeExtension */ protected $end; - public function __construct(DateTimeExtension $start, DateTimeExtension $end, ?int $precisionMask = null, ?int $boundaryExclusionMask = null) + public function __construct(DateTimeImmutable $start, DateTimeImmutable $end, ?int $precisionMask = null, ?int $boundaryExclusionMask = null) { parent::__construct($start, $end, $precisionMask, $boundaryExclusionMask); } diff --git a/tests/EndlessPeriodTest.php b/tests/EndlessPeriodTest.php index 20e95b5..aa6d74d 100644 --- a/tests/EndlessPeriodTest.php +++ b/tests/EndlessPeriodTest.php @@ -33,12 +33,12 @@ public function it_can_determine_if_two_periods_overlap_with_each_other(Period $ public function it_can_determine_if_two_periods_touch_each_other() { $this->assertTrue( - EndlessPeriod::make('2018-01-01') + Period::make('2018-01-01', '2018-01-02') ->touchesWith(EndlessPeriod::make('2018-01-02')) ); - $this->assertTrue( - EndlessPeriod::make('2018-01-02', '2018-01-02') + $this->assertFalse( + EndlessPeriod::make('2018-01-02') ->touchesWith(EndlessPeriod::make('2018-01-01', '2018-01-01')) );