from typing import Optional, Tuple, Union, overload, List, Any, TypeVar, IO, Iterable, Sequence, Dict, Callable T = TypeVar('T') @overload def part_prefix(line: str, char: str, from_right=False) -> Tuple[Optional[str], str]: pass @overload def part_prefix(line: str, char: str, missing: T, from_right=False) -> Tuple[Union[str, T], str]: pass def part_prefix(line: str, char: str, missing=None, from_right=False): try: if from_right: i = line.rindex(char) else: i = line.index(char) return line[:i], line[i + len(char):] except ValueError: return missing, line @overload def part_suffix(line: str, char: str, from_right=False) -> Tuple[str, Optional[str]]: pass @overload def part_suffix(line: str, char: str, missing: T, from_right=False) -> Tuple[str, Union[str, T]]: pass def part_suffix(line: str, char: str, missing=None, from_right=False): try: if from_right: i = line.rindex(char) else: i = line.index(char) return line[:i], line[i + len(char):] except ValueError: return line, missing def print_block(block: str, file: IO = None): lines = block.split('\n') if len(lines) == 0: return if len(lines[0]) != 0: indent = len(lines[0]) - len(lines[0].lstrip()) elif len(lines[1]) != 0: indent = len(lines[1]) - len(lines[1].lstrip()) else: indent = 0 for i, line in enumerate(lines): line = line[indent:] if i == 0 and len(line) == 0: continue print(line, file=file) def _line_max_length(ls: List[str]) -> int: length = 0 for s in ls: length = max(length, len(s)) return length def list_padding(ls: List[str], ele: Optional[List[str]] = None, align_right=False, padding=' ', split=' '): """align""" if ele is None: length = _line_max_length(ls) for i, ln in enumerate(ls): ls[i] = ln + split + padding * (length - len(ln)) else: if len(ls) != len(ele): raise RuntimeError('size not match %d != %d' % (len(ls), len(ele))) if align_right: length = _line_max_length(ls) ele_length = _line_max_length(ele) for i in range(len(ls)): ls[i] = ls[i] + padding * (length - len(ls[i])) + split + padding * (ele_length - len(ele[i])) + ele[i] else: length = _line_max_length(ls) for i in range(len(ls)): ls[i] = ls[i] + padding * (length - len(ls[i])) + split + ele[i] def _expr_format_enclose(expr: str, from_index: int) -> Tuple[int, str]: if expr[from_index] != '{': raise ValueError('lost "{"') i = expr.find('}', from_index) if i < 0: raise ValueError('lost "}"') return i + 1, expr[from_index + 1: i] def expr_format(expr: str, mapper: Callable[[str], Optional[str]]) -> str: a = [] try: p = 0 while True: i = expr.index('%', p) c = expr[i + 1] j = i + 2 if c == '%': a.append('%') elif c == '?': c = expr[i + 2] if ord('A') <= ord(c) <= ord('Z'): c = expr[i + 2:i + 4] j = i + 4 else: j = i + 3 j, e = _expr_format_enclose(expr, j) r = mapper(c) if r is None: a.append('') else: a.append(e.replace('%', r)) else: if ord('A') <= ord(c) <= ord('Z'): c = expr[i + 1:i + 3] j = i + 3 r = mapper(c) if r is None: r = '' a.append(r) p = i + 2 expr = expr[:i] + '%s' + expr[j:] except ValueError: pass except IndexError as e: raise ValueError('lost field') from e return expr % tuple(a) class Table: def __init__(self, *title: str, column: int = None, align_right=True): if len(title) == 0: if column is None: raise ValueError('lost column') self._cols: List[List[str]] = [[] for _ in range(column)] else: self._cols: List[List[str]] = [[t] for t in title] self._fmt: List[Dict[str, Any]] = [{'align_right': align_right} for _ in self._cols] @property def columns(self) -> int: return len(self._cols) @property def rows(self) -> int: return len(self._cols[0]) def get_column(self, text: str) -> Optional[int]: for c in range(len(self._cols)): if self._cols[c][0] == text: return c return None def append(self, *content: Any): for i, col in enumerate(self._cols): try: elem = content[i] except IndexError: col.append('') else: col.append(str(elem) if elem is not None else '') def extend(self, content: Iterable[Sequence[Any]]): for line in content: for i, col in enumerate(self._cols): try: elem = line[i] except IndexError: col.append('') else: col.append(str(elem) if elem is not None else '') def set_format(self, column: Union[None, int, str, List[int], List[str]], align_right=True, padding=' ', split=None): f = { 'align_right': align_right, 'padding': padding, } if split is not None: f['split'] = split if column is None: for c in range(self.columns): self._fmt[c] = f elif isinstance(column, int): self._fmt[column] = f elif isinstance(column, str): c = self.get_column(column) if c is not None: self._fmt[c] = f elif isinstance(column, (tuple, list)): if len(column): if isinstance(column[0], int): for c in column: self._fmt[c] = f else: for t in column: c = self.get_column(t) if c is not None: self._fmt[c] = f else: raise ValueError('column : ' + str(t)) else: raise TypeError('column') def lines(self) -> List[str]: ret = ['' for _ in self._cols[0]] for i, col in enumerate(self._cols): list_padding(ret, col, **self._fmt[i]) return ret def print(self, indent='', file: IO = None): for line in self.lines(): print(indent, line, sep='', file=file) def dump(self, file: IO = None): for r in range(len(self._cols[0])): print(','.join(map(lambda col: col[r], self._cols)), file=file)