Dijkstra's two-stack expression evaluation in Python
Evaluates a fully parenthesized arithmetic expression using one stack for operands and another for operators.
- File
dijkstra_two_stack_evaluation.py, 39 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/dijkstra-two-stack-evaluation/. The authors are listed in prov/9KN9AK.jsonl.
from fractions import Fraction
from typing import Callable
OPS: dict[str, Callable[[Fraction, Fraction], Fraction]] = {
"+": lambda a, b: a + b,
"-": lambda a, b: a - b,
"*": lambda a, b: a * b,
"/": lambda a, b: a / b,
}
def evaluate(tokens: list[str]) -> Fraction:
"""Evaluate a fully parenthesized expression given as a list of tokens.
Tokens are "(", ")", one of + - * /, or a number. Left parentheses may
be left out. Cost: O(n) time and O(d) space for nesting depth d.
Precondition: the tokens form a fully parenthesized expression.
Raises ValueError if the stacks show that the input is not well formed.
"""
vals: list[Fraction] = [] # @line L1
ops: list[str] = []
for t in tokens: # @line L2
if t == "(": # @line L3
continue
elif t in OPS: # @line L4
ops.append(t)
elif t == ")": # @line L5
if not ops or len(vals) < 2:
raise ValueError("unbalanced ')'")
op, b, a = ops.pop(), vals.pop(), vals.pop() # @line L6
vals.append(OPS[op](a, b)) # @line L7
else:
vals.append(Fraction(t)) # @line L8
if len(vals) != 1 or ops:
raise ValueError("not a fully parenthesized expression")
return vals.pop() # @line L9
test_dijkstra_two_stack_evaluation.py 52 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/dijkstra-two-stack-evaluation/. The authors are listed in prov/9KN9AK.jsonl.
import random
from fractions import Fraction
import pytest
from dijkstra_two_stack_evaluation import evaluate
def test_page_example() -> None:
e = "( 1 + ( ( 2 + 3 ) * ( 4 * 5 ) ) )".split()
assert len(e) == 17
assert evaluate(e) == 101
assert evaluate("1 + 2 + 3 ) * 4 * 5 ) ) )".split()) == 101
def test_order_of_operands() -> None:
assert evaluate("( 7 - 2 )".split()) == 5
assert evaluate("( 7 / 2 )".split()) == Fraction(7, 2)
assert evaluate(["42"]) == 42
def test_malformed() -> None:
with pytest.raises(ValueError):
evaluate("( 1 + 2 * 3 )".split())
with pytest.raises(ValueError):
evaluate("1 + 2 ) )".split())
with pytest.raises(ValueError):
evaluate([])
def build(rng: random.Random, depth: int) -> tuple[list[str], Fraction]:
if depth == 0 or rng.random() < 0.3:
x = rng.randrange(1, 10)
return [str(x)], Fraction(x)
op = rng.choice("+-*/")
left, a = build(rng, depth - 1)
right, b = build(rng, depth - 1)
if op == "/" and b == 0:
op = "+"
v = {"+": a + b, "-": a - b, "*": a * b, "/": a / b if b else a}[op]
return ["("] + left + [op] + right + [")"], v
def test_random_against_recursive_value() -> None:
rng = random.Random(1961)
for _ in range(800):
toks, v = build(rng, rng.randrange(0, 6))
assert evaluate(toks) == v
assert evaluate([t for t in toks if t != "("]) == v
What the next level needs
- the tests did not run in this build
Read the full page about Dijkstra's two-stack expression evaluation