| """Shared typed data structures used across STRATA modules.""" |
|
|
| from __future__ import annotations |
|
|
| from dataclasses import dataclass |
|
|
|
|
| @dataclass(frozen=True, slots=True) |
| class CharSpan: |
| """Half-open character span in Python string coordinates.""" |
|
|
| start: int |
| end: int |
|
|
| def __post_init__(self) -> None: |
| if self.start < 0: |
| raise ValueError(f"char span start must be non-negative, got {self.start}") |
| if self.end < self.start: |
| raise ValueError( |
| f"char span end must be >= start, got start={self.start}, end={self.end}" |
| ) |
|
|
| @property |
| def length(self) -> int: |
| return self.end - self.start |
|
|
| def overlaps(self, other: "CharSpan") -> bool: |
| return self.start < other.end and other.start < self.end |
|
|
| def contains(self, other: "CharSpan") -> bool: |
| return self.start <= other.start and other.end <= self.end |
|
|
|
|
| @dataclass(frozen=True, slots=True) |
| class ByteSpan: |
| """Half-open byte span in UTF-8 encoded coordinates.""" |
|
|
| start: int |
| end: int |
|
|
| def __post_init__(self) -> None: |
| if self.start < 0: |
| raise ValueError(f"byte span start must be non-negative, got {self.start}") |
| if self.end < self.start: |
| raise ValueError( |
| f"byte span end must be >= start, got start={self.start}, end={self.end}" |
| ) |
|
|
| @property |
| def length(self) -> int: |
| return self.end - self.start |
|
|
| def overlaps(self, other: "ByteSpan") -> bool: |
| return self.start < other.end and other.start < self.end |
|
|
| def contains(self, other: "ByteSpan") -> bool: |
| return self.start <= other.start and other.end <= self.end |
|
|
|
|
| @dataclass(frozen=True, slots=True) |
| class AnchoredSpan: |
| """Span carrying both character and byte coordinates.""" |
|
|
| char: CharSpan |
| byte: ByteSpan |
|
|
| @classmethod |
| def from_offsets( |
| cls, |
| *, |
| char_start: int, |
| char_end: int, |
| byte_start: int, |
| byte_end: int, |
| ) -> "AnchoredSpan": |
| return cls( |
| char=CharSpan(char_start, char_end), |
| byte=ByteSpan(byte_start, byte_end), |
| ) |
|
|
| def overlaps(self, other: "AnchoredSpan") -> bool: |
| return self.byte.overlaps(other.byte) and self.char.overlaps(other.char) |
|
|