spreadsheet

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

commit
f4a35784689dd2e81e71773df59e47b7d73fdbb3
parent
01699e8d6b70938aae9b74f878605036e704d44b
Author
Tobias Bengfort <tobias.bengfort@posteo.de>
Date
2024-07-05 17:21
split into separate files

Diffstat

D foo.py 358 ------------------------------------------------------------
A sheet/__init__.py 0
A sheet/__main__.py 10 ++++++++++
A sheet/csv.py 38 ++++++++++++++++++++++++++++++++++++++
A sheet/expression.py 117 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A sheet/sheet.py 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A sheet/term.py 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

7 files changed, 378 insertions, 358 deletions


diff --git a/foo.py b/foo.py

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

diff --git a/sheet/__init__.py b/sheet/__init__.py

diff --git a/sheet/__main__.py b/sheet/__main__.py

@@ -0,0 +1,10 @@
   -1     1 import sys
   -1     2 
   -1     3 from .csv import load_csv
   -1     4 from .term import render
   -1     5 
   -1     6 
   -1     7 with open(sys.argv[1]) as fh:
   -1     8     sheet = load_csv(fh)
   -1     9 
   -1    10 print(render(sheet, 80, 40, 'A1', 10))

diff --git a/sheet/csv.py b/sheet/csv.py

@@ -0,0 +1,38 @@
   -1     1 import csv
   -1     2 
   -1     3 from .sheet import Sheet
   -1     4 from .sheet import xy2ref
   -1     5 
   -1     6 
   -1     7 def to_display(value: float|int|str|None|Exception) -> str:
   -1     8     if isinstance(value, float):
   -1     9         return str(value)
   -1    10     elif isinstance(value, int):
   -1    11         return str(value)
   -1    12     elif isinstance(value, str):
   -1    13         return value
   -1    14     elif value is None:
   -1    15         return ''
   -1    16     elif isinstance(value, Exception):
   -1    17         return repr(value)
   -1    18 
   -1    19 
   -1    20 def load_csv(fh, **kwargs):
   -1    21     sheet = Sheet()
   -1    22     for y, row in enumerate(csv.reader(fh, **kwargs)):
   -1    23         for x, raw in enumerate(row):
   -1    24             ref = xy2ref(x, y)
   -1    25             sheet.set(ref, raw)
   -1    26     return sheet
   -1    27 
   -1    28 
   -1    29 def dump_csv(sheet, fh, *, display=False, **kwargs):
   -1    30     if display:
   -1    31         def get(cell):
   -1    32             return to_display(sheet.get_value(cell))
   -1    33     else:
   -1    34         get = sheet.get_raw
   -1    35 
   -1    36     w = csv.writer(fh, **kwargs)
   -1    37     for y in range(sheet.height):
   -1    38         w.writerow([get(xy2ref(x, y)) for x in range(sheet.width)])

diff --git a/sheet/expression.py b/sheet/expression.py

