merge(master_data): dev 받기 · 공통8장 기계 고르기 · 재료_자체 손 병합

- 로직_건설품셈_공통8장: 양쪽 고르기 합침
- 재료_자체: 양쪽 줄 합침 · dev 쪽 겹친 열쇠 12개 한 줄로(비고 합침)
- 로직_건설품셈_유지관리1장: 3-7-5 편성 칸 이름 크롤러드릴_공기식

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 19:07:46 +09:00
co-authored by Claude Opus 5
30 changed files with 38264 additions and 83 deletions
@@ -104,9 +104,16 @@ class _Parser:
return ("neg", self.unary())
node = self.primary()
while self.take("."):
node = ("field", node, self.name())
node = ("field", node, self.quoted() if self.take("'") else self.name())
return node
def quoted(self) -> str:
end = self.s.find("'", self.i)
if end < 0:
self.fail("작은따옴표 안 닫힘")
text, self.i = self.s[self.i : end], end + 1
return text
def name(self) -> str:
self.ws()
m = _NAME.match(self.s, self.i)
@@ -115,8 +122,9 @@ class _Parser:
self.i = m.end()
return m.group()
def ref(self) -> str:
"""열쇠 글자 — 괄호 밖의 `,` · `)` · `.칸` · ` 연산자 ` 앞에서 멈춤(열쇠에 빈칸·괄호가 들어감)."""
def ref(self, head: bool = False) -> str:
"""열쇠 글자 — 괄호 밖의 `,` · `)` · `.칸` · ` 연산자 ` 앞에서 멈춤(열쇠에 빈칸·괄호가 들어감).
head = 찾기·로직 머리 — `,` · `)` 에서만 멈춤(표 이름의 `P.P마대` 같은 점)."""
start, depth = self.i, 0
while self.i < len(self.s):
ch = self.s[self.i]
@@ -128,6 +136,8 @@ class _Parser:
depth -= 1
elif depth == 0 and ch == ",":
break
elif head:
pass
elif depth == 0 and ch == "." and not self.s[self.i + 1 : self.i + 2].isdigit():
# 「K.P」「T.S」 처럼 영문자 사이 점은 열쇠 글자
around = self.s[self.i - 1 : self.i + 2 : 2]
@@ -141,7 +151,7 @@ class _Parser:
def args(self) -> list[tuple[str, object]]:
out = []
while self.take(","):
key = self.name()
key = self.quoted() if self.take("'") else self.name()
self.need("=")
out.append((key, self.expr()))
self.need(")")
@@ -168,10 +178,10 @@ class _Parser:
self.need("(")
head = self.name()
self.need(":")
return ("find", f"{head}:{self.ref()}", self.args())
return ("find", f"{head}:{self.ref(True)}", self.args())
if word == "로직":
self.need("(")
return ("logic", self.ref(), self.args())
return ("logic", self.ref(True), self.args())
if word in FUNCS:
self.need("(")
items = [self.expr()]
@@ -269,20 +279,24 @@ def _match(table: dict, row: dict, cond: str, want) -> bool:
)
if rule == "이하": # 칸마다 위 끝만 — 맞는 줄 가운데 위 끝이 가장 작은 줄(find)
return (low is None or want > low) and (high is None or want <= high)
if rule == "이상·이하": # 양 끝 포함 — 이웃 칸 끝이 겹치면 위 끝이 가장 작은 줄(find)
return (low is None or want >= low) and (high is None or want <= high)
raise FormulaError(f"모르는 범위규칙 「{table.get('범위규칙')}")
return have == want
def find(table: dict, conds: dict) -> dict:
def find(table: dict, conds: dict, want: str | None = None) -> dict:
for cond in conds:
if cond not in table.get("조건", {}):
raise FormulaError(f"{table['열쇠']}」 에 없는 조건 「{cond}")
hits = [row for row in table[""] if all(_match(table, row, c, v) for c, v in conds.items())]
if len(hits) > 1 and table.get("범위규칙", "").split(" · ")[0] == "이하":
if len(hits) > 1 and table.get("범위규칙", "").split(" · ")[0] in ("이하", "상·이"):
for c in conds:
ups = [r[c][1] for r in hits if table["조건"].get(c) == "범위" and r.get(c)]
if ups and None not in ups:
hits = [r for r in hits if not r.get(c) or r[c][1] == min(ups)]
if len(hits) > 1 and want: # 여러 줄이면 찾는 값 칸이 있는 줄만
hits = [row for row in hits if want in row]
if len(hits) != 1:
raise FormulaError(f"{table['열쇠']}{conds} 맞는 줄 {len(hits)}")
return hits[0]
@@ -304,7 +318,12 @@ def evaluate(node, env: dict, master: Master, depth: int = 0):
raise FormulaError(f"{node[1]}」 값이 범위 — 입력으로 받아야 함")
return value
if kind == "field":
base = evaluate(node[1], env, master, depth)
if node[1][0] == "find":
table = master.get(fill(node[1][1], env))
conds = {k: evaluate(v, env, master, depth) for k, v in node[1][2]}
base = find(table, conds, node[2])
else:
base = evaluate(node[1], env, master, depth)
if not isinstance(base, dict) or node[2] not in base:
raise FormulaError(f"칸 「{node[2]}」 없음")
value = base[node[2]]