- commit
- 01699e8d6b70938aae9b74f878605036e704d44b
- parent
- 837533d41f076a2e1436f4f225996b0b839619c5
- Author
- Tobias Bengfort <tobias.bengfort@posteo.de>
- Date
- 2024-07-05 16:55
init
Diffstat
| A | foo.py | 358 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
1 files changed, 358 insertions, 0 deletions
diff --git a/foo.py b/foo.py
@@ -0,0 +1,358 @@
-1 1 import csv
-1 2 import re
-1 3 import string
-1 4 import sys
-1 5
-1 6
-1 7 class ParseError(ValueError):
-1 8 pass
-1 9
-1 10
-1 11 class ReferenceError(ValueError):
-1 12 pass
-1 13
-1 14
-1 15 class ExpressionParser:
-1 16 def parse_any(self, text, parsers):
-1 17 for parser in parsers:
-1 18 try:
-1 19 return parser(text)
-1 20 except ParseError:
-1 21 pass
-1 22 raise ParseError(f'None of the subparsers matched for {text}')
-1 23
-1 24 def parse_re(self, text, pattern):
-1 25 m = re.match(pattern, text)
-1 26 if not m:
-1 27 raise ParseError
-1 28 return m, text[m.end():]
-1 29
-1 30 def parse_string(self, text):
-1 31 m, tail = self.parse_re(text, r'"[^"]*"')
-1 32 return ('str', m[0][1:-1]), tail
-1 33
-1 34 def parse_float(self, text):
-1 35 m, tail = self.parse_re(text, r'[0-9]+\.[0-9]+')
-1 36 return ('float', float(m[0])), tail
-1 37
-1 38 def parse_int(self, text):
-1 39 m, tail = self.parse_re(text, r'[0-9]+')
-1 40 return ('int', int(m[0], 10)), tail
-1 41
-1 42 def parse_ref(self, text):
-1 43 m, tail = self.parse_re(text, r'\$?([A-Z]+)\$?([1-9][0-9]*)')
-1 44 return ('ref', m[1] + m[2], m[0]), tail
-1 45
-1 46 def parse_range(self, text):
-1 47 ref1, tail = self.parse_ref(text)
-1 48 _, tail = self.parse_re(tail, r':')
-1 49 ref2, tail = self.parse_ref(tail)
-1 50 return ('range', ref1, ref2), tail
-1 51
-1 52 def parse_brace(self, text):
-1 53 _, tail = self.parse_re(text, r'\(')
-1 54 exp, tail = self.parse_expression(tail)
-1 55 _, tail = self.parse_re(tail, r'\)')
-1 56 return exp, tail
-1 57
-1 58 def parse_add(self, text):
-1 59 lhs, tail = self.parse_expression2(text)
-1 60 m, tail = self.parse_re(tail, r'\s*[-+]\s*')
-1 61 rhs, tail = self.parse_expression(tail)
-1 62 return (m[0].strip(), lhs, rhs), tail
-1 63
-1 64 def parse_mul(self, text):
-1 65 lhs, tail = self.parse_expression3(text)
-1 66 m, tail = self.parse_re(tail, r'\s*[*/]\s*')
-1 67 rhs, tail = self.parse_expression2(tail)
-1 68 return (m[0].strip(), lhs, rhs), tail
-1 69
-1 70 def parse_call(self, text):
-1 71 m, tail = self.parse_re(text, r'[a-zA-Z][a-zA-Z0-9]*')
-1 72 _, tail = self.parse_re(tail, r'\(')
-1 73 args = []
-1 74 if tail.startswith(')'):
-1 75 return (m[0], args), tail[1:]
-1 76 while True:
-1 77 arg, tail = self.parse_expression(tail)
-1 78 args.append(arg)
-1 79 if tail.startswith(')'):
-1 80 return (m[0], args), tail[1:]
-1 81 _, tail = self.parse_re(tail, r',\s*')
-1 82 raise ParseError
-1 83
-1 84 def parse_expression3(self, text):
-1 85 return self.parse_any(text, [
-1 86 self.parse_string,
-1 87 self.parse_float,
-1 88 self.parse_int,
-1 89 self.parse_range,
-1 90 self.parse_ref,
-1 91 self.parse_call,
-1 92 self.parse_brace,
-1 93 ])
-1 94
-1 95 def parse_expression2(self, text):
-1 96 return self.parse_any(text, [
-1 97 self.parse_mul,
-1 98 self.parse_expression3,
-1 99 ])
-1 100
-1 101 def parse_expression(self, text):
-1 102 return self.parse_any(text, [
-1 103 self.parse_add,
-1 104 self.parse_expression2,
-1 105 ])
-1 106
-1 107 def parse(self, text):
-1 108 expr, tail = self.parse_expression(text)
-1 109 if tail:
-1 110 raise ParseError(f'unexpected tail: {tail}')
-1 111 return expr
-1 112
-1 113
-1 114 def x2col(x):
-1 115 a, b = divmod(x, len(string.ascii_uppercase))
-1 116 s = string.ascii_uppercase[b]
-1 117 while a:
-1 118 a, b = divmod(a - 1, len(string.ascii_uppercase))
-1 119 s = string.ascii_uppercase[b] + s
-1 120 return s
-1 121
-1 122
-1 123 def xy2ref(x, y):
-1 124 return x2col(x) + str(y + 1)
-1 125
-1 126
-1 127 def col2x(col):
-1 128 alph = string.ascii_uppercase
-1 129 x = -1
-1 130 for c in col:
-1 131 x = (x + 1) * len(alph) + alph.index(c)
-1 132 return x
-1 133
-1 134
-1 135 def ref2xy(ref):
-1 136 m = re.match('([A-Z]*)([0-9]*)', ref)
-1 137 return col2x(m[1]), int(m[2], 10) - 1
-1 138
-1 139
-1 140 def iter_range(cell1, cell2):
-1 141 x1, y1 = ref2xy(cell1)
-1 142 x2, y2 = ref2xy(cell2)
-1 143 if x1 > x2:
-1 144 x1, x2 = x2, x1
-1 145 if y1 > y2:
-1 146 y1, y2 = y2, y1
-1 147 for y in range(y1, y2 + 1):
-1 148 for x in range(x1, x2 + 1):
-1 149 yield xy2ref(x, y)
-1 150
-1 151
-1 152 class Sheet:
-1 153 def __init__(self):
-1 154 self.expression_parser = ExpressionParser()
-1 155 self.reset()
-1 156
-1 157 def reset(self):
-1 158 self.raw = {}
-1 159 self.parsed = {}
-1 160 self.cache = {}
-1 161 self.width = 0
-1 162 self.height = 0
-1 163
-1 164 def parse(self, raw: str) -> tuple|float|int|str:
-1 165 if raw.startswith('='):
-1 166 try:
-1 167 return self.expression_parser.parse(raw[1:])
-1 168 except ParseError as err:
-1 169 return ('err', err)
-1 170 try:
-1 171 return int(raw, 10)
-1 172 except ValueError:
-1 173 pass
-1 174 try:
-1 175 return float(raw)
-1 176 except ValueError:
-1 177 pass
-1 178 return raw
-1 179
-1 180 def call_function(self, name: str, args: list[tuple]) -> float|int|str:
-1 181 if name == 'sum':
-1 182 if len(args) != 1 or args[0][0] != 'range':
-1 183 raise ValueError(args)
-1 184 _, ref1, ref2 = args[0]
-1 185 return sum(
-1 186 self.to_number(self.get_value(ref))
-1 187 for ref in iter_range(ref1[1], ref2[1])
-1 188 )
-1 189 elif name == 'power':
-1 190 if len(args) != 2:
-1 191 raise ValueError(args)
-1 192 base = self.to_number(self.evaluate(args[0]))
-1 193 exp = self.to_number(self.evaluate(args[1]))
-1 194 return base ** exp
-1 195 else:
-1 196 raise NameError(name)
-1 197
-1 198 def evaluate(self, expr: tuple) -> float|int|str:
-1 199 if expr[0] in ['int', 'float', 'str']:
-1 200 return expr[1]
-1 201 elif expr[0] == 'ref':
-1 202 return self.get_value(expr[1])
-1 203 elif expr[0] == 'err':
-1 204 raise expr[1]
-1 205 elif expr[0] == '+':
-1 206 return self.evaluate(expr[1]) + self.evaluate(expr[2])
-1 207 elif expr[0] == '-':
-1 208 return self.evaluate(expr[1]) - self.evaluate(expr[2])
-1 209 elif expr[0] == '*':
-1 210 return self.evaluate(expr[1]) * self.evaluate(expr[2])
-1 211 elif expr[0] == '/':
-1 212 return self.evaluate(expr[1]) / self.evaluate(expr[2])
-1 213 else:
-1 214 return self.call_function(*expr)
-1 215
-1 216 def set(self, cell: str, raw: str):
-1 217 if raw:
-1 218 self.raw[cell] = raw
-1 219 self.parsed[cell] = self.parse(raw)
-1 220 x, y = ref2xy(cell)
-1 221 self.width = max(self.width, x + 1)
-1 222 self.height = max(self.height, y + 1)
-1 223 elif cell in self.raw:
-1 224 del self.raw[cell]
-1 225 del self.parsed[cell]
-1 226 self.width = max(ref2xy(cell)[0] for cell in self.raw) + 1
-1 227 self.height = max(ref2xy(cell)[1] for cell in self.raw) + 1
-1 228 self.cache = {}
-1 229
-1 230 def get_raw(self, cell: str) -> str:
-1 231 return self.raw.get(cell, '')
-1 232
-1 233 def get_parsed(self, cell: str) -> tuple|float|int|str|None:
-1 234 return self.parsed.get(cell)
-1 235
-1 236 def get_value(self, cell: str) -> float|int|str|None|Exception:
-1 237 parsed = self.get_parsed(cell)
-1 238 if isinstance(parsed, tuple):
-1 239 if cell not in self.cache:
-1 240 self.cache[cell] = ReferenceError(cell)
-1 241 try:
-1 242 self.cache[cell] = self.evaluate(parsed)
-1 243 except Exception as err:
-1 244 self.cache[cell] = err
-1 245 return self.cache[cell]
-1 246 else:
-1 247 return parsed
-1 248
-1 249 def to_number(self, value: float|int|str|None|Exception) -> float|int:
-1 250 if isinstance(value, float):
-1 251 return value
-1 252 elif isinstance(value, int):
-1 253 return value
-1 254 elif isinstance(value, str):
-1 255 raise TypeError(value)
-1 256 elif value is None:
-1 257 return 0
-1 258 elif isinstance(value, Exception):
-1 259 raise value
-1 260
-1 261 def to_display(self, value: float|int|str|None|Exception) -> str:
-1 262 if isinstance(value, float):
-1 263 return str(value)
-1 264 elif isinstance(value, int):
-1 265 return str(value)
-1 266 elif isinstance(value, str):
-1 267 return value
-1 268 elif value is None:
-1 269 return ''
-1 270 elif isinstance(value, Exception):
-1 271 return repr(value)
-1 272
-1 273 def render(self, value: float|int|str|None|Exception, width: int) -> str:
-1 274 if isinstance(value, float):
-1 275 return align_right(str(value), width)
-1 276 elif isinstance(value, int):
-1 277 return align_right(str(value), width)
-1 278 elif isinstance(value, str):
-1 279 return align_left(value, width)
-1 280 elif value is None:
-1 281 return ' ' * width
-1 282 elif isinstance(value, Exception):
-1 283 return red(align_left(repr(value), width))
-1 284
-1 285 def load_csv(self, fh, **kwargs):
-1 286 self.raw = {}
-1 287 self.parsed = {}
-1 288 self.cache = {}
-1 289 for y, row in enumerate(csv.reader(fh, **kwargs)):
-1 290 for x, raw in enumerate(row):
-1 291 ref = xy2ref(x, y)
-1 292 self.set(ref, raw)
-1 293
-1 294 def write_csv(self, fh, *, display=False, **kwargs):
-1 295 if display:
-1 296 def get(cell):
-1 297 return self.to_display(self.get_value(cell))
-1 298 else:
-1 299 get = self.get_raw
-1 300
-1 301 w = csv.writer(fh, **kwargs)
-1 302 for y in range(self.height):
-1 303 w.writerow([get(xy2ref(x, y)) for x in range(self.width)])
-1 304
-1 305
-1 306 def align_right(s, width):
-1 307 if len(s) > width:
-1 308 s = '###'
-1 309 return ' ' + ' ' * (width - len(s) - 1) + s
-1 310
-1 311
-1 312 def align_left(s, width):
-1 313 if len(s) > width:
-1 314 s = '###'
-1 315 return s + ' ' * (width - len(s))
-1 316
-1 317
-1 318 def align_center(s, width):
-1 319 if len(s) > width:
-1 320 s = '###'
-1 321 t = width - len(s)
-1 322 return ' ' * (t // 2) + s + ' ' * (t - t // 2)
-1 323
-1 324
-1 325 def red(s):
-1 326 return f'\033[31m{s}\033[0m'
-1 327
-1 328
-1 329 def invert(s):
-1 330 return f'\033[7m{s}\033[0m'
-1 331
-1 332
-1 333 def render(sheet, width, height, cell_offset, cell_width):
-1 334 x0, y0 = ref2xy(cell_offset)
-1 335 rows = []
-1 336 w = width // cell_width
-1 337 rows.append([
-1 338 ' ' * cell_width,
-1 339 ] + [
-1 340 align_center(x2col(x0 + dx), cell_width)
-1 341 for dx in range(w - 1)
-1 342 ])
-1 343 for dy in range(height - 1):
-1 344 rows.append([
-1 345 align_right(str(y0 + dy + 1), cell_width),
-1 346 ] + [
-1 347 sheet.render(sheet.get_value(xy2ref(x0 + dx, y0 + dy)), cell_width)
-1 348 for dx in range(w - 1)
-1 349 ])
-1 350 return '\n'.join([''.join(row) for row in rows])
-1 351
-1 352
-1 353 if __name__ == '__main__':
-1 354 s = Sheet()
-1 355 with open(sys.argv[1]) as fh:
-1 356 s.load_csv(fh)
-1 357
-1 358 print(render(s, 80, 40, 'A1', 10))