Created
February 2, 2024 04:26
-
-
Save silphendio/d39ed56e239d0c701c2bd7660c19f519 to your computer and use it in GitHub Desktop.
a simple math expression parser
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This is a simple math expression parser | |
# it solves expressions the way we learned it in math class | |
# and optionally prints the in-between steps | |
# | |
# malformed input produces either ValueError or IndexError | |
# supported are numbers (int, float, complex), parentheses, and operators (+ , - , / , * and ^) | |
# ^ is used for exponents, because ** would be a pain to implement | |
# | |
# licensed under Zero-Clause BSD (https://opensource.org/license/0bsd/) | |
# - silphendio | |
dbg = False | |
def dbg_array(arr): | |
if dbg: print(' '.join(map(str, arr))) | |
def str_to_number(x): | |
try: | |
return int(x) | |
except ValueError: | |
try: | |
return float(x) | |
except ValueError: | |
return complex(x) | |
# calculations | |
def _calc_prefix(x, op): | |
if op == '+': | |
return x | |
if op == '-': | |
return -x | |
raise ValueError("invalid operand") | |
def _calc_infix(a, b, op): | |
if op == '+': | |
return a + b | |
if op == '-': | |
return a - b | |
if op == '*': | |
return a * b | |
if op == '/': | |
return a / b | |
if op == '^': | |
return a ** b | |
raise ValueError("invalid operand") | |
# helper functions | |
def _get_matching_paren(tokens, lp_idx): | |
p_count = 1 | |
for i in range(lp_idx + 1, len(tokens)): | |
p_count += (tokens[i] == '(') | |
p_count -= (tokens[i] == ')') | |
if p_count == 0: | |
return i | |
raise ValueError("mismatched parenthesis") | |
def _is_op(token, ops = '+-/*()^'): | |
return token in iter(ops) | |
def solve_expr(expr): | |
tokens = [] | |
j = 0 | |
for i, x in enumerate(expr): | |
if x in '()*/+-^': | |
if expr[j:i].strip() != "": | |
tokens += [expr[j:i].strip()] | |
tokens += [x] | |
j = i + 1 | |
if expr[j:].strip() != '': | |
tokens += [expr[j:].strip()] | |
for i, tok in enumerate(tokens): | |
if not _is_op(tok): | |
tokens[i] = str_to_number(tok) | |
return _solve(tokens) | |
def _solve(tokens): | |
dbg_array(tokens) | |
# ( ) | |
while '(' in tokens: | |
lp_idx = tokens.index('(') | |
rp_idx = _get_matching_paren(tokens, lp_idx) | |
tokens[lp_idx : rp_idx+1] = [_solve(tokens[lp_idx+1 : rp_idx])] | |
dbg_array(tokens) | |
# prefix + - | |
i = 0 | |
while i < len(tokens): | |
if _is_op(tokens[i], '+-') and (i==0 or _is_op(tokens[i-1])) and not _is_op(tokens[i+1]): | |
tokens[i: i+2] = [_calc_prefix(tokens[i+1], tokens[i])] | |
dbg_array(["="] + tokens) | |
i += 1 | |
# do ^ then * / then + - | |
for ops in ['^', '*/', '+-']: | |
i = 0 | |
while i < len(tokens): | |
if _is_op(tokens[i], ops): | |
tokens[i-1: i+2] = [_calc_infix(tokens[i-1], tokens[i+1], tokens[i])] | |
dbg_array(["="] + tokens) | |
i -= 1 | |
i += 1 | |
if len(tokens) > 1: | |
raise ValueError("can't finish calculation: " + ' '.join(map(str, tokens))) | |
return tokens[0] | |
if __name__ == "__main__": | |
import sys | |
dbg = True | |
if len(sys.argv) > 1: | |
solve_expr(sys.argv[1]) | |
exit() | |
while True: | |
expr = input(">>>") | |
try: | |
result = solve_expr(expr) | |
print("-------------------------") | |
print(result) | |
except ValueError as error: | |
print("error: " + error) | |
except Exception: | |
print("syntax error") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment