spreadsheet

terminal spreadsheet application
git clone https://git.ce9e.org/spreadsheet.git

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))