- 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>
261 lines
8.7 KiB
Python
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 []
|