@@ -0,0 +1,117 @@
   -1     1 import re
   -1     2 
   -1     3 
   -1     4 class ParseError(ValueError):
   -1     5     pass
   -1     6 
   -1     7 
   -1     8 def parse_any(text, parsers):
   -1     9     for parser in parsers:
   -1    10         try:
   -1    11             return parser(text)
   -1    12         except ParseError:
   -1    13             pass
   -1    14     raise ParseError(f'None of the subparsers matched for {text}')
   -1    15 
   -1    16 
   -1    17 def parse_re(text, pattern):
   -1    18     m = re.match(pattern, text)
   -1    19     if not m:
   -1    20         raise ParseError
   -1    21     return m, text[m.end():]
   -1    22 
   -1    23 
   -1    24 def parse_string(text):
   -1    25     m, tail = parse_re(text, r'"[^"]*"')
   -1    26     return ('str', m[0][1:-1]), tail
   -1    27 
   -1    28 
   -1    29 def parse_float(text):
   -1    30     m, tail = parse_re(text, r'[0-9]+\.[0-9]+')
   -1    31     return ('float', float(m[0])), tail
   -1    32 
   -1    33 
   -1    34 def parse_int(text):
   -1    35     m, tail = parse_re(text, r'[0-9]+')
   -1    36     return ('int', int(m[0], 10)), tail
   -1    37 
   -1    38 
   -1    39 def parse_ref(text):
   -1    40     m, tail = parse_re(text, r'\$?([A-Z]+)\$?([1-9][0-9]*)')
   -1    41     return ('ref', m[1] + m[2], m[0]), tail
   -1    42 
   -1    43 
   -1    44 def parse_range(text):
   -1    45     ref1, tail = parse_ref(text)
   -1    46     _, tail = parse_re(tail, r':')
   -1    47     ref2, tail = parse_ref(tail)
   -1    48     return ('range', ref1, ref2), tail
   -1    49 
   -1    50 
   -1    51 def parse_brace(text):
   -1    52     _, tail = parse_re(text, r'\(')
   -1    53     exp, tail = parse_expression(tail)
   -1    54     _, tail = parse_re(tail, r'\)')
   -1    55     return exp, tail
   -1    56 
   -1    57 
   -1    58 def parse_add(text):
   -1    59     lhs, tail = parse_expression2(text)
   -1    60     m, tail = parse_re(tail, r'\s*[-+]\s*')
   -1    61     rhs, tail = parse_expression(tail)
   -1    62     return (m[0].strip(), lhs, rhs), tail
   -1    63 
   -1    64 
   -1    65 def parse_mul(text):
   -1    66     lhs, tail = parse_expression3(text)
   -1    67     m, tail = parse_re(tail, r'\s*[*/]\s*')
   -1    68     rhs, tail = parse_expression2(tail)
   -1    69     return (m[0].strip(), lhs, rhs), tail
   -1    70 
   -1    71 
   -1    72 def parse_call(text):
   -1    73     m, tail = parse_re(text, r'[a-zA-Z][a-zA-Z0-9]*')
   -1    74     _, tail = parse_re(tail, r'\(')
   -1    75     args = []
   -1    76     if tail.startswith(')'):
   -1    77         return (m[0], args), tail[1:]
   -1    78     while True:
   -1    79         arg, tail = parse_expression(tail)
   -1    80         args.append(arg)
   -1    81         if tail.startswith(')'):
   -1    82             return (m[0], args), tail[1:]
   -1    83         _, tail = parse_re(tail, r',\s*')
   -1    84     raise ParseError('no closing brace on function call')
   -1    85 
   -1    86 
   -1    87 def parse_expression3(text):
   -1    88     return parse_any(text, [
   -1    89         parse_string,
   -1    90         parse_float,
   -1    91         parse_int,
   -1    92         parse_range,
   -1    93         parse_ref,
   -1    94         parse_call,
   -1    95         parse_brace,
   -1    96     ])
   -1    97 
   -1    98 
   -1    99 def parse_expression2(text):
   -1   100     return parse_any(text, [
   -1   101         parse_mul,
   -1   102         parse_expression3,
   -1   103     ])
   -1   104 
   -1   105 
   -1   106 def parse_expression(text):
   -1   107     return parse_any(text, [
   -1   108         parse_add,
   -1   109         parse_expression2,
   -1   110     ])
   -1   111 
   -1   112 
   -1   113 def parse(text):
   -1   114     expr, tail = parse_expression(text)
   -1   115     if tail:
   -1   116         raise ParseError(f'unexpected tail: {tail}')
   -1   117     return expr

diff --git a/sheet/sheet.py b/sheet/sheet.py

