Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add uniqueIntervals Method for Non-Overlapping Interval Calculation #139

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
});