├── .gitignore ├── README.md └── calc.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | calc 2 | ==== 3 | 4 | A very simple calculator based on a recursive descent parser (not 5 | using python `eval`) 6 | 7 | Supports exponentiation, add, sub, mul, div, modulo/formatting, 8 | shift, bitwise and/or/xor, comparison operators, logical and/or 9 | (short-circuited), unary negation, parenthesized subexpressions 10 | and assignment (an operator like in C, not a statement like in Python). 11 | 12 | Atoms can be integers, floats, strings or variables. 13 | 14 | It's also possible to store an expression in a variable (like a spreadsheet 15 | formula) using the `:=` operator. No check about circular formulas is present. 16 | 17 | The core parser/evaluator is about 40 lines. The whole program is less 18 | than 100 lines in total. 19 | 20 | Works in both Python 2.x and 3.x 21 | 22 | Example session: 23 | 24 | > 1 + 1 25 | 2 26 | > 1 + 2*3 27 | 7 28 | > 1 + 2*(3 + 4) 29 | 15 30 | > pi = 3.141592654 31 | 3.141592654 32 | > r = 10 33 | 10 34 | > r * r * pi 35 | 314.1592654 36 | > area := r * r * pi 37 | 314.1592654 38 | > r = 100 39 | 100 40 | > area 41 | 31415.92654 42 | > r =42 43 | 42 44 | > area 45 | 5541.76944166 46 | -------------------------------------------------------------------------------- /calc.py: -------------------------------------------------------------------------------- 1 | import re, sys 2 | 3 | ops = {"**": (1, "R", lambda x, y: lambda : value(x) ** value(y) ), 4 | "*": (2, "L", lambda x, y: lambda : value(x) * value(y) ), 5 | "/": (2, "L", lambda x, y: lambda : value(x) / value(y) ), 6 | "%": (2, "L", lambda x, y: lambda : value(x) % value(y) ), 7 | "+": (3, "L", lambda x, y: lambda : value(x) + value(y) ), 8 | "-": (3, "L", lambda x, y: lambda : value(x) - value(y) ), 9 | "<<": (4, "L", lambda x, y: lambda : value(x) << value(y) ), 10 | ">>": (4, "L", lambda x, y: lambda : value(x) >> value(y) ), 11 | "|": (5, "L", lambda x, y: lambda : value(x) | value(y) ), 12 | "&": (5, "L", lambda x, y: lambda : value(x) & value(y) ), 13 | "^": (5, "L", lambda x, y: lambda : value(x) ^ value(y) ), 14 | "<": (6, "L", lambda x, y: lambda : value(x) < value(y) ), 15 | "<=": (6, "L", lambda x, y: lambda : value(x) <= value(y) ), 16 | ">": (6, "L", lambda x, y: lambda : value(x) > value(y) ), 17 | ">=": (6, "L", lambda x, y: lambda : value(x) >= value(y) ), 18 | "==": (6, "L", lambda x, y: lambda : value(x) == value(y) ), 19 | "!=": (6, "L", lambda x, y: lambda : value(x) != value(y) ), 20 | "and": (7, "L", lambda x, y: lambda : value(x) and value(y)), 21 | "or": (8, "L", lambda x, y: lambda : value(x) or value(y) ), 22 | "=": (9, "R", lambda x, y: lambda n=name(x): (setvar(n, value(y)), 23 | vars[n])[1]), 24 | ":=": (10, "R", lambda x, y: lambda n=name(x): (setvar(n, y), 25 | value(y))[1])} 26 | vars = {} 27 | 28 | def setvar(n, x): 29 | vars[n] = x 30 | return x 31 | 32 | def value(x): 33 | if isinstance(x, tuple): return value(vars[x[0]]) # Unbox 34 | if callable(x): return value(x()) # Evaluate 35 | return x 36 | 37 | def name(x): 38 | return x[0] 39 | 40 | number = "[0-9]+(?:\\.[0-9]*)?(?:[Ee][-+]?[0-9]+)?" 41 | string = "\"(?:[^\"\\\\]|\\\\.)*\"" 42 | var = "[A-Za-z_][A-Za-z0-9_]*" 43 | 44 | tkexpr = ("|".join(map(re.escape, sorted(ops.keys(), key=len, reverse=True))) + 45 | "|[()]" + 46 | "|" + number + 47 | "|" + string + 48 | "|True|False" + 49 | "|" + var + 50 | "|[^ ]") 51 | 52 | string_esc = {"n": "\n", "t": "\t", "f": "\f", "v": "\v"} 53 | 54 | def expr(tk, i, level=max(v[0] for v in ops.values())): 55 | if level == 0: 56 | if tk[i] == "#": raise RuntimeError("Expression expected") 57 | if tk[i] == "-": 58 | x, i = expr(tk, i + 1, 0) 59 | return (lambda : -value(x)), i 60 | if tk[i] == "(": 61 | x, i = expr(tk, i + 1) 62 | if tk[i] != ")": raise RuntimeError("')' expected") 63 | return x, i+1 64 | if len(tk[i]) > 1 and tk[i][0] == "\"": 65 | return re.sub("\\\\(.)", 66 | (lambda x: string_esc.get(x.group(1)) or x.group(1)), 67 | tk[i][1:-1]), i + 1 68 | if tk[i] in ("True", "False"): 69 | return tk[i] == "True", i + 1 70 | if re.match(var, tk[i]): 71 | return (tk[i],), i + 1 72 | try: 73 | return int(tk[i]), i + 1 74 | except ValueError: 75 | return float(tk[i]), i + 1 76 | else: 77 | x, i = expr(tk, i, level - 1) 78 | op = ops.get(tk[i]) 79 | while op and op[0] == level: 80 | y, i = expr(tk, i + 1, level - (op[1] == "L")) 81 | x = op[2](x, y) 82 | op = ops.get(tk[i]) 83 | return x, i 84 | 85 | def calc(s): 86 | tk = re.findall(tkexpr, s) + ["#"] 87 | x, i = expr(tk, 0) 88 | if tk[i] != "#": 89 | raise RuntimeError("Extra tokens at end of expression") 90 | return value(x) 91 | 92 | if __name__ == "__main__": 93 | if sys.version_info < (3,): 94 | input = raw_input 95 | while True: 96 | try: 97 | x = calc(input("> ")) 98 | print(x) 99 | except EOFError: break 100 | except Exception as e: print("Error: %s" % e) 101 | --------------------------------------------------------------------------------