- commit
- 9b711dfb6fac33d95989e79836fb8a7326ff4de7
- parent
- e96a2a8d36523dbeb960190e1bd43b2809fba8d5
- Author
- Tobias Bengfort <tobias.bengfort@posteo.de>
- Date
- 2026-05-08 10:13
refactor: types
Diffstat
| M | sheet/__main__.py | 7 | ++++--- |
| M | sheet/expression.py | 359 | ++++++++++++++++++++++++++++++++++++++----------------------- |
| M | sheet/sheet.py | 109 | ++++++++++++++++++++++++++++++------------------------------- |
3 files changed, 280 insertions, 195 deletions
diff --git a/sheet/__main__.py b/sheet/__main__.py
@@ -8,8 +8,9 @@ from .csv import load_csv 8 8 from .expression import x2col 9 9 from .input import Input 10 10 from .sheet import Bar11 -1 from .sheet import Sheet-1 11 from .sheet import Value 12 12 from .sheet import iter_range -1 13 from .sheet import Sheet 13 14 from .term import align_center 14 15 from .term import align_left 15 16 from .term import align_right @@ -37,8 +38,8 @@ q - quit 37 38 """ 38 39 39 4040 -1 def to_cell(value: float|int|str|None|Exception, width: int) -> str:41 -1 if isinstance(value, float|int):-1 41 def to_cell(value: Value, width: int) -> str: -1 42 if isinstance(value, float | int): 42 43 s = f'{{:{width}.{min(width - 2, 6)}g}}'.format(value) 43 44 return align_right(s, width) 44 45 elif isinstance(value, str):
diff --git a/sheet/expression.py b/sheet/expression.py
@@ -1,43 +1,45 @@ -1 1 import abc 1 2 import re 2 3 import string -1 4 from dataclasses import dataclass 3 5 4 6 5 7 class ParseError(ValueError): 6 8 pass 7 9 8 109 -1 def parse_any(text, parsers):10 -1 for parser in parsers:11 -1 try:12 -1 return parser(text)13 -1 except ParseError:14 -1 pass15 -1 raise ParseError(f'None of the subparsers matched for {text}')-1 11 class Expr(abc.ABC): -1 12 @classmethod -1 13 @abc.abstractmethod -1 14 def parse(cls, text: str) -> tuple['Expr', str]: -1 15 ... -1 16 -1 17 @abc.abstractmethod -1 18 def unparse(self) -> str: -1 19 ... 16 20 -1 21 @abc.abstractmethod -1 22 def shift_refs(self, d: tuple[int, int]) -> 'Expr': -1 23 ... 17 2418 -1 def parse_re(text, pattern):-1 25 -1 26 def parse_re(text: str, pattern: str) -> tuple[re.Match, str]: 19 27 m = re.match(pattern, text) 20 28 if not m: 21 29 raise ParseError 22 30 return m, text[m.end():] 23 31 24 3225 -1 def parse_string(text):26 -1 m, tail = parse_re(text, r'"[^"]*"')27 -1 return ('str', m[0][1:-1], m[0]), tail28 -129 -130 -1 def parse_float(text):31 -1 m, tail = parse_re(text, r'[0-9]+\.[0-9]+')32 -1 return ('float', float(m[0]), m[0]), tail33 -134 -135 -1 def parse_int(text):36 -1 m, tail = parse_re(text, r'[0-9]+')37 -1 return ('int', int(m[0], 10), m[0]), tail-1 33 def parse_any(text: str, parsers: list) -> tuple[Expr, str]: -1 34 for parser in parsers: -1 35 try: -1 36 return parser(text) -1 37 except ParseError: -1 38 pass -1 39 raise ParseError(f'None of the subparsers matched for {text}') 38 40 39 4140 -1 def col2x(col):-1 42 def col2x(col: str) -> int: 41 43 alph = string.ascii_uppercase 42 44 x = -1 43 45 for c in col: @@ -45,7 +47,7 @@ def col2x(col): 45 47 return x 46 48 47 4948 -1 def x2col(x):-1 50 def x2col(x: int) -> str: 49 51 if x < 0: 50 52 return '#' 51 53 a, b = divmod(x, len(string.ascii_uppercase)) @@ -56,133 +58,218 @@ def x2col(x): 56 58 return s 57 59 58 6059 -1 def parse_ref(text):60 -1 m, tail = parse_re(text, r'(\$)?([A-Z]+)(\$)?([1-9][0-9]*)')61 -1 x_fixed = bool(m[1])62 -1 x = col2x(m[2])63 -1 y_fixed = bool(m[3])64 -1 y = int(m[4], 10) - 165 -1 return ('ref', (x, y), (x_fixed, y_fixed)), tail-1 61 @dataclass(frozen=True) -1 62 class String(Expr): -1 63 value: str -1 64 raw: str 66 65 -1 66 @classmethod -1 67 def parse(cls, text: str) -> tuple['String', str]: -1 68 m, tail = parse_re(text, r'"[^"]*"') -1 69 return cls(m[0][1:-1], m[0]), tail 67 7068 -1 def parse_range(text):69 -1 ref1, tail = parse_ref(text)70 -1 _, tail = parse_re(tail, r':')71 -1 ref2, tail = parse_ref(tail)72 -1 return ('range', ref1, ref2), tail-1 71 def unparse(self) -> str: -1 72 return self.raw 73 73 -1 74 def shift_refs(self, d: tuple[int, int]) -> 'String': -1 75 return self 74 7675 -1 def parse_brace(text):76 -1 _, tail = parse_re(text, r'\(')77 -1 exp, tail = parse_expression(tail)78 -1 _, tail = parse_re(tail, r'\)')79 -1 return ('brace', exp), tail80 77 -1 78 @dataclass(frozen=True) -1 79 class Float(Expr): -1 80 value: float -1 81 raw: str 81 8282 -1 def parse_call(text):83 -1 m, tail = parse_re(text, r'[a-zA-Z][a-zA-Z0-9]*')84 -1 _, tail = parse_re(tail, r'\(')85 -1 args = []86 -1 commas = []87 -1 if tail.startswith(')'):88 -1 return (m[0], args, commas), tail[1:]89 -1 while True:90 -1 arg, tail = parse_expression(tail)91 -1 args.append(arg)92 -1 if tail.startswith(')'):93 -1 return (m[0], args, commas), tail[1:]94 -1 c, tail = parse_re(tail, r',\s*')95 -1 commas.append(c[0])96 -1 raise ParseError('no closing brace on function call')97 -198 -199 -1 def parse_expression3(text):100 -1 return parse_any(text, [101 -1 parse_string,102 -1 parse_float,103 -1 parse_int,104 -1 parse_range,105 -1 parse_ref,106 -1 parse_call,107 -1 parse_brace,108 -1 ])109 -1110 -1111 -1 def parse_expression2(text):112 -1 lhs, tail = parse_expression3(text)113 -1 while True:114 -1 try:115 -1 m, tail = parse_re(tail, r'\s*[*/]\s*')116 -1 except ParseError:117 -1 break118 -1 rhs, tail = parse_expression3(tail)119 -1 lhs = m[0].strip(), lhs, rhs, m[0]120 -1 return lhs, tail-1 83 @classmethod -1 84 def parse(cls, text: str) -> tuple['Float', str]: -1 85 m, tail = parse_re(text, r'[0-9]+\.[0-9]+') -1 86 return cls(float(m[0]), m[0]), tail 121 87 -1 88 def unparse(self) -> str: -1 89 return self.raw 122 90123 -1 def parse_expression(text):124 -1 lhs, tail = parse_expression2(text)125 -1 while True:126 -1 try:127 -1 m, tail = parse_re(tail, r'\s*[-+]\s*')128 -1 except ParseError:129 -1 break130 -1 rhs, tail = parse_expression2(tail)131 -1 lhs = m[0].strip(), lhs, rhs, m[0]132 -1 return lhs, tail-1 91 def shift_refs(self, d: tuple[int, int]) -> 'Float': -1 92 return self 133 93 134 94135 -1 def parse(text):136 -1 expr, tail = parse_expression(text)137 -1 if tail:138 -1 raise ParseError(f'unexpected tail: {tail}')139 -1 return expr-1 95 @dataclass(frozen=True) -1 96 class Int(Expr): -1 97 value: int -1 98 raw: str 140 99 -1 100 @classmethod -1 101 def parse(cls, text: str) -> tuple['Int', str]: -1 102 m, tail = parse_re(text, r'[0-9]+') -1 103 return cls(int(m[0], 10), m[0]), tail 141 104142 -1 def unparse(expr):143 -1 if expr[0] in ['str', 'float', 'int']:144 -1 return expr[2]145 -1 elif expr[0] == 'ref':-1 105 def unparse(self) -> str: -1 106 return self.raw -1 107 -1 108 def shift_refs(self, d: tuple[int, int]) -> 'Int': -1 109 return self -1 110 -1 111 -1 112 @dataclass(frozen=True) -1 113 class Ref(Expr): -1 114 cell: tuple[int, int] -1 115 fixed: tuple[bool, bool] -1 116 -1 117 @classmethod -1 118 def parse(cls, text: str) -> tuple['Ref', str]: -1 119 m, tail = parse_re(text, r'(\$)?([A-Z]+)(\$)?([1-9][0-9]*)') -1 120 x_fixed = bool(m[1]) -1 121 x = col2x(m[2]) -1 122 y_fixed = bool(m[3]) -1 123 y = int(m[4], 10) - 1 -1 124 return cls((x, y), (x_fixed, y_fixed)), tail -1 125 -1 126 def unparse(self) -> str: 146 127 s = ''147 -1 if expr[2][0]:148 -1 s += '$'149 -1 s += x2col(expr[1][0])150 -1 if expr[2][1]:151 -1 s += '$'152 -1 s += str(expr[1][1] + 1)-1 128 s += '$' if self.fixed[0] else '' -1 129 s += x2col(self.cell[0]) -1 130 s += '$' if self.fixed[1] else '' -1 131 s += str(self.cell[1] + 1) 153 132 return s154 -1 elif expr[0] == 'range':155 -1 return unparse(expr[1]) + ':' + unparse(expr[2])156 -1 elif expr[0] == 'brace':157 -1 return '(' + unparse(expr[1]) + ')'158 -1 elif expr[0] in '+-*/':159 -1 return unparse(expr[1]) + expr[3] + unparse(expr[2])160 -1 else:161 -1 name, args, commas = expr162 -1 assert len(args) == len(commas) + 1163 -1 sargs = ''164 -1 if args:165 -1 for i, comma in enumerate(commas):166 -1 sargs += unparse(args[i]) + comma167 -1 sargs += unparse(args[-1])168 -1 return f'{name}({sargs})'169 -1170 133171 -1 def shift_refs(expr, d):172 -1 if expr[0] == 'ref':173 -1 x, y = expr[1]174 -1 if not expr[2][0]:-1 134 def shift_refs(self, d: tuple[int, int]) -> 'Ref': -1 135 x, y = self.cell -1 136 if not self.fixed[0]: 175 137 x += d[0]176 -1 if not expr[2][1]:-1 138 if not self.fixed[1]: 177 139 y += d[1]178 -1 return 'ref', (x, y), expr[2]179 -1 elif expr[0] in ['str', 'float', 'int']:180 -1 return expr181 -1 elif expr[0] == 'range':182 -1 return 'range', shift_refs(expr[1], d), shift_refs(expr[2], d)183 -1 elif expr[0] == 'brace':184 -1 return 'brace', shift_refs(expr[1], d)185 -1 elif expr[0] in '+-*/':186 -1 return expr[0], shift_refs(expr[1], d), shift_refs(expr[2], d), expr[3]187 -1 else:188 -1 return expr[0], [shift_refs(arg, d) for arg in expr[1]], expr[2]-1 140 return Ref((x, y), self.fixed) -1 141 -1 142 -1 143 @dataclass(frozen=True) -1 144 class Range(Expr): -1 145 start: Ref -1 146 end: Ref -1 147 -1 148 @classmethod -1 149 def parse(cls, text: str) -> tuple['Range', str]: -1 150 ref1, tail = Ref.parse(text) -1 151 _, tail = parse_re(tail, r':') -1 152 ref2, tail = Ref.parse(tail) -1 153 return cls(ref1, ref2), tail -1 154 -1 155 def unparse(self) -> str: -1 156 return f'{self.start.unparse()}:{self.end.unparse()}' -1 157 -1 158 def shift_refs(self, d: tuple[int, int]) -> 'Range': -1 159 return Range(self.start.shift_refs(d), self.end.shift_refs(d)) -1 160 -1 161 -1 162 @dataclass(frozen=True) -1 163 class Brace(Expr): -1 164 inner: Expr -1 165 -1 166 @classmethod -1 167 def parse(cls, text: str) -> tuple['Brace', str]: -1 168 _, tail = parse_re(text, r'\(') -1 169 exp, tail = Op.parse(tail) -1 170 _, tail = parse_re(tail, r'\)') -1 171 return cls(exp), tail -1 172 -1 173 def unparse(self) -> str: -1 174 return f'({self.inner.unparse()})' -1 175 -1 176 def shift_refs(self, d: tuple[int, int]) -> 'Brace': -1 177 return Brace(self.inner.shift_refs(d)) -1 178 -1 179 -1 180 @dataclass(frozen=True) -1 181 class Call(Expr): -1 182 name: str -1 183 args: list[Expr] -1 184 commas: list[str] -1 185 -1 186 @classmethod -1 187 def parse(cls, text: str) -> tuple['Call', str]: -1 188 m, tail = parse_re(text, r'[a-zA-Z][a-zA-Z0-9]*') -1 189 _, tail = parse_re(tail, r'\(') -1 190 args = [] -1 191 commas = [] -1 192 if tail.startswith(')'): -1 193 return cls(m[0], args, commas), tail[1:] -1 194 while True: -1 195 arg, tail = Op.parse(tail) -1 196 args.append(arg) -1 197 if tail.startswith(')'): -1 198 return cls(m[0], args, commas), tail[1:] -1 199 c, tail = parse_re(tail, r',\s*') -1 200 commas.append(c[0]) -1 201 raise ParseError('no closing brace on function call') -1 202 -1 203 def unparse(self) -> str: -1 204 assert len(self.args) == len(self.commas) + 1 -1 205 sargs = '' -1 206 if self.args: -1 207 for i, comma in enumerate(self.commas): -1 208 sargs += str(self.args[i].unparse()) + comma -1 209 sargs += str(self.args[-1].unparse()) -1 210 return f'{self.name}({sargs})' -1 211 -1 212 def shift_refs(self, d: tuple[int, int]) -> 'Call': -1 213 return Call( -1 214 self.name, -1 215 [arg.shift_refs(d) for arg in self.args], -1 216 self.commas, -1 217 ) -1 218 -1 219 -1 220 @dataclass(frozen=True) -1 221 class Op(Expr): -1 222 op: str -1 223 lhs: Expr -1 224 rhs: Expr -1 225 op_raw: str -1 226 -1 227 @classmethod -1 228 def _parse(cls, text: str, pattern: str, parse_child) -> tuple['Op', str]: -1 229 lhs, tail = parse_child(text) -1 230 while True: -1 231 try: -1 232 m, tail = parse_re(tail, pattern) -1 233 except ParseError: -1 234 break -1 235 rhs, tail = parse_child(tail) -1 236 lhs = cls(m[0].strip(), lhs, rhs, m[0]) -1 237 return lhs, tail -1 238 -1 239 @classmethod -1 240 def parse_basic(cls, text: str) -> tuple[Expr, str]: -1 241 return parse_any(text, [ -1 242 String.parse, -1 243 Float.parse, -1 244 Int.parse, -1 245 Range.parse, -1 246 Ref.parse, -1 247 Call.parse, -1 248 Brace.parse, -1 249 ]) -1 250 -1 251 @classmethod -1 252 def parse_mult(cls, text: str) -> tuple['Op', str]: -1 253 return cls._parse(text, r'\s*[*/]\s*', cls.parse_basic) -1 254 -1 255 @classmethod -1 256 def parse(cls, text: str) -> tuple['Op', str]: -1 257 return cls._parse(text, r'\s*[-+]\s*', cls.parse_mult) -1 258 -1 259 def unparse(self) -> str: -1 260 return f'{self.lhs.unparse()}{self.op_raw}{self.rhs.unparse()}' -1 261 -1 262 def shift_refs(self, d: tuple[int, int]) -> 'Op': -1 263 return Op( -1 264 self.op, -1 265 self.lhs.shift_refs(d), -1 266 self.rhs.shift_refs(d), -1 267 self.op_raw, -1 268 ) -1 269 -1 270 -1 271 def parse(text: str) -> Expr: -1 272 expr, tail = Op.parse(text) -1 273 if tail: -1 274 raise ParseError(f'unexpected tail: {tail}') -1 275 return expr
diff --git a/sheet/sheet.py b/sheet/sheet.py
@@ -1,9 +1,8 @@ 1 1 import math -1 2 from collections.abc import Callable -1 3 from collections.abc import Generator 2 43 -1 from .expression import ParseError4 -1 from .expression import parse5 -1 from .expression import shift_refs6 -1 from .expression import unparse-1 5 from . import expression 7 6 8 7 BLOCKS = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'] 9 8 @@ -22,7 +21,13 @@ class Bar: 22 21 return a * BLOCKS[-1] + BLOCKS[b] + (width - a - 1) * BLOCKS[0] 23 22 24 2325 -1 FUNCTIONS = {-1 24 Cell = tuple[int, int] -1 25 RawValue = float | int | str -1 26 Parsed = RawValue | expression.Expr | expression.ParseError -1 27 Value = RawValue | Bar | None | Exception -1 28 -1 29 -1 30 FUNCTIONS: dict[str, tuple[Callable, str | int]] = { 26 31 'sum': (sum, 'range'), 27 32 'min': (min, 'range'), 28 33 'max': (max, 'range'), @@ -36,7 +41,7 @@ FUNCTIONS = { 36 41 } 37 42 38 4339 -1 def iter_range(cell1, cell2):-1 44 def iter_range(cell1: Cell, cell2: Cell) -> Generator[Cell, None, None]: 40 45 x1, y1 = cell1 41 46 x2, y2 = cell2 42 47 if x1 > x2: @@ -48,7 +53,7 @@ def iter_range(cell1, cell2): 48 53 yield x, y 49 54 50 5551 -1 def to_number(value: float|int|str|Bar|None|Exception) -> float|int:-1 56 def to_number(value: Value) -> float | int: 52 57 if isinstance(value, float): 53 58 return value 54 59 elif isinstance(value, int): @@ -65,16 +70,16 @@ def to_number(value: float|int|str|Bar|None|Exception) -> float|int: 65 70 66 71 class Sheet: 67 72 def __init__(self):68 -1 self.raw = {}69 -1 self.parsed = {}70 -1 self.cache = {}-1 73 self.raw: dict[Cell, str] = {} -1 74 self.parsed: dict[Cell, Parsed] = {} -1 75 self.cache: dict[Cell, Value] = {} 71 7672 -1 def parse(self, raw: str) -> tuple|float|int|str:-1 77 def parse(self, raw: str) -> Parsed: 73 78 if raw.startswith('='): 74 79 try:75 -1 return parse(raw[1:])76 -1 except ParseError as err:77 -1 return ('err', err)-1 80 return expression.parse(raw[1:]) -1 81 except expression.ParseError as err: -1 82 return err 78 83 try: 79 84 return int(raw, 10) 80 85 except ValueError: @@ -85,52 +90,43 @@ class Sheet: 85 90 pass 86 91 return raw 87 9288 -1 def call_function(89 -1 self, name: str, args: list[tuple], _commas: list[str]90 -1 ) -> float|int|str|Bar:-1 93 def call_function(self, name: str, args: list[expression.Expr]) -> Value: 91 94 fn, nargs = FUNCTIONS[name.lower()] 92 95 if nargs == 'range':93 -1 if len(args) != 1 or args[0][0] != 'range':-1 96 if len(args) != 1 or not isinstance(args[0], expression.Range): 94 97 raise ValueError(args)95 -1 _, ref1, ref2 = args[0]96 98 return fn( 97 99 to_number(self.get_value(ref))98 -1 for ref in iter_range(ref1[1], ref2[1])-1 100 for ref in iter_range(args[0].start.cell, args[0].end.cell) 99 101 ) 100 102 else: 101 103 if len(args) != nargs: 102 104 raise ValueError(args) 103 105 return fn(*[to_number(self.evaluate(a)) for a in args]) 104 106105 -1 def evaluate(self, expr: tuple) -> float|int|str|Bar:106 -1 if expr[0] in ['int', 'float', 'str']:107 -1 return expr[1]108 -1 elif expr[0] == 'ref':109 -1 return self.get_value(expr[1])110 -1 elif expr[0] == 'brace':111 -1 return self.evaluate(expr[1])112 -1 elif expr[0] == 'err':113 -1 raise expr[1]114 -1 elif expr[0] == '+':115 -1 lhs = to_number(self.evaluate(expr[1]))116 -1 rhs = to_number(self.evaluate(expr[2]))117 -1 return lhs + rhs118 -1 elif expr[0] == '-':119 -1 lhs = to_number(self.evaluate(expr[1]))120 -1 rhs = to_number(self.evaluate(expr[2]))121 -1 return lhs - rhs122 -1 elif expr[0] == '*':123 -1 lhs = to_number(self.evaluate(expr[1]))124 -1 rhs = to_number(self.evaluate(expr[2]))125 -1 return lhs * rhs126 -1 elif expr[0] == '/':127 -1 lhs = to_number(self.evaluate(expr[1]))128 -1 rhs = to_number(self.evaluate(expr[2]))129 -1 return lhs / rhs130 -1 else:131 -1 return self.call_function(*expr)132 -1133 -1 def set(self, cell, raw: str):-1 107 def evaluate(self, expr: expression.Expr) -> Value: -1 108 if isinstance(expr, (expression.Int, expression.Float, expression.String)): -1 109 return expr.value -1 110 elif isinstance(expr, expression.Ref): -1 111 return self.get_value(expr.cell) -1 112 elif isinstance(expr, expression.Brace): -1 113 return self.evaluate(expr.inner) -1 114 elif isinstance(expr, expression.Op): -1 115 lhs = to_number(self.evaluate(expr.lhs)) -1 116 rhs = to_number(self.evaluate(expr.rhs)) -1 117 if expr.op == '+': -1 118 return lhs + rhs -1 119 elif expr.op == '-': -1 120 return lhs - rhs -1 121 elif expr.op == '*': -1 122 return lhs * rhs -1 123 elif expr.op == '/': -1 124 return lhs / rhs -1 125 elif isinstance(expr, expression.Call): -1 126 return self.call_function(expr.name, expr.args) -1 127 raise ValueError(expr) -1 128 -1 129 def set(self, cell: Cell, raw: str): 134 130 if raw: 135 131 self.raw[cell] = raw 136 132 self.parsed[cell] = self.parse(raw) @@ -139,22 +135,23 @@ class Sheet: 139 135 del self.parsed[cell] 140 136 self.cache = {} 141 137142 -1 def set_shifted(self, cell, raw: str, shift) -> str:-1 138 def set_shifted(self, cell: Cell, raw: str, shift: Cell): 143 139 if raw.startswith('='): 144 140 expr = self.parse(raw)145 -1 shifted = shift_refs(expr, shift)146 -1 raw = '=' + unparse(shifted)-1 141 if isinstance(expr, expression.Expr): -1 142 shifted = expr.shift_refs(shift) -1 143 raw = f'={shifted.unparse()}' 147 144 self.set(cell, raw) 148 145149 -1 def get_raw(self, cell) -> str:-1 146 def get_raw(self, cell: Cell) -> str: 150 147 return self.raw.get(cell, '') 151 148152 -1 def get_parsed(self, cell) -> tuple|float|int|str|None:-1 149 def get_parsed(self, cell: Cell) -> Parsed | None: 153 150 return self.parsed.get(cell) 154 151155 -1 def get_value(self, cell) -> float|int|str|Bar|None|Exception:-1 152 def get_value(self, cell: Cell) -> Value: 156 153 parsed = self.get_parsed(cell)157 -1 if isinstance(parsed, tuple):-1 154 if isinstance(parsed, expression.Expr): 158 155 if cell not in self.cache: 159 156 self.cache[cell] = ReferenceError(cell) 160 157 try: