198 lines
6.8 KiB
Python
198 lines
6.8 KiB
Python
|
|
"""Parsing for the indexed_range argument syntax.
|
||
|
|
|
||
|
|
An indexed_range selects positions in one dimension of a grid, with an
|
||
|
|
optional extent in the other dimension. The same syntax serves the table
|
||
|
|
klammer's :hline and :vline arguments (index = boundary, subsets = how far
|
||
|
|
along the line) and its :colspan and :rowspan arguments (index = row or
|
||
|
|
column, subsets = the cells to merge). Which dimension the index selects
|
||
|
|
is a property of the argument, not of the syntax.
|
||
|
|
|
||
|
|
This module replaces the former sequences.py and span.py (see debris).
|
||
|
|
"""
|
||
|
|
|
||
|
|
import re
|
||
|
|
|
||
|
|
syntax_description = """
|
||
|
|
An indexed_range is a selector, optionally followed by parenthesized
|
||
|
|
subsets, written with no spaces:
|
||
|
|
|
||
|
|
<selector> full extent
|
||
|
|
<selector>(<subsets>) restricted extent
|
||
|
|
|
||
|
|
The selector is a single index "3", a closed index range "2-5", an open
|
||
|
|
index range "2-" (to the last index), or a name defined by the argument
|
||
|
|
(for example "top" or "inner" for table lines). Subsets are separated by
|
||
|
|
commas; each is an index "4", a closed range "1-4", or an open range "6-"
|
||
|
|
(to the end). All indices are zero-origin.
|
||
|
|
|
||
|
|
Examples:
|
||
|
|
3 index 3, full extent
|
||
|
|
2-5(0-2) indices 2 through 5, each restricted to 0 through 2
|
||
|
|
3(1-4,6-9) index 3, restricted to 1-4 and 6-9
|
||
|
|
head(1-) with table hline names: boundary 1, from column 1 on
|
||
|
|
""".strip()
|
||
|
|
|
||
|
|
|
||
|
|
class Range_error(Exception):
|
||
|
|
def __init__(self, message):
|
||
|
|
super().__init__(f"{message}\n\n{syntax_description}")
|
||
|
|
|
||
|
|
|
||
|
|
item_rgx = re.compile(r"(?:(\d+)(-)?(\d*)|([A-Za-z]+))(?:\(([\d,\-]+)\))?$")
|
||
|
|
subset_rgx = re.compile(r"(\d+)(-)?(\d*)$")
|
||
|
|
|
||
|
|
|
||
|
|
def hline_names(count):
|
||
|
|
"""Boundary-name map for horizontal lines; count = row_count + 1."""
|
||
|
|
last = count - 1
|
||
|
|
return {"top": [0],
|
||
|
|
"head": [1],
|
||
|
|
"bottom": [last],
|
||
|
|
"inner": list(range(1, last)),
|
||
|
|
"all": list(range(count))}
|
||
|
|
|
||
|
|
|
||
|
|
def vline_names(count):
|
||
|
|
"""Boundary-name map for vertical lines; count = column_count + 1."""
|
||
|
|
last = count - 1
|
||
|
|
return {"outer": [0, last],
|
||
|
|
"inner": list(range(1, last)),
|
||
|
|
"all": list(range(count))}
|
||
|
|
|
||
|
|
|
||
|
|
class Indexed_range:
|
||
|
|
"""The selected extent for one primary-dimension index."""
|
||
|
|
|
||
|
|
def __init__(self, index, maxval):
|
||
|
|
self.index = index
|
||
|
|
self.maxval = maxval
|
||
|
|
self.all = False # Full extent (no subsets given)
|
||
|
|
self.ranges = [] # [[start, end], ...], inclusive
|
||
|
|
|
||
|
|
def add_full(self):
|
||
|
|
self.all = True
|
||
|
|
self.ranges = [[0, self.maxval]]
|
||
|
|
|
||
|
|
def add_ranges(self, ranges):
|
||
|
|
if not self.all:
|
||
|
|
self.ranges += ranges
|
||
|
|
|
||
|
|
def has(self, i):
|
||
|
|
return any(start <= i <= end for start, end in self.ranges)
|
||
|
|
|
||
|
|
def items(self, invert=False):
|
||
|
|
result = []
|
||
|
|
for start, end in self.ranges:
|
||
|
|
for e in range(start, end + 1):
|
||
|
|
result.append((e, self.index) if invert else (self.index, e))
|
||
|
|
return result
|
||
|
|
|
||
|
|
def __str__(self):
|
||
|
|
subsets = ",".join([f"{s}-{e}" for s, e in self.ranges])
|
||
|
|
return f"{self.index}({subsets})"
|
||
|
|
|
||
|
|
def __repr__(self):
|
||
|
|
return self.__str__()
|
||
|
|
|
||
|
|
|
||
|
|
class Indexed_ranges:
|
||
|
|
"""A parsed indexed_range argument: Indexed_range entries by index.
|
||
|
|
|
||
|
|
count - number of valid primary indices (0 .. count-1)
|
||
|
|
maxval - largest valid subset value (the cross dimension)
|
||
|
|
specs - the argument value: a list of items (from the argtype's
|
||
|
|
python_cast), a whitespace-separated string, or None
|
||
|
|
names - map of selector names to index lists (hline_names, ...)
|
||
|
|
argument - argument name for error messages (":hline", ...)
|
||
|
|
|
||
|
|
Items targeting the same index merge: their subsets are unioned, and a
|
||
|
|
full-extent item absorbs any subsets.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(self, count, maxval, specs, names=None, argument=""):
|
||
|
|
self.count = count
|
||
|
|
self.maxval = maxval
|
||
|
|
self.names = names or {}
|
||
|
|
self.argument = argument
|
||
|
|
self.by_index = {}
|
||
|
|
if specs is None:
|
||
|
|
specs = []
|
||
|
|
elif isinstance(specs, str):
|
||
|
|
specs = specs.split()
|
||
|
|
for spec in specs:
|
||
|
|
self.parse(spec)
|
||
|
|
|
||
|
|
def error(self, message):
|
||
|
|
argument = f"{self.argument} argument: " if self.argument else ""
|
||
|
|
raise Range_error(f"{argument}{message}")
|
||
|
|
|
||
|
|
def parse(self, spec):
|
||
|
|
match = item_rgx.match(spec)
|
||
|
|
if not match:
|
||
|
|
self.error(f'"{spec}" is not a valid indexed_range.')
|
||
|
|
number, hyphen, end, name, subsets = match.groups()
|
||
|
|
if name is not None:
|
||
|
|
if name not in self.names:
|
||
|
|
known = " ".join(self.names) or "none"
|
||
|
|
self.error(f'"{name}" is not a valid name here '
|
||
|
|
f"(valid names: {known}).")
|
||
|
|
indices = self.names[name]
|
||
|
|
else:
|
||
|
|
start = int(number)
|
||
|
|
if not hyphen:
|
||
|
|
indices = [start]
|
||
|
|
else:
|
||
|
|
last = int(end) if end else self.count - 1
|
||
|
|
if start > last:
|
||
|
|
self.error(f'In "{spec}", the index range start {start} '
|
||
|
|
f"is greater than its end {last}.")
|
||
|
|
indices = list(range(start, last + 1))
|
||
|
|
for i in indices:
|
||
|
|
if i >= self.count:
|
||
|
|
self.error(f'In "{spec}", index {i} is out of range '
|
||
|
|
f"(0 through {self.count - 1}).")
|
||
|
|
ranges = self.parse_subsets(spec, subsets) if subsets else None
|
||
|
|
for i in indices:
|
||
|
|
entry = self.by_index.setdefault(i, Indexed_range(i, self.maxval))
|
||
|
|
if ranges is None:
|
||
|
|
entry.add_full()
|
||
|
|
else:
|
||
|
|
entry.add_ranges(ranges)
|
||
|
|
|
||
|
|
def parse_subsets(self, spec, subsets):
|
||
|
|
ranges = []
|
||
|
|
for part in subsets.split(","):
|
||
|
|
match = subset_rgx.match(part)
|
||
|
|
if not match:
|
||
|
|
self.error(f'In "{spec}", "{part}" is not a valid subset.')
|
||
|
|
number, hyphen, end = match.groups()
|
||
|
|
start = int(number)
|
||
|
|
if not hyphen:
|
||
|
|
last = start
|
||
|
|
else:
|
||
|
|
last = int(end) if end else self.maxval
|
||
|
|
if start > last:
|
||
|
|
self.error(f'In "{spec}", the subset start {start} '
|
||
|
|
f"is greater than its end {last}.")
|
||
|
|
if last > self.maxval:
|
||
|
|
self.error(f'In "{spec}", {last} is out of range '
|
||
|
|
f"(0 through {self.maxval}).")
|
||
|
|
ranges.append([start, last])
|
||
|
|
return ranges
|
||
|
|
|
||
|
|
def __getitem__(self, index):
|
||
|
|
return self.by_index.get(index)
|
||
|
|
|
||
|
|
def __iter__(self):
|
||
|
|
return iter(self.by_index)
|
||
|
|
|
||
|
|
def has(self, index, i):
|
||
|
|
entry = self[index]
|
||
|
|
return entry.has(i) if entry else False
|
||
|
|
|
||
|
|
def __str__(self):
|
||
|
|
return " ".join([str(self.by_index[i]) for i in sorted(self.by_index)])
|
||
|
|
|
||
|
|
def __repr__(self):
|
||
|
|
return self.__str__()
|