# -*- 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)