restructure project + unit tests

This commit is contained in:
2026-04-13 11:44:26 +02:00
parent 50550a1f06
commit 1fb4d30407
9 changed files with 217 additions and 122 deletions
+6
View File
@@ -0,0 +1,6 @@
from .parser import Parser, tokenize
from .printer import format_poly
def convert(expr: str) -> str:
poly = Parser(tokenize(expr)).parse()
return format_poly(poly)
+18
View File
@@ -0,0 +1,18 @@
from __future__ import annotations
import sys
from . import convert
def main() -> None:
if len(sys.argv) > 1:
expr = " ".join(sys.argv[1:])
else:
expr = input("Enter formula: ").strip()
print(convert(expr))
if __name__ == "__main__":
main()
+38
View File
@@ -0,0 +1,38 @@
from __future__ import annotations
from typing import FrozenSet
Monomial = FrozenSet[str]
Poly = set[Monomial]
def zero() -> Poly:
return set()
def one() -> Poly:
return {frozenset()}
def var(name: str) -> Poly:
return {frozenset([name])}
def poly_xor(a: Poly, b: Poly) -> Poly:
return a ^ b
def poly_and(a: Poly, b: Poly) -> Poly:
out: Poly = set()
for ma in a:
for mb in b:
m = frozenset(set(ma) | set(mb))
if m in out:
out.remove(m)
else:
out.add(m)
return out
def poly_not(a: Poly) -> Poly:
return poly_xor(a, one())
+100
View File
@@ -0,0 +1,100 @@
from __future__ import annotations
import re
from typing import List
from .core import Poly, poly_and, poly_not, poly_xor, one, var, zero
TOKEN_RE = re.compile(
r"""
\s*(
\(|\)|
!|
\+|\*|
AND|OR|
0|1|
X\d+|
[A-Za-z_][A-Za-z0-9_]*
)
""",
re.VERBOSE,
)
def tokenize(s: str) -> List[str]:
tokens = TOKEN_RE.findall(s)
stripped = re.sub(r"\s+", "", s)
if "".join(tokens) != stripped:
raise ValueError(f"Invalid token near: {s!r}")
return tokens
class Parser:
def __init__(self, tokens: List[str]):
self.tokens = tokens
self.i = 0
def peek(self) -> str | None:
return self.tokens[self.i] if self.i < len(self.tokens) else None
def eat(self, expected: str | None = None) -> str:
tok = self.peek()
if tok is None:
raise ValueError("Unexpected end of input")
if expected is not None and tok != expected:
raise ValueError(f"Expected {expected!r}, got {tok!r}")
self.i += 1
return tok
def parse(self) -> Poly:
p = self.parse_or()
if self.peek() is not None:
raise ValueError(f"Unexpected token: {self.peek()!r}")
return p
def parse_or(self) -> Poly:
left = self.parse_and()
while self.peek() in ("OR", "+"):
self.eat()
right = self.parse_and()
left = poly_xor(poly_xor(left, right), poly_and(left, right))
return left
def parse_and(self) -> Poly:
left = self.parse_not()
while self.peek() in ("AND", "*"):
self.eat()
right = self.parse_not()
left = poly_and(left, right)
return left
def parse_not(self) -> Poly:
if self.peek() == "!":
self.eat("!")
return poly_not(self.parse_not())
return self.parse_atom()
def parse_atom(self) -> Poly:
tok = self.peek()
if tok is None:
raise ValueError("Unexpected end of input")
if tok == "(":
self.eat("(")
inside = self.parse_or()
self.eat(")")
return inside
if tok == "0":
self.eat("0")
return zero()
if tok == "1":
self.eat("1")
return one()
if re.fullmatch(r"X\d+|[A-Za-z_][A-Za-z0-9_]*", tok):
self.eat()
return var(tok)
raise ValueError(f"Unexpected token: {tok!r}")
+24
View File
@@ -0,0 +1,24 @@
from __future__ import annotations
from .core import Monomial, Poly
def monomial_key(m: Monomial):
return (len(m), tuple(sorted(m)))
def format_monomial(m: Monomial) -> str:
if len(m) == 0:
return "1"
if len(m) == 1:
return next(iter(m))
parts = sorted(m, key=lambda x: (len(x), x))
return "(" + "".join(parts) + ")"
def format_poly(p: Poly) -> str:
if not p:
return "0"
monos = sorted(p, key=monomial_key)
return "".join(format_monomial(m) for m in monos)