67 lines
2.1 KiB
Python
67 lines
2.1 KiB
Python
"""Interval merge utilities.
|
|
|
|
Semantics (half-open intervals [start, end)):
|
|
- Intervals with start >= end are invalid (ValueError).
|
|
- Two intervals overlap/touch if a.start <= b.end and b.start <= a.end? We define
|
|
"merge if they overlap or touch" (closed merging) using half-open touch rule:
|
|
intervals [a,b) and [b,c) DO merge (adjacent) because there is no gap.
|
|
intervals [a,b) and [b+1,c) do NOT merge.
|
|
- Output is sorted, disjoint, merged, each as (start, end) tuple.
|
|
- Empty input -> [].
|
|
"""
|
|
|
|
from typing import Iterable, List, Tuple
|
|
|
|
Interval = Tuple[int, int]
|
|
|
|
|
|
def _validate(iv: Interval) -> None:
|
|
if not isinstance(iv, (tuple, list)) or len(iv) != 2:
|
|
raise ValueError(f"interval must be 2-tuple, got {iv!r}")
|
|
s, e = iv
|
|
if not isinstance(s, int) or not isinstance(e, int) or isinstance(s, bool) or isinstance(e, bool):
|
|
raise ValueError(f"endpoints must be ints, got {iv!r}")
|
|
if s >= e:
|
|
raise ValueError(f"empty/invalid interval {iv!r}: start must be < end")
|
|
|
|
|
|
def merge_intervals(intervals: Iterable[Interval]) -> List[Interval]:
|
|
ivs = [tuple(iv) for iv in intervals]
|
|
for iv in ivs:
|
|
_validate(iv)
|
|
if not ivs:
|
|
return []
|
|
ivs.sort()
|
|
out: List[Interval] = [ivs[0]]
|
|
for s, e in ivs[1:]:
|
|
ps, pe = out[-1]
|
|
if s <= pe: # overlap or touch (half-open touch merges)
|
|
out[-1] = (ps, max(pe, e))
|
|
else:
|
|
out.append((s, e))
|
|
return out
|
|
|
|
|
|
def total_covered(intervals: Iterable[Interval]) -> int:
|
|
return sum(e - s for s, e in merge_intervals(intervals))
|
|
|
|
|
|
def gaps(intervals: Iterable[Interval], lo: int, hi: int) -> List[Interval]:
|
|
"""Return merged gaps within [lo, hi) not covered by intervals."""
|
|
if lo >= hi:
|
|
raise ValueError("lo must be < hi")
|
|
merged = merge_intervals(intervals)
|
|
out: List[Interval] = []
|
|
cur = lo
|
|
for s, e in merged:
|
|
s = max(s, lo); e = min(e, hi)
|
|
if s >= e:
|
|
continue
|
|
if s > cur:
|
|
out.append((cur, s))
|
|
cur = max(cur, e)
|
|
if cur >= hi:
|
|
break
|
|
if cur < hi:
|
|
out.append((cur, hi))
|
|
return out |