py-05-calc
1.000
Challenge · difficulty 5/5
# Arithmetic Expression Evaluator
Implement a file **`solution.py`** containing a function:
```python
def evaluate(expr: str) -> float:
...
```
Evaluate an arithmetic expression given as a string and return its value as a
`float`.
Supported syntax:
- Binary operators `+`, `-`, `*`, `/` with standard precedence
(`*` and `/` bind tighter than `+` and `-`) and left-to-right associativity.
- Parentheses `(` ... `)` for grouping.
- Unary plus and minus, e.g. `-3`, `-(2 + 1)`, `2 * -3`.
- Integer and float literals, e.g. `42`, `3.14`, `.5`, `10.`.
- Arbitrary surrounding / internal whitespace, which is ignored.
Requirements:
- Return the numeric result as a `float`. For example
`evaluate("1 + 2 * 3") == 7.0` and `evaluate("(1 + 2) * 3") == 9.0`.
- Implement a real parser/evaluator that respects precedence and parentheses.
**Do not** use `eval`, `exec`, or similar (the harness may inspect for this).
- Raise `ValueError` on malformed input. Malformed includes: empty / whitespace-only
input, unbalanced parentheses, a missing operand or operator (e.g. `"1 +"`,
`"1 2"`, `"* 3"`), and unexpected characters.
- Division by zero should raise an error (a `ZeroDivisionError` or `ValueError`
is acceptable).
Examples:
```python
evaluate("2 + 3 * 4") # 14.0
evaluate("(2 + 3) * 4") # 20.0
evaluate("10 / 4") # 2.5
evaluate("-3 + 2") # -1.0
evaluate("2 * (1 + -1.5)") # -1.0
```
tests/test_calc.py
import pytest
from solution import evaluate
def test_simple_addition():
assert evaluate("1 + 2") == 3.0
def test_precedence():
assert evaluate("2 + 3 * 4") == 14.0
assert evaluate("1 + 2 * 3") == 7.0
def test_parentheses_override_precedence():
assert evaluate("(2 + 3) * 4") == 20.0
assert evaluate("(1 + 2) * 3") == 9.0
def test_division_is_float():
assert evaluate("10 / 4") == 2.5
assert isinstance(evaluate("4 / 2"), float)
def test_unary_minus():
assert evaluate("-3 + 2") == -1.0
assert evaluate("2 * -3") == -6.0
assert evaluate("-(2 + 1)") == -3.0
def test_float_and_whitespace():
assert evaluate(" 3.14 + .86 ") == pytest.approx(4.0)
assert evaluate("10. / 4") == 2.5
def test_left_associativity():
assert evaluate("10 - 2 - 3") == 5.0
assert evaluate("100 / 5 / 2") == 10.0
def test_nested_parens():
assert evaluate("2 * (1 + (3 - 1) * 2)") == 10.0
def test_malformed_raises_value_error():
for bad in ["", " ", "1 +", "* 3", "1 2", "(1 + 2", "1 + 2)", "1 + * 2", "3 $ 4"]:
with pytest.raises(ValueError):
evaluate(bad)
def test_division_by_zero_raises():
with pytest.raises((ValueError, ZeroDivisionError)):
evaluate("1 / 0")
System prompt
You are an expert programmer. Solve the task exactly as specified. Output your solution as fenced code blocks using the required file name(s) and the exact function/type signatures requested. Prefer correctness; do not include prose outside code unless asked.
Proposed solution
```python
def evaluate(expr: str) -> float:
if not expr or expr.strip() == "":
raise ValueError("Empty expression")
# Tokenize the expression
tokens = []
i = 0
n = len(expr)
while i < n:
c = expr[i]
# Skip whitespace
if c.isspace():
i += 1
continue
# Check for numbers (integers and floats)
if c.isdigit() or c == '.':
start = i
has_dot = False
while i < n and (expr[i].isdigit() or expr[i] == '.'):
if expr[i] == '.':
if has_dot:
raise ValueError("Invalid number format")
has_dot = True
i += 1
num_str = expr[start:i]
try:
# Handle cases like ".5" or "10."
if num_str == '.':
raise ValueError("Invalid number format")
value = float(num_str)
tokens.append(('NUM', value))
except ValueError:
raise ValueError(f"Invalid number: {num_str}")
continue
# Check for operators and parentheses
if c in '+-*/':
tokens.append(('OP', c))
i += 1
continue
if c == '(':
tokens.append(('LPAREN', c))
i += 1
continue
if c == ')':
tokens.append(('RPAREN', c))
i += 1
continue
# Invalid character
raise ValueError(f"Unexpected character: {c}")
if not tokens:
raise ValueError("Empty expression")
# Parse and evaluate using recursive descent parser
pos = [0] # Use list to allow modification in nested functions
def peek():
if pos[0] < len(tokens):
return tokens[pos[0]]
return None
def consume():
token = tokens[pos[0]]
pos[0] += 1
return token
def parse_expression():
"""Parse addition and subtraction (lowest precedence)"""
left = parse_term()
while peek() and peek()[0] == 'OP' and peek()[1] in '+-':
op = consume()[1]
right = parse_term()
if op == '+':
left = left + right
else: # op == '-'
left = left - right
return left
def parse_term():
"""Parse multiplication and division (higher precedence)"""
left = parse_factor()
while peek() and peek()[0] == 'OP' and peek()[1] in '*/':
op = consume()[1]
right = parse_factor()
if op == '*':
left = left * right
else: # op == '/'
if right == 0:
raise ZeroDivisionError("Division by zero")
left = left / right
return left
def parse_factor():
"""Parse unary operators and primary expressions"""
token = peek()
# Handle unary plus/minus
if token and token[0] == 'OP' and token[1] in '+-':
op = consume()[1]
operand = parse_factor() # Recursive call for nested unary operators
if op == '+':
return operand
else: # op == '-'
return -operand
# Handle parentheses
if token and token[0] == 'LPAREN':
consume() # Consume '('
result = parse_expression()
if not peek() or peek()[0] != 'RPAREN':
raise ValueError("Unbalanced parentheses")
consume() # Consume ')'
return result
# Handle numbers
if token and token[0] == 'NUM':
return consume()[1]
# Error cases
if token is None:
raise ValueError("Unexpected end of expression")
elif token[0] == 'RPAREN':
raise ValueError("Unbalanced parentheses")
else:
raise ValueError(f"Unexpected token: {token}")
try:
result = parse_expression()
# Check for leftover tokens
if pos[0] < len(tokens):
raise ValueError("Unexpected tokens at end of expression")
return float(result)
except ZeroDivisionError:
raise
except Exception as e:
if isinstance(e, ValueError):
raise
else:
raise ValueError(f"Parse error: {str(e)}")
```Test output (stdout)
.......... [100%] 10 passed in 0.02s