@@ -0,0 +1,150 @@
   -1     1 import re
   -1     2 import string
   -1     3 
   -1     4 from .expression import ParseError
   -1     5 from .expression import parse
   -1     6 
   -1     7 
   -1     8 def x2col(x):
   -1     9     a, b = divmod(x, len(string.ascii_uppercase))
   -1    10     s = string.ascii_uppercase[b]
   -1    11     while a:
   -1    12         a, b = divmod(a - 1, len(string.ascii_uppercase))
   -1    13         s = string.ascii_uppercase[b] + s
   -1    14     return s
   -1    15 
   -1    16 
   -1    17 def xy2ref(x, y):
   -1    18     return x2col(x) + str(y + 1)
   -1    19 
   -1    20 
   -1    21 def col2x(col):
   -1    22     alph = string.ascii_uppercase
   -1    23     x = -1
   -1    24     for c in col:
   -1    25         x = (x + 1) * len(alph) + alph.index(c)
   -1    26     return x
   -1    27 
   -1    28 
   -1    29 def ref2xy(ref):
   -1    30     m = re.match('([A-Z]*)([0-9]*)', ref)
   -1    31     return col2x(m[1]), int(m[2], 10) - 1
   -1    32 
   -1    33 
   -1    34 def iter_range(cell1, cell2):
   -1    35     x1, y1 = ref2xy(cell1)
   -1    36     x2, y2 = ref2xy(cell2)
   -1    37     if x1 > x2:
   -1    38         x1, x2 = x2, x1
   -1    39     if y1 > y2:
   -1    40         y1, y2 = y2, y1
   -1    41     for y in range(y1, y2 + 1):
   -1    42         for x in range(x1, x2 + 1):
   -1    43             yield xy2ref(x, y)
   -1    44 
   -1    45 
   -1    46 def to_number(value: float|int|str|None|Exception) -> float|int:
   -1    47     if isinstance(value, float):
   -1    48         return value
   -1    49     elif isinstance(value, int):
   -1    50         return value
   -1    51     elif isinstance(value, str):
   -1    52         raise TypeError(value)
   -1    53     elif value is None:
   -1    54         return 0
   -1    55     elif isinstance(value, Exception):
   -1    56         raise value
   -1    57 
   -1    58 
   -1    59 class Sheet:
   -1    60     def __init__(self):
   -1    61         self.raw = {}
   -1    62         self.parsed = {}
   -1    63         self.cache = {}
   -1    64         self.width = 0
   -1    65         self.height = 0
   -1    66 
   -1    67     def parse(self, raw: str) -> tuple|float|int|str:
   -1    68         if raw.startswith('='):
   -1    69             try:
   -1    70                 return parse(raw[1:])
   -1    71             except ParseError as err:
   -1    72                 return ('err', err)
   -1    73         try:
   -1    74             return int(raw, 10)
   -1    75         except ValueError:
   -1    76             pass
   -1    77         try:
   -1    78             return float(raw)
   -1    79         except ValueError:
   -1    80             pass
   -1    81         return raw
   -1    82 
   -1    83     def call_function(self, name: str, args: list[tuple]) -> float|int|str:
   -1    84         if name == 'sum':
   -1    85             if len(args) != 1 or args[0][0] != 'range':
   -1    86                 raise ValueError(args)
   -1    87             _, ref1, ref2 = args[0]
   -1    88             return sum(
   -1    89                 to_number(self.get_value(ref))
   -1    90                 for ref in iter_range(ref1[1], ref2[1])
   -1    91             )
   -1    92         elif name == 'power':
   -1    93             if len(args) != 2:
   -1    94                 raise ValueError(args)
   -1    95             base = to_number(self.evaluate(args[0]))
   -1    96             exp = to_number(self.evaluate(args[1]))
   -1    97             return base ** exp
   -1    98         else:
   -1    99             raise NameError(name)
   -1   100 
   -1   101     def evaluate(self, expr: tuple) -> float|int|str:
   -1   102         if expr[0] in ['int', 'float', 'str']:
   -1   103             return expr[1]
   -1   104         elif expr[0] == 'ref':
   -1   105             return self.get_value(expr[1])
   -1   106         elif expr[0] == 'err':
   -1   107             raise expr[1]
   -1   108         elif expr[0] == '+':
   -1   109             return self.evaluate(expr[1]) + self.evaluate(expr[2])
   -1   110         elif expr[0] == '-':
   -1   111             return self.evaluate(expr[1]) - self.evaluate(expr[2])
   -1   112         elif expr[0] == '*':
   -1   113             return self.evaluate(expr[1]) * self.evaluate(expr[2])
   -1   114         elif expr[0] == '/':
   -1   115             return self.evaluate(expr[1]) / self.evaluate(expr[2])
   -1   116         else:
   -1   117             return self.call_function(*expr)
   -1   118 
   -1   119     def set(self, cell: str, raw: str):
   -1   120         if raw:
   -1   121             self.raw[cell] = raw
   -1   122             self.parsed[cell] = self.parse(raw)
   -1   123             x, y = ref2xy(cell)
   -1   124             self.width = max(self.width, x + 1)
   -1   125             self.height = max(self.height, y + 1)
   -1   126         elif cell in self.raw:
   -1   127             del self.raw[cell]
   -1   128             del self.parsed[cell]
   -1   129             self.width = max(ref2xy(cell)[0] for cell in self.raw) + 1
   -1   130             self.height = max(ref2xy(cell)[1] for cell in self.raw) + 1
   -1   131         self.cache = {}
   -1   132 
   -1   133     def get_raw(self, cell: str) -> str:
   -1   134         return self.raw.get(cell, '')
   -1   135 
   -1   136     def get_parsed(self, cell: str) -> tuple|float|int|str|None:
   -1   137         return self.parsed.get(cell)
   -1   138 
   -1   139     def get_value(self, cell: str) -> float|int|str|None|Exception:
   -1   140         parsed = self.get_parsed(cell)
   -1   141         if isinstance(parsed, tuple):
   -1   142             if cell not in self.cache:
   -1   143                 self.cache[cell] = ReferenceError(cell)
   -1   144                 try:
   -1   145                     self.cache[cell] = self.evaluate(parsed)
   -1   146                 except Exception as err:
   -1   147                     self.cache[cell] = err
   -1   148             return self.cache[cell]
   -1   149         else:
   -1   150             return parsed

