Files
eomsangdonandClaude Opus 5.5 428d35cbd6 feat(793-1): 경쟁사 산근 글 식 파서 · 참조 평가기 · 실무 여덟 파일 지킴 시험
- M02_San_Expr — eval 없는 식 나무 · 셈 · 줄 자리 규칙(버림 · 반올림 · 올림 × 0~3)
- M02_San_Stmate — 숨은 G 열 원문 → 줄 나무(계 5 단 · (-=) · 대입 · [A] · ;; · &L/M/S/X/D& · &P/R& 율 · :=A01 · -> · // 덧글)
- M02_San_Est — 보이는 글 → 식 복원(단가 · 앞줄 · 상수 역할 · 값 줄 숨은 합 · 변수 얼림 · 단가 정체 · 모호 표지)
- M02_San_Eval — STmate 셈(특수 % · 끝전 두 갈래 · 결과금액 조정 · 계약단가) · EST 다시 셈 · 줄마다 검산 · 단가 전파
- M02_San_Book — 같은 책 블록 · 단가표 읽기(읽기만)
- test_793_1 — STmate 233/233 · 금액 줄 1,315 · 계 줄 695 · EST 222/222 · 줄 2,901/2,903 · 모호 9 · 전파 견본 7

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:59:56 +09:00

261 lines
8.7 KiB
Python

"""경쟁사 산근 글 식 — 셈 식 나무 · 평가 · 자리 규칙(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 []