"""경쟁사 산근 글 식 — 셈 식 나무 · 평가 · 자리 규칙(PLAN 793-1 · 요구서_652 · eval 없음). 토큰 = `("수", 값)` · `("이름", 글)` · `("참조",)` · `("연산", + - * / ^)` · `("(",)` · `(")",)` · `(",",)` · `("%",)` · `("끼움", 이름 | None, 토큰 목록)`. 나무 마디 = `("수", 값, 토큰 차례)` · `("이름", 소문자)` · `("참조",)` · `("끼움", 이름 | None, 마디)` · `("함수", round | int, [마디])` · `("음", 마디)` · `("셈", 연산, 왼, 오른)` · `("오류", 까닭)`(읽기 실패 — 셀 때 오류). 우선순위 = 파이썬 식과 같음(`^` = 거듭제곱 · 오른쪽 결합 · 단항 `-` 보다 셈 · `%` = 「/ 100」 을 곱셈 단에서 왼쪽부터) — 측정 도구(`tmp/brain_649/G2`)의 파이썬 식 평가와 같은 부동소수 결과. """ from __future__ import annotations import math from typing import Any FUNCS = ("round", "int") MAX_DEPTH = 64 class ExprError(Exception): """식 읽기 · 셈 실패 — 줄 표지로 남김.""" # ── 자리 규칙 ────────────────────────────────────────────────────────── def fl1(x: float) -> float: """소수 1 자리 버림(STmate 줄 금액).""" return math.floor(round(x * 10, 6)) / 10 def rnd(x: float, n: int) -> float: """반올림(half up) — STmate 변수 표시 자리.""" q = 10**n return math.floor(x * q + 0.5 + 1e-9) / q def _cut(q: float) -> Any: return lambda v: math.floor(round(v * q, 6)) / q def _half(q: float) -> Any: return lambda v: math.floor(v * q + 0.5 + 1e-9) / q def _up(q: float) -> Any: return lambda v: math.ceil(round(v * q, 6)) / q # 줄 자리 속성 — 차례 = 되찾기 우선(EST 기본 = 버림1) RULES: list[tuple[str, Any]] = [ ("버림1", _cut(10)), ("버림0", _cut(1)), ("반올림1", _half(10)), ("반올림0", _half(1)), ("버림2", _cut(100)), ("반올림2", _half(100)), ("버림3", _cut(1000)), ("반올림3", _half(1000)), ("올림0", _up(1)), ("올림1", _up(10)), ("올림2", _up(100)), ("그대로", lambda v: v), ] RULE = dict(RULES) EPS = 1e-6 def fit(value: float | None, shown: float) -> str | None: """셈 값 → 보인 값이 되는 첫 자리 규칙 이름(없으면 None).""" if value is None: return None return next((name for name, f in RULES if abs(f(value) - shown) < EPS), None) # ── 읽기(재귀 하강) ──────────────────────────────────────────────────── class _Parser: def __init__(self, toks: list[tuple]) -> None: self.toks, self.at, self.depth = toks, 0, 0 def peek(self) -> tuple | None: return self.toks[self.at] if self.at < len(self.toks) else None def take(self) -> tuple: tok = self.peek() if tok is None: raise ExprError("식이 덜 끝남") self.at += 1 return tok def is_op(self, *ops: str) -> bool: tok = self.peek() return tok is not None and tok[0] == "연산" and tok[1] in ops def expr(self) -> tuple: self.depth += 1 if self.depth > MAX_DEPTH: raise ExprError("괄호가 너무 깊음") node = self.term() while self.is_op("+", "-"): op = self.take()[1] node = ("셈", op, node, self.term()) self.depth -= 1 return node def term(self) -> tuple: node = self.unary() while True: if self.is_op("*", "/"): op = self.take()[1] node = ("셈", op, node, self.unary()) elif (tok := self.peek()) is not None and tok[0] == "%": self.take() node = ("셈", "/", node, ("수", 100.0, None)) else: return node def unary(self) -> tuple: if self.is_op("-"): self.take() return ("음", self.unary()) if self.is_op("+"): self.take() return self.unary() return self.power() def power(self) -> tuple: node = self.atom() if self.is_op("^"): self.take() return ("셈", "^", node, self.unary()) return node def atom(self) -> tuple: at = self.at tok = self.take() kind = tok[0] if kind == "수": return ("수", tok[1], at) if kind == "참조": return ("참조",) if kind == "끼움": return ("끼움", tok[1], parse(tok[2])) if kind == "(": node = self.expr() if self.take()[0] != ")": raise ExprError("괄호 짝 없음") return node if kind == "이름": name = tok[1].lower() if name in FUNCS and (nxt := self.peek()) is not None and nxt[0] == "(": self.take() args = [self.expr()] while (nxt := self.peek()) is not None and nxt[0] == ",": self.take() args.append(self.expr()) if self.take()[0] != ")": raise ExprError("함수 괄호 짝 없음") return ("함수", name, args) return ("이름", name) raise ExprError(f"식 자리 아닌 토큰 {kind}") def parse(toks: list[tuple]) -> tuple: """토큰 → 나무(남는 토큰 · 빈 식 = 오류).""" if not toks: raise ExprError("빈 식") p = _Parser(toks) node = p.expr() if p.peek() is not None: raise ExprError(f"식 뒤 남은 토큰 {p.peek()[0]}") return node # ── 셈 ───────────────────────────────────────────────────────────────── def _brackets(node: tuple, env: dict, seen: dict) -> None: """끼움 `[A]` · `[Q=0.208]` 먼저(왼쪽부터 · 원문 치환 차례) — 값은 seen[id].""" kind = node[0] if kind == "끼움": _brackets(node[2], env, seen) value = calc(node[2], env, seen) if node[1]: env["V"][node[1].lower()] = value seen[id(node)] = value elif kind == "셈": _brackets(node[2], env, seen) _brackets(node[3], env, seen) elif kind == "음": _brackets(node[1], env, seen) elif kind == "함수": for arg in node[2]: _brackets(arg, env, seen) def calc(node: tuple, env: dict, seen: dict | None = None) -> float: """나무 → 값 · env = {"V": 변수(소문자), "R": 참조 값, "sub": {토큰 차례: 새 값}}.""" kind = node[0] if kind == "오류": raise ExprError(node[1]) if kind == "수": sub = env.get("sub") return sub[node[2]] if sub and node[2] in sub else node[1] if kind == "이름": if node[1] not in env["V"]: raise ExprError(f"미정의 변수 {node[1]}") return env["V"][node[1]] if kind == "참조": return env.get("R", 1.0) if kind == "끼움": if seen is not None and id(node) in seen: return seen[id(node)] return calc(node[2], env, seen) if kind == "음": return -calc(node[1], env, seen) if kind == "함수": args = [calc(a, env, seen) for a in node[2]] if node[1] == "int": return float(int(args[0])) return float(round(args[0], int(args[1])) if len(args) > 1 else round(args[0])) a, b = calc(node[2], env, seen), calc(node[3], env, seen) op = node[1] if op == "+": return a + b if op == "-": return a - b if op == "*": return a * b if op == "^": try: return float(a**b) except (OverflowError, ZeroDivisionError) as error: raise ExprError("거듭제곱 오류") from error if b == 0: raise ExprError("0 나누기") return a / b def value(node: tuple, env: dict) -> float: """끼움 먼저 · 그 다음 셈.""" seen: dict = {} _brackets(node, env, seen) out = calc(node, env, seen) if isinstance(out, complex): raise ExprError("복소수") return out def numbers(node: tuple) -> list[tuple]: """나무 속 수 마디(왼쪽부터).""" kind = node[0] if kind == "수": return [node] if node[2] is not None else [] if kind == "셈": return numbers(node[2]) + numbers(node[3]) if kind in ("음",): return numbers(node[1]) if kind == "끼움": return numbers(node[2]) if kind == "함수": return [n for a in node[2] for n in numbers(a)] return []