diff --git a/sheet/term.py b/sheet/term.py

@@ -0,0 +1,63 @@
   -1     1 from .sheet import ref2xy
   -1     2 from .sheet import x2col
   -1     3 from .sheet import xy2ref
   -1     4 
   -1     5 
   -1     6 def align_right(s, width):
   -1     7     if len(s) > width:
   -1     8         s = '###'
   -1     9     return ' ' + ' ' * (width - len(s) - 1) + s
   -1    10 
   -1    11 
   -1    12 def align_left(s, width):
   -1    13     if len(s) > width:
   -1    14         s = '###'
   -1    15     return s + ' ' * (width - len(s))
   -1    16 
   -1    17 
   -1    18 def align_center(s, width):
   -1    19     if len(s) > width:
   -1    20         s = '###'
   -1    21     t = width - len(s)
   -1    22     return ' ' * (t // 2) + s + ' ' * (t - t // 2)
   -1    23 
   -1    24 
   -1    25 def red(s):
   -1    26     return f'\033[31m{s}\033[0m'
   -1    27 
   -1    28 
   -1    29 def invert(s):
   -1    30     return f'\033[7m{s}\033[0m'
   -1    31 
   -1    32 
   -1    33 def to_cell(value: float|int|str|None|Exception, width: int) -> str:
   -1    34     if isinstance(value, float):
   -1    35         return align_right(str(value), width)
   -1    36     elif isinstance(value, int):
   -1    37         return align_right(str(value), width)
   -1    38     elif isinstance(value, str):
   -1    39         return align_left(value, width)
   -1    40     elif value is None:
   -1    41         return ' ' * width
   -1    42     elif isinstance(value, Exception):
   -1    43         return red(align_left(repr(value), width))
   -1    44 
   -1    45 
   -1    46 def render(sheet, width, height, cell_offset, cell_width):
   -1    47     x0, y0 = ref2xy(cell_offset)
   -1    48     rows = []
   -1    49     w = width // cell_width
   -1    50     rows.append([
   -1    51         ' ' * cell_width,
   -1    52     ] + [
   -1    53         align_center(x2col(x0 + dx), cell_width)
   -1    54         for dx in range(w - 1)
   -1    55     ])
   -1    56     for dy in range(height - 1):
   -1    57         rows.append([
   -1    58             align_right(str(y0 + dy + 1), cell_width),
   -1    59         ] + [
   -1    60             to_cell(sheet.get_value(xy2ref(x0 + dx, y0 + dy)), cell_width)
   -1    61             for dx in range(w - 1)
   -1    62         ])
   -1    63     return '\n'.join([''.join(row) for row in rows])