Files
eomsangdonandClaude Sonnet 5.5 7191dbabcd 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>
2026-11-07 17:11:44 +09:00

266 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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)