165 lines
5.0 KiB
Python
165 lines
5.0 KiB
Python
"""Tiny arithmetic expression evaluator with proper precedence.
|
|
|
|
Supports: + - * / // % **, unary +/-, parentheses, variables (dict),
|
|
functions (sin, cos, sqrt, abs, max, min), and error reporting with
|
|
position. No eval/exec — pure recursive descent.
|
|
"""
|
|
import math
|
|
import re
|
|
|
|
class EvalError(Exception):
|
|
def __init__(self, msg, pos=None):
|
|
super().__init__(msg)
|
|
self.pos = pos
|
|
|
|
_TOKEN_RE = re.compile(r"""
|
|
\s*(?:
|
|
(?P<num>\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)
|
|
| (?P<ident>[A-Za-z_][A-Za-z0-9_]*)
|
|
| (?P<op>\*\*|//|[+\-*/%(),])
|
|
)
|
|
""", re.VERBOSE)
|
|
|
|
def tokenize(s):
|
|
toks = []
|
|
i = 0
|
|
n = len(s)
|
|
while i < n:
|
|
m = _TOKEN_RE.match(s, i)
|
|
if not m:
|
|
raise EvalError(f"unexpected char {s[i:i+5]!r}", i)
|
|
i = m.end()
|
|
if m.group('num') is not None:
|
|
toks.append(('num', float(m.group('num')), m.start()))
|
|
elif m.group('ident') is not None:
|
|
toks.append(('ident', m.group('ident'), m.start()))
|
|
else:
|
|
toks.append(('op', m.group('op'), m.start()))
|
|
toks.append(('eof', None, n))
|
|
return toks
|
|
|
|
FUNCS = {
|
|
'sin': lambda a: math.sin(a),
|
|
'cos': lambda a: math.cos(a),
|
|
'sqrt': lambda a: math.sqrt(a),
|
|
'abs': lambda a: abs(a),
|
|
'max': lambda *a: max(a),
|
|
'min': lambda *a: min(a),
|
|
'round': lambda a: round(a),
|
|
'pow': lambda a, b: a ** b,
|
|
}
|
|
|
|
class Parser:
|
|
def __init__(self, toks, env=None):
|
|
self.toks = toks
|
|
self.pos = 0
|
|
self.env = env or {}
|
|
|
|
def peek(self):
|
|
return self.toks[self.pos]
|
|
|
|
def advance(self):
|
|
t = self.toks[self.pos]
|
|
self.pos += 1
|
|
return t
|
|
|
|
def expect(self, kind, val=None):
|
|
t = self.peek()
|
|
if t[0] != kind or (val is not None and t[1] != val):
|
|
raise EvalError(f"expected {kind} {val!r} got {t[0]} {t[1]!r}", t[2])
|
|
return self.advance()
|
|
|
|
# expr := term (('+'|'-') term)*
|
|
def parse_expr(self):
|
|
v = self.parse_term()
|
|
while self.peek()[0] == 'op' and self.peek()[1] in ('+', '-'):
|
|
op = self.advance()[1]
|
|
r = self.parse_term()
|
|
v = v + r if op == '+' else v - r
|
|
return v
|
|
|
|
def parse_term(self):
|
|
v = self.parse_factor()
|
|
while self.peek()[0] == 'op' and self.peek()[1] in ('*', '/', '//', '%'):
|
|
op = self.advance()[1]
|
|
r = self.parse_factor()
|
|
if op == '*':
|
|
v = v * r
|
|
elif op == '/':
|
|
if r == 0:
|
|
raise EvalError("division by zero", self.peek()[2])
|
|
v = v / r
|
|
elif op == '//':
|
|
if r == 0:
|
|
raise EvalError("division by zero", self.peek()[2])
|
|
v = v // r
|
|
elif op == '%':
|
|
if r == 0:
|
|
raise EvalError("modulo by zero", self.peek()[2])
|
|
v = v % r
|
|
return v
|
|
|
|
def parse_factor(self):
|
|
# right-assoc **
|
|
base = self.parse_unary()
|
|
if self.peek()[0] == 'op' and self.peek()[1] == '**':
|
|
self.advance()
|
|
exp = self.parse_factor()
|
|
return base ** exp
|
|
return base
|
|
|
|
def parse_unary(self):
|
|
if self.peek()[0] == 'op' and self.peek()[1] in ('+', '-'):
|
|
op = self.advance()[1]
|
|
v = self.parse_unary()
|
|
return -v if op == '-' else v
|
|
return self.parse_primary()
|
|
|
|
def parse_primary(self):
|
|
t = self.peek()
|
|
if t[0] == 'num':
|
|
self.advance()
|
|
return t[1]
|
|
if t[0] == 'op' and t[1] == '(':
|
|
self.advance()
|
|
v = self.parse_expr()
|
|
self.expect('op', ')')
|
|
return v
|
|
if t[0] == 'ident':
|
|
name = t[1]
|
|
self.advance()
|
|
if self.peek()[0] == 'op' and self.peek()[1] == '(':
|
|
self.advance()
|
|
args = []
|
|
if not (self.peek()[0] == 'op' and self.peek()[1] == ')'):
|
|
args.append(self.parse_expr())
|
|
while self.peek()[0] == 'op' and self.peek()[1] == ',':
|
|
self.advance()
|
|
args.append(self.parse_expr())
|
|
self.expect('op', ')')
|
|
if name not in FUNCS:
|
|
raise EvalError(f"unknown function {name}", t[2])
|
|
try:
|
|
return FUNCS[name](*args)
|
|
except Exception as e:
|
|
raise EvalError(f"function {name}: {e}", t[2])
|
|
# variable
|
|
if name not in self.env:
|
|
raise EvalError(f"unknown variable {name}", t[2])
|
|
return self.env[name]
|
|
raise EvalError(f"unexpected token {t[0]} {t[1]!r}", t[2])
|
|
|
|
def evaluate(s, env=None):
|
|
toks = tokenize(s)
|
|
p = Parser(toks, env)
|
|
v = p.parse_expr()
|
|
if p.peek()[0] != 'eof':
|
|
raise EvalError("trailing tokens", p.peek()[2])
|
|
return v
|
|
|
|
if __name__ == '__main__':
|
|
import sys
|
|
env = {'x': 3, 'pi': math.pi}
|
|
expr = ' '.join(sys.argv[1:]) or '2 + 3*4'
|
|
print(evaluate(expr, env))
|