| # Licensed to the Apache Software Foundation (ASF) under one | |
| # or more contributor license agreements. See the NOTICE file | |
| # distributed with this work for additional information | |
| # regarding copyright ownership. The ASF licenses this file | |
| # to you under the Apache License, Version 2.0 (the | |
| # "License"); you may not use this file except in compliance | |
| # with the License. You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, | |
| # software distributed under the License is distributed on an | |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| # KIND, either express or implied. See the License for the | |
| # specific language governing permissions and limitations | |
| # under the License. | |
| """Range utilities for global index.""" | |
| from dataclasses import dataclass | |
| from typing import List | |
| @dataclass | |
| class Range: | |
| """Represents a range [from_, to] inclusive.""" | |
| from_: int | |
| to: int | |
| def __post_init__(self): | |
| if self.from_ > self.to: | |
| raise ValueError(f"Invalid range: from ({self.from_}) > to ({self.to})") | |
| def contains(self, value: int) -> bool: | |
| """Check if the range contains the given value.""" | |
| return self.from_ <= value <= self.to | |
| def overlaps(self, other: 'Range') -> bool: | |
| """Check if this range overlaps with another range.""" | |
| return Range.intersect(self.from_, self.to, other.from_, other.to) | |
| def exclude(self, ranges: List['Range']) -> List['Range']: | |
| """ | |
| Exclude the given ranges from this range. | |
| Returns a list of non-overlapping ranges. | |
| """ | |
| if not ranges: | |
| return [self] | |
| # Sort ranges by start | |
| sorted_ranges = sorted(ranges, key=lambda r: r.from_) | |
| result = [] | |
| current_start = self.from_ | |
| for r in sorted_ranges: | |
| if r.to < current_start: | |
| # Range is completely before current position, skip | |
| continue | |
| if r.from_ > self.to: | |
| # Range is completely after our range, stop | |
| break | |
| if r.from_ > current_start: | |
| # There's a gap before this range | |
| result.append(Range(current_start, min(r.from_ - 1, self.to))) | |
| # Move current_start past this range | |
| current_start = max(current_start, r.to + 1) | |
| # Add remaining part after all ranges | |
| if current_start <= self.to: | |
| result.append(Range(current_start, self.to)) | |
| return result | |
| @staticmethod | |
| def to_ranges(values: List[int]) -> List['Range']: | |
| if not values: | |
| return [] | |
| sorted_ids = sorted(values) | |
| result = [] | |
| range_start = sorted_ids[0] | |
| range_end = range_start | |
| for current in sorted_ids[1:]: | |
| if current != range_end + 1: | |
| result.append(Range(range_start, range_end)) | |
| range_start = current | |
| range_end = current | |
| result.append(Range(range_start, range_end)) | |
| return result | |
| @staticmethod | |
| def intersect(start1: int, end1: int, start2: int, end2: int) -> bool: | |
| """Check if two ranges intersect.""" | |
| return not (end1 < start2 or end2 < start1) | |
| def count(self) -> int: | |
| """Return the number of elements in the range.""" | |
| return self.to - self.from_ + 1 | |
| @staticmethod | |
| def and_(ranges1: List['Range'], ranges2: List['Range']) -> List['Range']: | |
| """ | |
| Compute the intersection of two lists of ranges. | |
| Similar to Java's Range.and(fileRanges, rowRanges). | |
| Args: | |
| ranges1: First list of ranges | |
| ranges2: Second list of ranges | |
| Returns: | |
| List of ranges representing the intersection | |
| """ | |
| if not ranges1 or not ranges2: | |
| return [] | |
| # Sort both lists | |
| sorted1 = sorted(ranges1, key=lambda r: r.from_) | |
| sorted2 = sorted(ranges2, key=lambda r: r.from_) | |
| result = [] | |
| i, j = 0, 0 | |
| while i < len(sorted1) and j < len(sorted2): | |
| r1 = sorted1[i] | |
| r2 = sorted2[j] | |
| # Find intersection | |
| start = max(r1.from_, r2.from_) | |
| end = min(r1.to, r2.to) | |
| if start <= end: | |
| result.append(Range(start, end)) | |
| # Move the range that ends first | |
| if r1.to < r2.to: | |
| i += 1 | |
| elif r1.to > r2.to: | |
| j += 1 | |
| else: | |
| i += 1 | |
| j += 1 | |
| return result | |
| @staticmethod | |
| def merge_sorted_as_possible(ranges: List['Range']) -> List['Range']: | |
| """ | |
| Merge sorted ranges where possible (adjacent or overlapping). | |
| Similar to Java's Range.mergeSortedAsPossible(). | |
| """ | |
| if not ranges: | |
| return [] | |
| sorted_ranges = sorted(ranges, key=lambda r: r.from_) | |
| result = [sorted_ranges[0]] | |
| for r in sorted_ranges[1:]: | |
| last = result[-1] | |
| if r.from_ <= last.to + 1: | |
| # Merge with last range | |
| result[-1] = Range(last.from_, max(last.to, r.to)) | |
| else: | |
| result.append(r) | |
| return result | |
| @staticmethod | |
| def sort_and_merge_overlap(ranges: List['Range'], merge: bool = True, adjacent: bool = True) -> List['Range']: | |
| """ | |
| Sort ranges and optionally merge overlapping ones. | |
| """ | |
| if not ranges: | |
| return [] | |
| sorted_ranges = sorted(ranges, key=lambda r: r.from_) | |
| if not merge: | |
| return sorted_ranges | |
| adjacent_value = 1 if adjacent else 0 | |
| result = [sorted_ranges[0]] | |
| for r in sorted_ranges[1:]: | |
| last = result[-1] | |
| if r.from_ <= last.to + adjacent_value: | |
| # Merge with last range | |
| result[-1] = Range(last.from_, max(last.to, r.to)) | |
| else: | |
| result.append(r) | |
| return result | |
| def __eq__(self, other: object) -> bool: | |
| if not isinstance(other, Range): | |
| return False | |
| return self.from_ == other.from_ and self.to == other.to | |
| def __hash__(self) -> int: | |
| return hash((self.from_, self.to)) | |
| def __repr__(self) -> str: | |
| return f"Range({self.from_}, {self.to})" |