Merge Overlapping Intervals
Normalize half-open intervals, merge overlaps or adjacent ranges, and reject invalid bounds with clear validation errors.
Code
from __future__ import annotations
from collections.abc import Iterable
from dataclasses import dataclass
@dataclass(frozen=True, order=True)
class Interval:
start: int
end: int
def __post_init__(self) -> None:
if isinstance(self.start, bool) or isinstance(self.end, bool):
raise TypeError("interval bounds must be integers")
if not isinstance(self.start, int) or not isinstance(self.end, int):
raise TypeError("interval bounds must be integers")
if self.start >= self.end:
raise ValueError(
f"invalid interval [{self.start}, {self.end})"
)
def _coerce(item: Interval | tuple[int, int]) -> Interval:
if isinstance(item, Interval):
return item
if isinstance(item, tuple) and len(item) == 2:
return Interval(item[0], item[1])
raise TypeError("intervals must be Interval instances or (start, end) tuples")
def merge_intervals(
intervals: Iterable[Interval | tuple[int, int]],
*,
adjacent: bool = True,
) -> list[Interval]:
if not isinstance(adjacent, bool):
raise TypeError("adjacent must be a boolean")
ordered = sorted(_coerce(item) for item in intervals)
merged: list[Interval] = []
for interval in ordered:
if not merged:
merged.append(interval)
continue
previous = merged[-1]
touches = (
interval.start <= previous.end
if adjacent
else interval.start < previous.end
)
if touches:
merged[-1] = Interval(
previous.start,
max(previous.end, interval.end),
)
else:
merged.append(interval)
return merged
def main() -> None:
samples = [(1, 3), (2, 6), (8, 10), (10, 12), (20, 21)]
print(merge_intervals(samples))
print(merge_intervals(samples, adjacent=False))
print(merge_intervals([]))
try:
merge_intervals([(5, 5)])
except ValueError as error:
print(error)
try:
merge_intervals([(1, 2), ("3", 4)])
except TypeError as error:
print(error)
if __name__ == "__main__":
main()Report a problem with this exampleOpens a pre-filled issue on GitHub.