Skip to content

Commit

Permalink
Implement PeriodCollection UniqueIntervals
Browse files Browse the repository at this point in the history
  • Loading branch information
yehia-khalil committed Nov 5, 2024
1 parent 7ab2320 commit aa5cd21
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 0 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,31 @@ Merges all periods in collection with overlapping ranges.

![](./docs/img/collection-union.png)

### `uniqueIntervals(): static`

Returns a collection of unique, non-overlapping intervals by breaking down overlapping or intersecting periods within the original collection.

This method is helpful when you want to understand distinct time ranges across multiple periods without overlapping segments. It iterates through all periods in the collection and recursively splits them until all overlapping sections are fully separated, resulting in a collection of unique, non-overlapping intervals.

```php
$periods = new PeriodCollection(
Period::make('2024-08-01', '2024-08-05'),
Period::make('2024-08-03', '2024-08-07'),
Period::make('2024-08-06', '2024-08-10')
);

$uniqueIntervals = $periods->uniqueIntervals()->sort();

foreach ($uniqueIntervals as $interval) {
echo $interval->start()->format('Y-m-d') . ' - ' . $interval->end()->format('Y-m-d') . PHP_EOL;
}

// Output:
// 2024-08-01 - 2024-08-02
// 2024-08-03 - 2024-08-05
// 2024-08-06 - 2024-08-07
// 2024-08-08 - 2024-08-10
```
---

Finally, there are a few utility methods available on `PeriodCollection` as well:
Expand Down
48 changes: 48 additions & 0 deletions src/PeriodCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -211,4 +211,52 @@ public function union(): PeriodCollection

return static::make($boundaries)->subtract($boundaries->subtract(...$this));
}

public function uniqueIntervals(): PeriodCollection
{
return $this->processIntervalsRecursively($this->periods);
}

private function processIntervalsRecursively(array $periods): PeriodCollection
{
$uniquePeriods = [];
$newSegments = [];

foreach ($periods as $i => $currentPeriod) {
$hasOverlap = false;

foreach ($periods as $j => $otherPeriod) {
if ($i === $j) continue;

if (!$currentPeriod->overlapsWith($otherPeriod)) {
continue;
}
$hasOverlap = true;

$intersection = $currentPeriod->overlap($otherPeriod);
$subtracted = $currentPeriod->subtract($otherPeriod);

if ($intersection) $newSegments[] = $intersection;
if (!empty($subtracted)) {
foreach ($subtracted as $segment) {
$newSegments[] = $segment;
}
}
}

if (!$hasOverlap) {
$uniquePeriods[] = $currentPeriod;
}
}

$newSegments = self::make(...array_filter($newSegments))->unique()->periods;

if (empty($newSegments)) {
return new PeriodCollection(...$uniquePeriods);
}

return $this->processIntervalsRecursively($newSegments)
->add(...$uniquePeriods)
->unique();
}
}
107 changes: 107 additions & 0 deletions tests/PeriodCollectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -305,3 +305,110 @@
expect($unioned[2]->start() == $collection[3]->start())->toBeTrue();
expect($unioned[2]->end() == $collection[4]->end())->toBeTrue();
});

/**
* Given periods:
*
* A [==========]
* B [==========]
* C [==========]
* OVERLAP [==] [=====] [==] [====]
*/
it('can determine unique non-overlapping intervals from overlapping periods', function () {
$periods = new PeriodCollection(
Period::make('2024-08-01', '2024-08-05'),
Period::make('2024-08-03', '2024-08-07'),
Period::make('2024-08-06', '2024-08-10')
);

$uniqueIntervals = $periods->uniqueIntervals();
$uniqueIntervals = $uniqueIntervals->sort();

expect($uniqueIntervals)->toHaveCount(4);

expect($uniqueIntervals[0]->equals(Period::make('2024-08-01', '2024-08-02')))->toBeTrue();
expect($uniqueIntervals[1]->equals(Period::make('2024-08-03', '2024-08-05')))->toBeTrue();
expect($uniqueIntervals[2]->equals(Period::make('2024-08-06', '2024-08-07')))->toBeTrue();
expect($uniqueIntervals[3]->equals(Period::make('2024-08-08', '2024-08-10')))->toBeTrue();
});

/**
* Given periods:
*
* A [=====]
* B [====]
* C [=====]
*
* Expected unique intervals:
*
* Result [==][==][=][==]
*/
it('can handle unique intervals from partially overlapping periods', function () {
$periods = new PeriodCollection(
Period::make('2024-08-01', '2024-08-04'),
Period::make('2024-08-03', '2024-08-05'),
Period::make('2024-08-05', '2024-08-07')
);

$uniqueIntervals = $periods->uniqueIntervals();
$uniqueIntervals = $uniqueIntervals->sort();

expect($uniqueIntervals)->toHaveCount(4);

expect($uniqueIntervals[0]->equals(Period::make('2024-08-01', '2024-08-02')))->toBeTrue();
expect($uniqueIntervals[1]->equals(Period::make('2024-08-03', '2024-08-04')))->toBeTrue();
expect($uniqueIntervals[2]->equals(Period::make('2024-08-05', '2024-08-05')))->toBeTrue();
expect($uniqueIntervals[3]->equals(Period::make('2024-08-06', '2024-08-07')))->toBeTrue();
});

/**
* Given periods:
*
* A [==========]
* B [===]
*
* Expected unique intervals:
*
* Result [==][===][=====]
*/
it('can handle unique intervals from a period that fully contains another period', function () {
$periods = new PeriodCollection(
Period::make('2024-08-01', '2024-08-10'),
Period::make('2024-08-03', '2024-08-05')
);

$uniqueIntervals = $periods->uniqueIntervals();
$uniqueIntervals = $uniqueIntervals->sort();

expect($uniqueIntervals)->toHaveCount(3);

expect($uniqueIntervals[0]->equals(Period::make('2024-08-01', '2024-08-02')))->toBeTrue();
expect($uniqueIntervals[1]->equals(Period::make('2024-08-03', '2024-08-05')))->toBeTrue();
expect($uniqueIntervals[2]->equals(Period::make('2024-08-06', '2024-08-10')))->toBeTrue();
});

/**
* Given periods:
*
* A [===]
* B [===]
* C [===]
*
* Expected unique intervals (no changes since they're non-overlapping):
*
* Result [===] [===] [===]
*/
it('unique intervals returns non-overlapping periods as they are', function () {
$periods = new PeriodCollection(
Period::make('2024-08-01', '2024-08-03'),
Period::make('2024-08-05', '2024-08-07'),
Period::make('2024-08-09', '2024-08-11')
);

$uniqueIntervals = $periods->uniqueIntervals();
$uniqueIntervals = $uniqueIntervals->sort();

expect($uniqueIntervals[0]->equals(Period::make('2024-08-01', '2024-08-03')))->toBeTrue();
expect($uniqueIntervals[1]->equals(Period::make('2024-08-05', '2024-08-07')))->toBeTrue();
expect($uniqueIntervals[2]->equals(Period::make('2024-08-09', '2024-08-11')))->toBeTrue();
});

0 comments on commit aa5cd21

Please sign in to comment.