merge(800): sub2 800-1 변수 기호 기본값 · sub3 800-2~4 수식 영역 / 정의 영역 합침
- master_equation.export: 기호 씌움(msym.apply) 먼저 → 정의(view.definitions)는 기호 씌운 줄로 · _clean 마지막 - test_800_2: 800-1 기호로 중간 · 표 찾기가 다른 기호 = 다른 정의(옛 「한 줄로 합침」 기대 낡음) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""M01 로직 변수 기호 기본값 전부 채우기 (PLAN 800-1 · 요구 660-1 · 659-2) — 796-3 이어서.
|
||||
|
||||
돌리기: ./venv/Scripts/python.exe resources/master_data/scripts/build_변수기호_800_1.py [--check]
|
||||
- 입력 · 중간 변수 중 기호가 없는 것(이름이 한글 · 영문 낱말)에 `master_symbol` 규칙대로 `변수표` 기호 · 아래 · 위를 채움.
|
||||
원문 기호(796-3) · 사용자가 고친 칸은 덮지 않음 · 한 로직 안 겹치면 아래에 숫자 · 판마다 다시 돌려도 같은 결과.
|
||||
- 찾기 · 요소 · 합 · 결과 변수는 정의 칸이 없어 수식 내보내기가 `master_symbol.apply` 로 씌움(저장 안 함).
|
||||
- 엔진 열쇠 · 식 · 값은 그대로(보이기만). 저장 = M01 저장 길 · `AISLO_MASTER_DIR` 로 사본에 시험.
|
||||
- `--check` = 쓰지 않고 쓸 것이 남았는지만(남았으면 종료 코드 1).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(HERE.parents[2]))
|
||||
import master_equation as meq # noqa: E402
|
||||
import master_symbol as sym # noqa: E402
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as st # noqa: E402
|
||||
|
||||
|
||||
def logic_rows(files: dict[str, dict]) -> list[dict]:
|
||||
return [r for d in files.values() if d.get("그룹") == "로직" for r in d.get("줄", [])]
|
||||
|
||||
|
||||
def needs(name: str) -> bool:
|
||||
"""기호를 정할 이름 — 이미 기호 꼴(영문 · 그리스 ≤ 6 자)이면 그대로."""
|
||||
return not sym.is_symbol(name)
|
||||
|
||||
|
||||
def _hints(rows: list[dict]) -> dict[str, str]:
|
||||
"""변수 이름 → 힌트(고름 > 중간 > 빈)."""
|
||||
rank = {"": 0, "중간": 1, "고름": 2}
|
||||
out: dict[str, str] = {}
|
||||
for r in rows:
|
||||
for kind in ("입력", "중간"):
|
||||
for spec in r.get(kind) or []:
|
||||
now = (
|
||||
"고름"
|
||||
if kind == "입력" and spec.get("고르기")
|
||||
else "중간"
|
||||
if kind == "중간"
|
||||
else ""
|
||||
)
|
||||
name = str(spec["이름"])
|
||||
if name not in out or rank[now] > rank[out[name]]:
|
||||
out[name] = now
|
||||
return out
|
||||
|
||||
|
||||
# ── 정본에 채우기 ──────────────────────────────────────────────────────
|
||||
def plan(files: dict[str, dict]) -> tuple[list[dict], dict]:
|
||||
"""(저장 묶음, 숫자) — 읽기 전용 · 고친 줄만 담음."""
|
||||
batch, stat = [], {"줄": 0, "변수": 0, "겹침비킴": 0}
|
||||
hints = _hints(logic_rows(files))
|
||||
for name, data in files.items():
|
||||
if data.get("그룹") != "로직":
|
||||
continue
|
||||
changes = []
|
||||
for row in data.get("줄", []):
|
||||
new, hit = copy.deepcopy(row), False
|
||||
table = {k: dict(v) for k, v in (new.get(meq.FIELD) or {}).items()}
|
||||
used = {sym_text for v in meq.table(new) for sym_text in (meq.token(v), v["이름"])}
|
||||
for kind in ("입력", "중간"):
|
||||
for spec in new.get(kind) or []:
|
||||
key = str(spec["이름"])
|
||||
look = table.get(key, {})
|
||||
if look.get("기호") or not needs(key):
|
||||
continue
|
||||
sign, low, up = sym.lookup(key, "변수", hints.get(key, ""))
|
||||
if look.get("아래") or look.get("위"):
|
||||
continue # 사용자가 첨자만 정한 칸 — 건드리지 않음
|
||||
one = {**look, "기호": sign, "아래": low}
|
||||
if up:
|
||||
one["위"] = up
|
||||
text = meq.token({"열쇠": key, **one})
|
||||
n, base = 1, low
|
||||
while text in used: # 한 로직 안 겹침 — 아래에 숫자
|
||||
n += 1
|
||||
one["아래"] = base[: sym.SUBMAX - len(str(n))] + str(n)
|
||||
text = meq.token({"열쇠": key, **one})
|
||||
stat["겹침비킴"] += 1
|
||||
used.add(text)
|
||||
table[key] = one
|
||||
stat["변수"] += 1
|
||||
hit = True
|
||||
if not hit:
|
||||
continue
|
||||
new[meq.FIELD] = table
|
||||
changes.append({"op": "edit", "key": row["키"], "row": new})
|
||||
stat["줄"] += 1
|
||||
if changes:
|
||||
batch.append({"file": name, "version": st.read(name)[1], "changes": changes})
|
||||
return batch, stat
|
||||
|
||||
|
||||
def main() -> None:
|
||||
files, _ = st.loaded()
|
||||
batch, stat = plan(files)
|
||||
print(f"쓸 것 — 줄 {stat['줄']} · 변수 {stat['변수']} · 겹침 비킴 {stat['겹침비킴']}")
|
||||
if "--check" in sys.argv:
|
||||
raise SystemExit(1 if batch else 0)
|
||||
if batch:
|
||||
print(st.save(batch))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -31,6 +31,7 @@ import master_cut as mcut
|
||||
import master_equation_view as view
|
||||
import master_formula as mf
|
||||
import master_material as mm
|
||||
import master_symbol as msym
|
||||
|
||||
from common_util.common_util_money import fmt
|
||||
|
||||
@@ -461,12 +462,7 @@ def export(master, key: str, given: dict | None = None, depth: int = 0) -> dict:
|
||||
total = {k: cut.get(k) for k in mf.BUNDLE}
|
||||
if depth == 0:
|
||||
sub = {"소계": subtotals(row, cut, total)}
|
||||
defs = (
|
||||
{"정의": view.definitions(out_lines, row, table(row, master), _unused(row))}
|
||||
if depth == 0
|
||||
else {}
|
||||
)
|
||||
return _clean(
|
||||
out = msym.apply( # 기호 먼저(800-1) → 정의는 기호 씌운 줄로(800-2)
|
||||
{
|
||||
"열쇠": row.get("키"),
|
||||
"이름": row.get("이름") or "",
|
||||
@@ -476,9 +472,11 @@ def export(master, key: str, given: dict | None = None, depth: int = 0) -> dict:
|
||||
"변수표": table(row, master),
|
||||
"합계": total,
|
||||
**sub,
|
||||
**defs,
|
||||
}
|
||||
)
|
||||
if depth == 0:
|
||||
out["정의"] = view.definitions(out_lines, row, out["변수표"], _unused(row))
|
||||
return _clean(out)
|
||||
|
||||
|
||||
def _unused(row: dict):
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user