feat(800-1): M01 로직 변수 기호 기본값 전부 — 입력 · 중간 6,664 칸 채움 + 찾기 · 요소 · 합 · 결과 기호 씌움

- master_symbol.py(이름 끝말 → 머리글자 규칙 · 아래첨자 · 갈래별 찾기 위 「표」 · 요소 P · w · 합 C · 결과 P · Q · 한 로직 안 겹침 숫자) + build_변수기호_800_1.py(--check · 채우기만 · 원문 796-3 · 사용자 고침 안 덮음)
- 로직 줄 변수표 6,664 칸(1,403 로직) · master_equation.export 끝 한 줄 msym.apply(보이기만 · 저장 안 함) · 셈 열쇠 · 식 · 값 그대로(1,519 로직 셈 지문 전후 같음)
- 검토 목록 xlsx 첫 시트 「기호 기본값(800-1)」 · 읽는 법(관례 줄 위 · 원문 줄 아래)
- test_800_1_var_symbols 8 · 지문 파일 마스터 해시만 갱신(결과 같음)
- 낡은 시험 고침: test_796_1(작업시간 T · 운반량 Q · 운반회수 n · 운반량 N 겹침은 기본 아래 비움) · test_796_2b(형틀목공 입력글 Q_형틀목공) — 기호 기본값이 생겨 기대 글이 바뀜

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-07 17:11:44 +09:00
co-authored by Claude Sonnet 5.5
parent e4394d2cd8
commit 7191dbabcd
66 changed files with 20225 additions and 55877 deletions
@@ -0,0 +1,265 @@
# -*- coding: utf-8 -*-
"""변수 기호 기본값 — 국문 이름 → 기호 · 아래 · 위 (PLAN 800-1 · 요구 660-1 · 659-2).
보이기만 — 셈 · 엔진 열쇠 · 식에 안 씀. 기호 정하는 틀:
- 이름 끝말로 머리글자(관례): 율 r · 계수 k · 효율 E · 단가 P · 노임 w · 시간 T · 횟수 n · 개수 N ·
수량 · 소요량 · 품 Q · 길이 · 거리 L · 높이 H · 폭 B · 두께 t · 면적 A · 부피 V · 무게 W · 지름 D …
- 아래첨자 = 이름에서 끝말을 뺀 알맹이(6 자까지) · 같은 이름 = 같은 기호(로직이 달라도).
- 갈래별: 찾기(표 칸) = 위 「표」 · 요소 단가 P · 노임 w · 합(금액 줄) C · 결과 = 단위가 원이면 P 아니면 Q.
- 규칙만(자료 파일 없음) — 한 로직 안 겹침은 쓰는 쪽(빌더 · 내보내기)이 아래첨자 숫자로 가름.
- 이미 기호 꼴(영문 · 그리스 ≤ 6 자)이거나 사용자가 정한 기호는 건드리지 않음.
"""
from __future__ import annotations
import re
SYMBOL = re.compile(r"^[A-Za-zα-ωΑ-Ω][A-Za-z0-9′']{0,5}(?:_[A-Za-z0-9′']{1,6})?$") # 이미 기호 꼴
LATIN_HEAD = re.compile(r"^([A-Za-zα-ω]{1,2})([가-힣].*)$") # 「E입력」 「f분류」 = 기호 + 말
FAMILY = ("변수", "찾기", "요소", "합", "결과")
SUBMAX = 6 # master_equation_edit.SUB_FORM 과 같게
# (끝말들, 머리글자) — 먼저 맞는 것이 이김(긴 끝말 · 특수 먼저)
SUFFIX: list[tuple[tuple[str, ...], str]] = [
(("배율", "계수", "상수"), "k"),
(("중량", "무게"), "W"),
(("용량", "능력", "출력"), "q"),
(("효율",), "E"),
(
(
"함수비",
"배합비",
"용적비",
"인력비",
"기계비",
"감률",
"증가율",
"할증율",
"가산율",
"비율",
),
"r",
),
(("율", "률", "할증", "가산", "증감", "가감", "감산", "증가", "보정", "감"), "r"),
(("노임",), "w"),
(("단가", "가격"), "P"),
(("재료비", "노무비", "경비", "손료", "비용"), "C"),
(("시간",), "T"),
(("일수", "기간", "연수"), "d"),
(("횟수", "회수"), "n"),
(("품",), "Q"),
(("수량", "소요량", "사용량", "적재량", "용수", "량"), "Q"),
(("개수", "대수", "본수", "매수", "점수", "단수", "층수", "인원", "수"), "N"),
(("거리", "길이", "연장", "간격"), "L"),
(("반경",), "R"),
(("폭",), "B"),
(("높이", "깊이", "심도", "표고", "수심", "수고", "수직고", "직고"), "H"),
(("두께",), "t"),
(("면적",), "A"),
(("부피", "체적", "용적", "재적"), "V"),
(("직경", "지름", "치수", "경"), "D"),
(("속도", "유속"), "v"),
(("경사도", "경사", "구배", "기울기"), "i"),
(("각도",), "θ"),
(("강도",), "σ"),
(("밀도",), "ρ"),
(("압력",), "p"),
(
(
"규격",
"종류",
"구분",
"방식",
"방법",
"유형",
"조건",
"상태",
"분류",
"형식",
"형태",
"지역",
"종별",
"재질",
"직종",
"기종",
"등급",
"구조",
"토질",
"지반",
"대상",
"위치",
"장소",
"부위",
"명칭",
),
"S",
),
(("인부", "기사", "기능사", "기술자", "기술인", "조수", "반장", "신호수", "측량사"), "Q"),
]
EXACT = { # 끝말 틀에 안 맞는 이름
"공수": ("Q", ""),
"총품": ("Q", ""),
"방폭": ("S", "방폭"),
"급수": ("S", "급수"),
"세밀도": ("S", "세밀도"),
"완제품": ("S", "완제품"),
"단위용수": ("Q", "단위용수"),
}
TRADE_END = ("공", "원", "사", "부") # 일꾼 직종 끝말(3 자 이상 이름만)
SHORT = ( # 긴 말 줄임
("플랜트", "플"),
("건설기계", "기계"),
("크롤러", "크롤"),
("전기용접기", "전용"),
("산소용접기", "산용"),
("콘크리트", "콘"),
("트럭탑재형", "트탑"),
("조종원", "조종"),
("작업", "작"),
)
FALLBACK = {"변수": "X", "찾기": "Q", "요소": "P", "합": "C", "결과": "Q"}
def short(text: str) -> str:
"""아래첨자 글 — 낱말 아닌 글 걷고 첫 낱말(+ 이어지는 낱말 · 크기 숫자) 6 자 안으로."""
parts = [p for p in (re.sub(r"[^\w′']|_", "", p) for p in text.split()) if p]
if not parts:
return "·"
out = parts[0]
for part in parts[1:]:
num = re.match(r"\d+", part)
if len(out + part) <= SUBMAX:
out += part
elif num and len(out + num.group()) <= SUBMAX:
out += num.group()
break
for long, cut in SHORT:
if len(out) > SUBMAX:
out = out.replace(long, cut)
return out[:SUBMAX]
def _norm(name: str) -> str:
"""분석용 이름 — 「입력」 「(…)」 걷기."""
out = re.sub(r"\(.*?\)", "", name)
out = re.sub(r"입력$", "", out) if len(out) > 3 else out
return out.strip()
def rule(name: str, family: str = "변수", hint: str = "") -> tuple[str, str, str, str]:
"""이름 → (기호, 아래, 위, 규칙). hint: 「고름」 = 고르기 입력 · 「중간」."""
up = "표" if family == "찾기" else ""
if family == "결과":
return ("P" if "원" in name else "Q"), "", "", "결과 단위"
m = LATIN_HEAD.match(name)
if m:
return m.group(1), short(m.group(2)), up, "앞 영문 = 기호"
base = _norm(name) or name
if base in EXACT:
letter, stem = EXACT[base]
return letter, short(stem or base), up, "이름 따로"
if family == "요소" and base.endswith("노임"):
return "w", short(base[:-2] or base), up, "노임 w"
for kws, letter in SUFFIX:
for kw in kws:
if base.endswith(kw) and len(base) >= len(kw) + (len(kw) < 2):
stem = base[: -len(kw)] if len(base) > len(kw) and letter != "S" else base
if letter == "Q" and kw in (
"인부",
"기사",
"기능사",
"기술자",
"기술인",
"조수",
"반장",
):
stem = base
if family == "합":
return "C", short(base), up, "금액 C"
return letter, short(stem), up, f"끝말 「{kw}」 → {letter}"
if len(base) >= 3 and base.endswith(TRADE_END) and family != "합":
return "Q", short(base), up, "직종 → Q(품)"
if family == "합":
return "C", short(base), up, "금액 C"
letter = "S" if hint == "고름" else "Q" if hint == "중간" else FALLBACK[family]
return letter, short(base), up, "끝말 없음 — 기본"
def lookup(name: str, family: str = "변수", hint: str = "") -> tuple[str, str, str]:
return rule(name, family, hint)[:3]
def is_symbol(sign: str) -> bool:
return bool(SYMBOL.match(sign or ""))
# ── 수식 내보내기 결과에 씌우기(찾기 · 요소 · 합 · 결과 — 정의 칸이 없는 것) ─────────────
def family_name(node: dict) -> tuple[str, str, str] | None:
"""변수 마디 → (갈래, 이름, 같은 것 가름 열쇠) — 찾기 · 요소 · 합만 · 아니면 None."""
kind = node.get("갈래")
if kind not in ("찾기", "요소", "합") or "기호" not in node or node.get("열쇠") == "결과":
return None
if kind == "찾기": # 칸이 같아도 표 · 조건이 다르면 다른 변수
return kind, str(node.get("칸") or ""), str(node.get("열쇠"))
name = str(node.get("이름") or node.get("열쇠") or "")
if kind == "요소":
if name.startswith("줄."): # 다른 호표 줄 금액 참조
name = name[2:].strip("'")
return "합", name, name
if m := re.match(
r"^[A-Z]{1,3}:\{(.+?)\}\.?(.*)$", name
): # EQ:{기계}.조종원 — 풀리지 않은 열쇠
return kind, m.group(1) + m.group(2).strip("'"), name
return kind, name, name
def _text(sign: str, low: str, up: str) -> str:
return sign + (f"_{low}" if low else "") + (f"^{up}" if up else "")
def apply(out: dict) -> dict:
"""내보낸 수식(export 결과) 안 변수 마디 기호가 한글 그대로면 기본 기호로 — 기호 꼴 · 고친 것은 그대로.
한 내보내기(= 한 로직) 안에서 다른 이름이 같은 글이 되면 뒤 것 아래에 숫자(`Q_크레인2`).
"""
seen: dict[str, tuple[str, str]] = {} # 글 → (갈래, 이름)
for v in out.get("변수표") or []:
seen.setdefault(
_text(str(v.get("기호") or ""), str(v.get("아래") or ""), str(v.get("위") or "")),
("변수", str(v.get("열쇠"))),
)
chosen: dict[tuple[str, str], tuple[str, str, str]] = {}
def pick(fam: str, name: str, got: tuple[str, str, str]) -> tuple[str, str, str]:
one = chosen.get((fam, name))
if one is None:
sign, low, up = got
n, base = 1, low
while seen.get(_text(sign, low, up), (fam, name)) != (fam, name):
n += 1
low = base[: SUBMAX - len(str(n))] + str(n)
seen[_text(sign, low, up)] = (fam, name)
one = chosen[(fam, name)] = (sign, low, up)
return one
for line in out.get("줄") or []:
head = line.get("결과") if line.get("갈래") == "결과" else None
if isinstance(head, dict) and not is_symbol(str(head.get("기호") or "")):
head["기호"], head["아래"], head["위"] = pick(
"결과", "결과", rule(out.get("결과단위") or "", "결과")[:3]
)
_walk(out, pick)
return out
def _walk(tree, pick) -> None:
if isinstance(tree, list):
for one in tree:
_walk(one, pick)
elif isinstance(tree, dict):
got = family_name(tree)
if got and got[1] and not is_symbol(str(tree["기호"])):
tree["기호"], tree["아래"], tree["위"] = pick(got[0], got[2], lookup(got[1], got[0]))
for v in tree.values():
if isinstance(v, (dict, list)):
_walk(v, pick)