feat(master_data): 산림품셈 9장 로직 — 돈 로직 27 · 작업량 로직 20

- 로직_산림품셈_9장.json: 굴착기 0.7㎥ 작업량 식 절 · 인력 10%+기계 90% 터파기·되메우기 · 인력 절 · 철근절단 · 면고르기 · 뿌리다듬기 · 제근
- 불도저·플레이트콤펙터 절은 작업량만(기계 요소 없음)
- 계수_산림품셈_9장.json: 절별 굴착기 규격 · 인력·장비 비율 · 9-3-2 주④ f 표 12개
- master_formula: 범위규칙 「이상·미만」 · 분수 글자 값(「1/1.30」) 계산
- 시험: 이상·미만 경계 · 분수 값 · 9장 계수 표 본문 대조

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
This commit is contained in:
2026-09-19 16:14:43 +09:00
co-authored by Claude Opus 5
parent 10ee321a47
commit bb89ffebc0
4 changed files with 1756 additions and 13 deletions
@@ -20,6 +20,7 @@ _NAME = re.compile(r"[\w가-힣]+")
_NUMBER = re.compile(r"\d+(?:\.\d+)?")
_HOLE = re.compile(r"\{([^{}]+)\}")
_OPS = ("<=", ">=", "==", "!=", "<", ">")
_ARITH = re.compile(r"[\d.\s+\-*/()]*\d[\d.\s+\-*/()]*") # 분수·식 값 칸(「1/1.30」)
class FormulaError(ValueError):
@@ -250,9 +251,12 @@ def _match(table: dict, row: dict, cond: str, want) -> bool:
have = row[cond]
if table.get("조건", {}).get(cond) == "범위":
low, high = have
if table.get("범위규칙", "초과·이하") != "초과·이하":
raise FormulaError(f"모르는 범위규칙 「{table.get('범위규칙')}")
return (low is None or want > low) and (high is None or want <= high)
rule = table.get("범위규칙", "초과·이하").split(" · ")[0]
if rule == "초과·이하":
return (low is None or want > low) and (high is None or want <= high)
if rule == "이상·미만":
return (low is None or want >= low) and (high is None or want < high)
raise FormulaError(f"모르는 범위규칙 「{table.get('범위규칙')}")
return have == want
@@ -286,6 +290,8 @@ def evaluate(node, env: dict, master: Master, depth: int = 0):
value = base[node[2]]
if isinstance(value, list):
raise FormulaError(f"칸 「{node[2]}」 값이 범위 — 입력으로 받아야 함")
if isinstance(value, str) and _ARITH.fullmatch(value):
return evaluate(parse(value), env, master, depth)
return value
if kind == "neg":
return -evaluate(node[1], env, master, depth)