154 lines
4.2 KiB
Python

"""Simple arithmetic expression calculator with a recursive-descent parser.
Supported operations: +, -, *, / and parentheses.
Does NOT use Python's eval().
Grammar:
expression = term (('+' | '-') term)*
term = factor (('*' | '/') factor)*
factor = NUMBER | '(' expression ')'
"""
def tokenize(expression_text):
"""Convert an expression string into a list of tokens.
Tokens are either numbers (float) or single-character operators / parentheses.
Raises ValueError for characters that are not part of a valid expression.
"""
tokens = []
position = 0
while position < len(expression_text):
character = expression_text[position]
if character.isspace():
position += 1
continue
if character in "+-*/()":
tokens.append(character)
position += 1
continue
if character.isdigit() or character == ".":
start = position
while position < len(expression_text) and (
expression_text[position].isdigit()
or expression_text[position] == "."
):
position += 1
number_text = expression_text[start:position]
tokens.append(float(number_text))
continue
raise ValueError(
f"Unexpected character '{character}' at position {position}"
)
return tokens
def parse_expression(tokens, position):
"""Parse an expression: term (('+' | '-') term)*."""
result, position = parse_term(tokens, position)
while position < len(tokens) and tokens[position] in ("+", "-"):
operator = tokens[position]
position += 1
right_value, position = parse_term(tokens, position)
if operator == "+":
result += right_value
else:
result -= right_value
return result, position
def parse_term(tokens, position):
"""Parse a term: factor (('*' | '/') factor)*."""
result, position = parse_factor(tokens, position)
while position < len(tokens) and tokens[position] in ("*", "/"):
operator = tokens[position]
position += 1
right_value, position = parse_factor(tokens, position)
if operator == "*":
result *= right_value
else:
if right_value == 0:
raise ZeroDivisionError("Division by zero")
result /= right_value
return result, position
def parse_factor(tokens, position):
"""Parse a factor: NUMBER | '(' expression ')'."""
if position >= len(tokens):
raise ValueError("Unexpected end of expression")
token = tokens[position]
if token == "(":
position += 1
result, position = parse_expression(tokens, position)
if position >= len(tokens) or tokens[position] != ")":
raise ValueError("Missing closing parenthesis")
position += 1
return result, position
if isinstance(token, float):
return token, position + 1
raise ValueError(f"Unexpected token: {token}")
def calculate(expression_text):
"""Evaluate an arithmetic expression string and return the result.
Returns the numeric result or an error message string.
"""
if not expression_text.strip():
return "Error: empty expression"
try:
tokens = tokenize(expression_text)
result, final_position = parse_expression(tokens, 0)
if final_position != len(tokens):
return f"Error: unexpected token '{tokens[final_position]}'"
if result == int(result):
return int(result)
return round(result, 10)
except (ValueError, ZeroDivisionError) as error:
return f"Error: {error}"
def main():
"""Run the calculator on a set of test expressions."""
test_expressions = [
"3 + 5",
"10 - 2 * 3",
"(4 + 6) * 2",
"100 / (5 * 2)",
"3.5 + 2.5 * 4",
"(1 + 2) * (3 + 4)",
"",
"10 / 0",
"abc + 1",
]
for expression in test_expressions:
result = calculate(expression)
display_expr = expression if expression else "(empty)"
print(f"{display_expr} = {result}")
if __name__ == "__main__":
main()