Compare commits
2 Commits
b50b42c01b
...
5bbbb4f1da
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bbbb4f1da | ||
|
|
b7b584a989 |
Binary file not shown.
2
CS1527/assessment-2/.envrc
Normal file
2
CS1527/assessment-2/.envrc
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export NIX_PATH="nixpkgs=flake:nixpkgs"
|
||||||
|
use nix
|
||||||
404
CS1527/assessment-2/assessment2.py
Normal file
404
CS1527/assessment-2/assessment2.py
Normal file
@@ -0,0 +1,404 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Usage: `python assessment2.py {subcommand} {expressions}`
|
||||||
|
|
||||||
|
If expressions are not provided, they are taken from each line of stdin.
|
||||||
|
Documentation of subcommands is available from `python assessment2.py -h`.
|
||||||
|
|
||||||
|
Examples for marking criteria:
|
||||||
|
1. Eval: `python assessment2.py eval "(((2*(3+2))+5)/2)"`
|
||||||
|
2. Tree: `python assessment2.py render-tree "(((2*(3+2))+5)/2)"`
|
||||||
|
3. Preorder: `python assessment2.py format-prefix "(((2*(3+2))+5)/2)"`
|
||||||
|
In-order: `python assessment2.py format-infix "(((2*(3+2))+5)/2)"`
|
||||||
|
Postorder: `python assessment2.py format-postfix "(((2*(3+2))+5)/2)"`
|
||||||
|
4. Errors: `python assessment2.py eval "(4*3*2)" "(4*(2))" "(4*(3+2)*(2+1))" "(2*4)*(3+2)" "((2+3)*(4*5)" "((2+3)*(4*5)" "(2+5)*(4/(2+2)))" "(2+5)*(4/(2+2)))" "(((2+3)*(4*5))+(1(2+3)))"`
|
||||||
|
5. Tests: `python assessment2.py test`
|
||||||
|
|
||||||
|
Tests can be run with `pytest assessment2.py` or `python assessment2.py test`.
|
||||||
|
|
||||||
|
All of the formatting is implemented with post-order traversals. This is
|
||||||
|
necessary to output parenthesis in the pre- and in-order cases. It could also be
|
||||||
|
done with a buffer and a combined traversal (with callbacks before, between, and
|
||||||
|
after subtree visits), but that requires mutation.
|
||||||
|
|
||||||
|
Note: this program requires Python 3.12 due to the generic syntax used to define
|
||||||
|
`BTree.traverse`. Pytest is also required. These should be installed already
|
||||||
|
on Codio.
|
||||||
|
"""
|
||||||
|
# stdlib
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Literal, Tuple, Callable, cast, override
|
||||||
|
from operator import add, sub, mul, truediv as div
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
|
# pytest, from https://pypi.org/project/pytest/
|
||||||
|
# authors can be found at https://github.com/pytest-dev/pytest/graphs/contributors,
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
OPERATORS: dict[Literal["+", "-", "*", "/"], Callable[[float, float], float]] = {
|
||||||
|
"+": add,
|
||||||
|
"-": sub,
|
||||||
|
"*": mul,
|
||||||
|
"/": div,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BTree:
|
||||||
|
value: float
|
||||||
|
|
||||||
|
def traverse[T](
|
||||||
|
self,
|
||||||
|
value: Callable[["Value", int], T],
|
||||||
|
operator: Callable[["Operator", T, T, int], T],
|
||||||
|
depth: int = 0,
|
||||||
|
) -> T:
|
||||||
|
"""Traverse the binary tree with a post-order traversal
|
||||||
|
|
||||||
|
`value` takes the value node and its depth from the root node
|
||||||
|
`operator` takes the operator node, the results of traversing the left
|
||||||
|
and right subtrees, and the depth of the node
|
||||||
|
|
||||||
|
The tree could, for example, be evaluated with `traverse`:
|
||||||
|
```
|
||||||
|
(...).traverse(
|
||||||
|
lambda node, _: node.value,
|
||||||
|
lambda node, left, right, _: OPERATORS[node.operator](left, right)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def as_preorder_str(self) -> str:
|
||||||
|
"""Format the tree in prefix notation"""
|
||||||
|
return self.traverse(
|
||||||
|
lambda value, _: str(value.value),
|
||||||
|
lambda op, left, right, _: f"({op.operator} {left} {right})",
|
||||||
|
)
|
||||||
|
|
||||||
|
def as_inorder_str(self) -> str:
|
||||||
|
"""Format the tree as a typical parenthesized expression"""
|
||||||
|
return self.traverse(
|
||||||
|
lambda value, _: str(value.value),
|
||||||
|
lambda op, left, right, _: f"({left} {op.operator} {right})",
|
||||||
|
)
|
||||||
|
|
||||||
|
__str__ = as_inorder_str
|
||||||
|
|
||||||
|
def as_inorder_lines(self) -> str:
|
||||||
|
"""Format the tree, visually as a tree"""
|
||||||
|
return self.traverse(
|
||||||
|
lambda value, depth: " " * depth + str(value.value),
|
||||||
|
lambda op, left, right, depth: f"{left}\n{' ' * depth}{op.operator}\n{right}",
|
||||||
|
)
|
||||||
|
|
||||||
|
def as_postorder_str(self) -> str:
|
||||||
|
"""Format the tree in postfix notation (RPN)"""
|
||||||
|
return self.traverse(
|
||||||
|
lambda value, _: str(value.value),
|
||||||
|
lambda op, left, right, _: f"{left} {right} {op.operator}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class Value(BTree):
|
||||||
|
value: float
|
||||||
|
|
||||||
|
@override
|
||||||
|
def traverse[T](
|
||||||
|
self,
|
||||||
|
value: Callable[["Value", int], T],
|
||||||
|
operator: Callable[["Operator", T, T, int], T],
|
||||||
|
depth: int = 0,
|
||||||
|
) -> T:
|
||||||
|
"""Traverse the binary tree with a post-order traversal
|
||||||
|
|
||||||
|
`value` takes the value node and its depth from the root node
|
||||||
|
`operator` takes the operator node, the results of traversing the left
|
||||||
|
and right subtrees, and the depth of the node
|
||||||
|
|
||||||
|
The tree could, for example, be evaluated with `traverse`:
|
||||||
|
```
|
||||||
|
(...).traverse(
|
||||||
|
lambda node, _: node.value,
|
||||||
|
lambda node, left, right, _: OPERATORS[node.operator](left, right)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
return value(self, depth)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class Operator(BTree):
|
||||||
|
operator: Literal["+", "-", "*", "/"]
|
||||||
|
left: BTree
|
||||||
|
right: BTree
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self) -> float: # type: ignore - the field should be read-only
|
||||||
|
return OPERATORS[self.operator](self.left.value, self.right.value)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def traverse[T](
|
||||||
|
self,
|
||||||
|
value: Callable[["Value", int], T],
|
||||||
|
operator: Callable[["Operator", T, T, int], T],
|
||||||
|
depth: int = 0,
|
||||||
|
) -> T:
|
||||||
|
"""Traverse the binary tree with a post-order traversal
|
||||||
|
|
||||||
|
`value` takes the value node and its depth from the root node
|
||||||
|
`operator` takes the operator node, the results of traversing the left
|
||||||
|
and right subtrees, and the depth of the node
|
||||||
|
|
||||||
|
The tree could, for example, be evaluated with `traverse`:
|
||||||
|
```
|
||||||
|
(...).traverse(
|
||||||
|
lambda node, _: node.value,
|
||||||
|
lambda node, left, right, _: OPERATORS[node.operator](left, right)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
return operator(
|
||||||
|
self,
|
||||||
|
self.left.traverse(value, operator, depth + 1),
|
||||||
|
self.right.traverse(value, operator, depth + 1),
|
||||||
|
depth,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse(expr: str) -> BTree:
|
||||||
|
"""Parse a parenthesized expression as in the spec.
|
||||||
|
|
||||||
|
Requires a single parenthesized string, potentially with whitespace
|
||||||
|
Raises ValueError on parse errors
|
||||||
|
"""
|
||||||
|
|
||||||
|
def number(expr: str) -> Tuple[BTree, str] | None:
|
||||||
|
if not expr[0].isdigit():
|
||||||
|
return None
|
||||||
|
rest = expr.lstrip("0123456789")
|
||||||
|
return Value(int(expr[: len(expr) - len(rest)])), rest.lstrip()
|
||||||
|
|
||||||
|
def parenthesized(expr: str) -> Tuple[BTree, str] | None:
|
||||||
|
if not expr.startswith("("):
|
||||||
|
return None
|
||||||
|
expr = expr[1:].lstrip()
|
||||||
|
|
||||||
|
left, expr = operand(expr)
|
||||||
|
|
||||||
|
operator, expr = expr[0], expr[1:]
|
||||||
|
if operator not in OPERATORS:
|
||||||
|
raise ValueError(f"Unknown operator {operator}")
|
||||||
|
operator = cast(Literal["+", "-", "*", "/"], operator)
|
||||||
|
|
||||||
|
right, expr = operand(expr.lstrip())
|
||||||
|
|
||||||
|
if not expr.startswith(")"):
|
||||||
|
if expr and expr[0] in OPERATORS:
|
||||||
|
raise ValueError("Too many operands in expression")
|
||||||
|
raise ValueError("Expected closing parenthesis")
|
||||||
|
return Operator(operator, left, right), expr[1:].lstrip()
|
||||||
|
|
||||||
|
def operand(expr: str) -> Tuple[BTree, str]:
|
||||||
|
v = number(expr) or parenthesized(expr)
|
||||||
|
if not v:
|
||||||
|
raise ValueError(
|
||||||
|
"Expected a number or the beginning of a parenthesized expression"
|
||||||
|
)
|
||||||
|
return v
|
||||||
|
|
||||||
|
result = parenthesized(expr.lstrip())
|
||||||
|
if not result or result[1]:
|
||||||
|
# If rest is non-empty, then something was after the parenthesis starting the expression
|
||||||
|
# i.e. the full expression was not parenthesized.
|
||||||
|
raise ValueError("Expected parenthesized expression at the top level")
|
||||||
|
|
||||||
|
return result[0]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("expr", "ast"),
|
||||||
|
[
|
||||||
|
("(1 + 1)", Operator("+", Value(1), Value(1))),
|
||||||
|
(
|
||||||
|
"(2*(3+ 2))",
|
||||||
|
Operator("*", Value(2), Operator("+", Value(3), Value(2))),
|
||||||
|
),
|
||||||
|
(" (1 +1) ", Operator("+", Value(1), Value(1))),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_parse(expr: str, ast: BTree):
|
||||||
|
"""Test parsing of expressions"""
|
||||||
|
assert parse(expr) == ast
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("expr", "value"),
|
||||||
|
[
|
||||||
|
("(1 + 2)", 3),
|
||||||
|
("(12 + (((4 / 2) * 3) + (3 * 2)))", 24),
|
||||||
|
("(((2 * (3 + 2)) + 5) / 2)", 7.5),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_eval(expr: str, value: float):
|
||||||
|
"""Test evaluation"""
|
||||||
|
assert parse(expr).value == value
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("expr", "tree"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"(1 + 2)",
|
||||||
|
"""
|
||||||
|
1
|
||||||
|
+
|
||||||
|
2
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"(1 + (((4 / 2) * 3) + (3 * 2)))",
|
||||||
|
"""
|
||||||
|
1
|
||||||
|
+
|
||||||
|
4
|
||||||
|
/
|
||||||
|
2
|
||||||
|
*
|
||||||
|
3
|
||||||
|
+
|
||||||
|
3
|
||||||
|
*
|
||||||
|
2
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"(((2*(3+2))+5)/2)",
|
||||||
|
"""
|
||||||
|
2
|
||||||
|
*
|
||||||
|
3
|
||||||
|
+
|
||||||
|
2
|
||||||
|
+
|
||||||
|
5
|
||||||
|
/
|
||||||
|
2
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_inorder_lines(expr: str, tree: str):
|
||||||
|
"""Test tree rendering"""
|
||||||
|
assert parse(expr).as_inorder_lines() == dedent(tree).strip("\n")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("expr", "prefix"),
|
||||||
|
[
|
||||||
|
("(1 + 2)", "(+ 1 2)"),
|
||||||
|
("(1 + (((4 / 2) * 3) + (3 * 2)))", "(+ 1 (+ (* (/ 4 2) 3) (* 3 2)))"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_prefix(expr: str, prefix: str):
|
||||||
|
"""Test prefix formatting"""
|
||||||
|
assert parse(expr).as_preorder_str() == prefix
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("expr"),
|
||||||
|
[
|
||||||
|
"(1 + 2)",
|
||||||
|
"(1 + (((4 / 2) * 3) + (3 * 2)))",
|
||||||
|
"(((3 + 2) * 2) + (((7 / 3) * 5) - 3))",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_infix_roundtrip(expr: str):
|
||||||
|
"""Test infix formatting roundtrips"""
|
||||||
|
assert str(parse(expr)) == expr
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("expr", "postfix"),
|
||||||
|
[
|
||||||
|
("(1 + 2)", "1 2 +"),
|
||||||
|
("(1 + (((4 / 2) * 3) + (3 * 2)))", "1 4 2 / 3 * 3 2 * + +"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_postfix(expr: str, postfix: str):
|
||||||
|
"""Test postfix formatting"""
|
||||||
|
assert parse(expr).as_postorder_str() == postfix
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("expr", "err"),
|
||||||
|
[
|
||||||
|
("(4 * 3 * 2)", "Too many operands in expression"),
|
||||||
|
("(4 * (2))", "Unknown operator )"),
|
||||||
|
("(4 * (3 + 2) * (2 + 1))", "Too many operands in expression"),
|
||||||
|
("(2 *4)*(3+2)", "Expected parenthesized expression at the top level"),
|
||||||
|
("(2+5)*(4/(2+2))", "Expected parenthesized expression at the top level"),
|
||||||
|
("(((2+3)*(4*5))+(1(2+3)))", "Unknown operator ("),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_error(expr: str, err: str):
|
||||||
|
"""Test error messages"""
|
||||||
|
with pytest.raises(ValueError, match=re.escape(err)):
|
||||||
|
parse(expr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = ArgumentParser(usage=__doc__)
|
||||||
|
subcommands = parser.add_subparsers(title="Subcommands", required=True)
|
||||||
|
|
||||||
|
def with_expressions(parser):
|
||||||
|
parser.add_argument(
|
||||||
|
"expressions",
|
||||||
|
nargs="*",
|
||||||
|
help="The expressions to operate on. If none are provided, operate on lines of stdin.",
|
||||||
|
)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
with_expressions(
|
||||||
|
subcommands.add_parser("eval", help="Evaluate the expression")
|
||||||
|
).set_defaults(func=lambda expr: expr.value)
|
||||||
|
with_expressions(
|
||||||
|
subcommands.add_parser("render-tree", help="Render the expression as a tree")
|
||||||
|
).set_defaults(func=BTree.as_inorder_lines)
|
||||||
|
with_expressions(
|
||||||
|
subcommands.add_parser(
|
||||||
|
"format-prefix", help="Format the expression in prefix notation"
|
||||||
|
)
|
||||||
|
).set_defaults(func=BTree.as_preorder_str)
|
||||||
|
with_expressions(
|
||||||
|
subcommands.add_parser(
|
||||||
|
"format-infix",
|
||||||
|
help="Format the expression in typical parenthesized infix notation",
|
||||||
|
)
|
||||||
|
).set_defaults(func=BTree.as_inorder_str)
|
||||||
|
with_expressions(
|
||||||
|
subcommands.add_parser(
|
||||||
|
"format-postfix",
|
||||||
|
help="Format the expression in postfix notation, i.e. as RPN",
|
||||||
|
)
|
||||||
|
).set_defaults(func=BTree.as_postorder_str)
|
||||||
|
subcommands.add_parser("test", help="Run tests").set_defaults(
|
||||||
|
func=lambda: pytest.main([__file__])
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
if "expressions" in args:
|
||||||
|
for expression in args.expressions or sys.stdin:
|
||||||
|
if len(args.expressions) > 1:
|
||||||
|
print(expression + ":")
|
||||||
|
try:
|
||||||
|
parsed = parse(expression)
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
continue
|
||||||
|
print(args.func(parsed))
|
||||||
|
else:
|
||||||
|
args.func()
|
||||||
3
CS1527/assessment-2/shell.nix
Normal file
3
CS1527/assessment-2/shell.nix
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{ pkgs ? import <nixpkgs> {} }: pkgs.mkShell {
|
||||||
|
packages = [(pkgs.python312.withPackages (p: with p; [ python-lsp-server pytest pytest-watch black ])) pkgs.nodePackages.pyright];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user