feat(785-1): 출처 풀이 · 조항 색인 새 M02_Hopyo_Clause — 블록 근거(머리 › 몸 › 절 제목) · 같음 › 윗마디 · 절마다 나눔 · 결과단위 거름 · 준용 · 옛 번호 · 기초 없음 · 번호 없음 표지
- Basis 앞 몫(_ours · ours · clause · road_map · _grams · _sections · _named · _part · resolve · settle · has_logic · _has · no_basis) Clause 로 옮김 · Basis = 다시 내보내는 얇은 껍데기(순위 몫 candidates · merge · pick 은 785-7 까지 남김) - Excel: settle 부름 Clause 로 · ReadEst 별칭 hopyo_basis → hopyo_est - 새 지킴 시험 test_642_1_clause_formula(설계 642-A 5-3 첫 줄 기대 — 단산 10 · 46 · 24 · 47 · 39 후보 · 33 준용 · 6 옛 · 3 번호 없음 · 절 나무 · 상수 · 대입식 · 절 몫) - test_628: hb = Clause(정본) · 여덟 파일 표지 수 「기초 없음」 18 → 3 · 「옛 번호」 121 → 120 — 782-1 · 782-2 · 782-3 새 M01 기초로 낡은 수(옛 Basis 로도 같은 실패 확인) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,9 +3,8 @@
|
||||
|
||||
읽기(`read` · `sources` · `trade_names` · `SHEET` · `GLINES` · `GCHARS`) = EST 어댑터
|
||||
`M02_Hopyo_ReadEst`(776-2 · 이름은 그대로 다시 냄).
|
||||
출처 풀이(`settle` · 781-1) = 건설품셈(부문 모름) 번호 → 뜻 낱말로 부문 ·
|
||||
임도품셈(옛) → 대응표 현행 산림 번호 ·
|
||||
기초 없음 표지(`no_basis` · 781-2) = 출처 조항에 우리 로직 0 · 옛 번호.
|
||||
출처 풀이 · 조항 · 기초 없음(`settle` · `resolve` · `ours` · `clause` · `no_basis` …) = 785-1 부터
|
||||
`M02_Hopyo_Clause` 정본(여기는 다시 냄 · 옛 맞춤 층이 지워질 때 785-7 이 걷음).
|
||||
견줌(`candidates`) = 같은 책(부문을 알면 같은 구분) 조항 같음 1.0 ·
|
||||
윗마디 0.9(엑셀 6-2 ⊃ 우리 6-2-2) ·
|
||||
앞 마디 같음 0.5(조항이 안 맞으면 후보 아님 · `clause`) · 조항 오타 「13-114」 = 「13-11-4」 ·
|
||||
@@ -19,15 +18,27 @@ M01 · 원본 로직은 읽기만.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import re
|
||||
from functools import lru_cache
|
||||
from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M01_MasterData import M01_MasterData_Store_Make as make
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hopyo_match # 서로 부름 — 부를 때만 씀
|
||||
from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs
|
||||
from M02_MasterTemplete.M02_Hopyo_Clause import ( # noqa: F401 — 785-1 정본 · 옛 이름 그대로
|
||||
OLD,
|
||||
_flat,
|
||||
_grams,
|
||||
_has,
|
||||
_head,
|
||||
_named,
|
||||
_ours,
|
||||
_part,
|
||||
_sections,
|
||||
clause,
|
||||
has_logic,
|
||||
no_basis,
|
||||
ours,
|
||||
resolve,
|
||||
road_map,
|
||||
settle,
|
||||
)
|
||||
from M02_MasterTemplete.M02_Hopyo_ReadEst import ( # noqa: F401 — 읽기는 EST 어댑터(776-2) · 옛 이름 그대로
|
||||
GCHARS,
|
||||
GLINES,
|
||||
@@ -43,208 +54,6 @@ _TYPOS = (("깍기", "깎기"),)
|
||||
_PLAIN_WORDS = frozenset({"인력", "기계", "장비"}) # 방법 낱말 — 「터널굴착 1발파당 인력」
|
||||
|
||||
|
||||
def _flat(value: Any) -> str:
|
||||
return re.sub(r"\s+", "", str(value or ""))
|
||||
|
||||
|
||||
# ── 우리 쪽 조항 · 직종 ────────────────────────────────────────────────
|
||||
@lru_cache(maxsize=2)
|
||||
def _ours(whole_id: int) -> dict[str, dict[str, Any]]:
|
||||
"""{로직 키: {책, 부문, 구분, 조항: {절 번호}, 직종: {이름}}} — 조항 = `sources()` 묶음 ·
|
||||
직종 = 호표 인력(LB) 요소."""
|
||||
files, whole = m01.loaded()
|
||||
lb = {str(row.get("키")): str(row.get("이름")) for row in whole.numbers.get("LB", {}).values()}
|
||||
out = {}
|
||||
for data in files.values():
|
||||
if data.get("그룹") != "로직":
|
||||
continue
|
||||
for row in data.get("줄") or []:
|
||||
kind = str(row.get("구분") or "")
|
||||
book, _, part = kind.partition(" ")
|
||||
trades = {
|
||||
lb[str(h.get("요소"))]
|
||||
for h in row.get("호표") or []
|
||||
if isinstance(h, dict) and str(h.get("요소")) in lb
|
||||
}
|
||||
clauses = set(labor_inputs._NUMBER.findall(str(labor_inputs.sources(row) or "")))
|
||||
out[str(row.get("키"))] = {
|
||||
"책": book,
|
||||
"부문": part,
|
||||
"구분": kind,
|
||||
"조항": clauses,
|
||||
"직종": trades,
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
def ours() -> dict[str, dict[str, Any]]:
|
||||
return _ours(id(m01.loaded()[1]))
|
||||
|
||||
|
||||
def _head(number: str) -> str:
|
||||
return number.rsplit("-", 1)[0]
|
||||
|
||||
|
||||
def clause(theirs: list[list[str]], our: dict[str, Any]) -> tuple[float, list[str], str]:
|
||||
"""엑셀 인용 조항 `[[책, 번호]]` ↔ 우리 로직(`ours()` 한 줄) → (점수, 맞은 우리 조항 글,
|
||||
꼬리표) — 같은 책(부문을 알면 같은 구분 · 「(옛)」 = 없음) · 같음 1 ·
|
||||
윗마디 0.9(엑셀 6-2 ⊃ 우리 6-2-2) · 앞 마디 0.5."""
|
||||
nums = [n for book, n in theirs if book in (our["책"], our["구분"])]
|
||||
same = sorted(n for n in nums if n in our["조항"])
|
||||
if same:
|
||||
return 1.0, [f"{our['구분']} {n}" for n in same], "조항 같음"
|
||||
kids = sorted({c for c in our["조항"] for n in nums if c.startswith(n + "-")})
|
||||
if kids:
|
||||
return 0.9, [f"{our['구분']} {n}" for n in kids], "윗마디 같음"
|
||||
heads = {_head(n) for n in nums}
|
||||
near = sorted(n for n in our["조항"] if _head(n) in heads)
|
||||
if near:
|
||||
return 0.5, [f"{our['구분']} {n}" for n in near], "앞 마디 같음"
|
||||
return 0.0, [], ""
|
||||
|
||||
|
||||
# ── 출처 풀이(781-1 · 781-2) — 건설 부문 · 임도 옛 번호 대응 · 기초 없음 ───────────
|
||||
OLD = "(옛)"
|
||||
_ROAD = "임도품셈" + OLD
|
||||
_ROAD_MAP = make.BODY.parent / "경쟁사 프로그램/STmate 분석/30_원자료/임도품셈_판_대응표.csv"
|
||||
_OLD_NO = re.compile(r"^(\d+(?:-\d+)+)(\s+[가-하])?$")
|
||||
_STOP_GRAMS = frozenset("설치 해체 작업 인력 기계 장비 보완 신설 공사".split())
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def road_map() -> dict[str, str]:
|
||||
"""임도품셈(옛) 번호 → 현행 산림 번호 — 지식DB 대응표 「확정」 한 대응만(일대다 · 미확인 뺌) ·
|
||||
옛 번호가 가 · 나 항이면 현행 윗마디(9-13-7 → 9-13 · 그 항만 대응하므로)."""
|
||||
with open(_ROAD_MAP, encoding="utf-8-sig", newline="") as f:
|
||||
rows = list(csv.DictReader(f))
|
||||
out = {}
|
||||
for row in rows:
|
||||
old, new = _OLD_NO.match(row["과거_절"].strip()), row["현행_절"].strip()
|
||||
if row["판정"] != "확정" or not old or not _OLD_NO.fullmatch(new) or " " in new:
|
||||
continue
|
||||
out[old.group(1)] = new.rsplit("-", 1)[0] if old.group(2) and new.count("-") > 1 else new
|
||||
return out
|
||||
|
||||
|
||||
def _grams(text: Any) -> set[str]:
|
||||
"""뜻 낱말 — 낱말 안 한글 두 글자 묶음(괄호 글 포함 · 굴삭 = 굴착)."""
|
||||
words = re.findall(r"[가-힣]+", str(text or "").replace("굴삭", "굴착"))
|
||||
return {w[i : i + 2] for w in words for i in range(len(w) - 1)} - _STOP_GRAMS
|
||||
|
||||
|
||||
@lru_cache(maxsize=2)
|
||||
def _sections(book: str) -> dict[tuple[str, str], str]:
|
||||
"""{(부문, 절 번호): 제목(괄호 글 · 「:」 뒤 뺌)} — 원문 본문."""
|
||||
return {
|
||||
(s["division"], s["section"]): re.sub(r"\(.*?\)", "", s["title"]).split(":")[0].strip()
|
||||
for s in make._all_sections(book)
|
||||
}
|
||||
|
||||
|
||||
@lru_cache(maxsize=2)
|
||||
def _named(whole_id: int) -> dict[tuple[str, str], list[str]]:
|
||||
"""{(구분, 원문번호): [로직 이름]} — M01 로직."""
|
||||
out: dict[tuple[str, str], list[str]] = {}
|
||||
for data in m01.loaded()[0].values():
|
||||
if data.get("그룹") == "로직":
|
||||
for row in data.get("줄") or []:
|
||||
key = (str(row.get("구분") or ""), str(row.get("원문번호") or "").split(" ")[0])
|
||||
out.setdefault(key, []).append(str(row.get("이름") or ""))
|
||||
return out
|
||||
|
||||
|
||||
def _part(number: str, name: Any, text: Any = "") -> str:
|
||||
"""건설품셈(부문 모름) 번호 → 원문에 그 번호가 있는 부문 중 블록 이름(안 맞으면 블록 글 ·
|
||||
두 묶음)이 원문 제목 · 같은 번호 로직 이름과 뜻 낱말을 나누는 부문이 하나 = 그 부문 ·
|
||||
둘 이상 = 모름 · 없음 = 「(옛)」(번호 뜻 안 맞음)."""
|
||||
if not _grams(name):
|
||||
return "건설품셈"
|
||||
named = _named(id(m01.loaded()[1]))
|
||||
texts = {
|
||||
part: [title, *named.get((f"건설품셈 {part}", no), [])]
|
||||
for (part, no), title in _sections("건설품셈").items()
|
||||
if no == number
|
||||
}
|
||||
hits = []
|
||||
for mine, least in ((_grams(name), 1), (_grams(text), 2)): # 글은 두 묶음 이상
|
||||
hits = [p for p, ts in texts.items() if any(len(mine & _grams(t)) >= least for t in ts)]
|
||||
if hits:
|
||||
break
|
||||
if len(hits) == 1:
|
||||
return f"건설품셈 {hits[0]}"
|
||||
return "건설품셈" if hits else "건설품셈" + OLD
|
||||
|
||||
|
||||
def resolve(theirs: list[list[str]], name: Any, text: Any = "") -> list[list[str]]:
|
||||
"""블록 출처 → 부문 풀이 · 임도 옛 번호 대응(`road_map`) 뒤 출처."""
|
||||
out: list[list[str]] = []
|
||||
for book, number in theirs:
|
||||
one = [book, number]
|
||||
if book == _ROAD and number in road_map():
|
||||
one = ["산림품셈", road_map()[number]]
|
||||
elif book == "건설품셈":
|
||||
one = [_part(number, name, text), number]
|
||||
if one not in out:
|
||||
out.append(one)
|
||||
return out
|
||||
|
||||
|
||||
def settle(book: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Book 단산 블록 출처를 제자리에서 풀이(줄 `근거.출처` 가 같은 리스트를 가리킴)."""
|
||||
for block in book.get("단산") or []:
|
||||
if block.get("출처"):
|
||||
text = " ".join(str(t) for t in block.get("글") or [])
|
||||
block["출처"][:] = resolve(block["출처"], block.get("이름"), text)
|
||||
return book
|
||||
|
||||
|
||||
def has_logic(kind: str, number: str) -> bool:
|
||||
"""그 구분 · 조항(윗마디 · 아랫마디 포함)에 M01 로직이 있나."""
|
||||
return _has(id(m01.loaded()[1]), kind, number)
|
||||
|
||||
|
||||
@lru_cache(maxsize=512)
|
||||
def _has(whole_id: int, kind: str, number: str) -> bool:
|
||||
named = _named(whole_id)
|
||||
parts = number.split("-")
|
||||
ups = {"-".join(parts[:i]) for i in range(2, len(parts) + 1)}
|
||||
return any(k == kind and (no in ups or no.startswith(number + "-")) for k, no in named)
|
||||
|
||||
|
||||
def no_basis(theirs: list | None) -> dict[str, str] | None:
|
||||
"""출처 조항이 모두 우리 기초 밖이면 표지 `{판정, 글, 이름}` — 기초 없음(로직 0) › 옛 번호
|
||||
(「(옛)」 · 지금 원문에 없는 번호) · 로직 있는 조항 · 건설품셈(부문 모름)이 하나라도
|
||||
있으면 None."""
|
||||
none, old = [], []
|
||||
for book, number in theirs or []:
|
||||
base, _, part = book.partition(" ")
|
||||
if book == "건설품셈":
|
||||
return None
|
||||
if book.endswith(OLD) or base not in make.BOOKS:
|
||||
old.append((book.removesuffix(OLD), number))
|
||||
elif has_logic(book, number):
|
||||
return None
|
||||
elif (title := _sections(base).get((part, number))) is None:
|
||||
old.append((book, number))
|
||||
else:
|
||||
none.append((book, number, title))
|
||||
if none:
|
||||
book, number, title = none[0]
|
||||
where = f"{book.replace('품셈', '')} {number} {title}"
|
||||
return {
|
||||
"판정": "기초 없음",
|
||||
"글": f"근거 조항 우리 기초에 없음 — {book} {number} {title}",
|
||||
"이름": f"기초 없음 — {where}",
|
||||
}
|
||||
if old:
|
||||
return {
|
||||
"판정": "옛 번호",
|
||||
"글": f"옛 번호 — 대응 미확인 — {old[0][0]} {old[0][1]}",
|
||||
"이름": "옛 번호 — 근거 못 씀",
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def _text(line: dict[str, Any]) -> str:
|
||||
text = f"{line.get('이름') or ''} {line.get('규격') or ''}"
|
||||
for old, new in _TYPOS:
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
"""M02 일위대가 실무자료 단가산출 출처 풀이 · 조항 색인(PLAN 785-1 · 요구 642 · 설계 642-A ④-1) —
|
||||
엑셀 「단가산출」 블록 품셈 번호 → M01 로직 후보(색인 · 순위 아님).
|
||||
|
||||
출처 풀이(`settle` · 781-1 · 옛 `M02_Hopyo_Basis` 에서 옮김) = 건설품셈(부문 모름) 번호 →
|
||||
뜻 낱말로 부문 · 임도품셈(옛) → 대응표 현행 산림 번호 ·
|
||||
기초 없음 표지(`no_basis` · 781-2) = 출처 조항에 우리 로직 0 · 옛 번호.
|
||||
조항 색인(`index`) = M01 로직 {(구분, 원문번호): [키]}.
|
||||
블록 근거(`basis`) = 머리 줄(첫 절 앞) 출처 › 머리에 품셈 글이 없으면 몸 글 출처(절 제목 뺌) ·
|
||||
절 제목 출처(「4. 기초지정[2025년 표준품셈3-4-8]」)는 그 절 후보 ·
|
||||
색인: 같음 › 윗마디(엑셀 6-2 ⊃ 6-2-2 · 6-2-3) · 부문 모르는 건설품셈 = 건설 부문 모두 ·
|
||||
옛 책 = 없음 · 윗마디 후보 여럿 = 일 절(`Formula.works`) 제목 ↔ 후보 이름 뜻 낱말 많이
|
||||
겹친 것을 그 절에(안 맞는 후보 뺌) ·
|
||||
결과단위 거름 = 후보 결과단위 성격(면적 · 체적 · 길이 · 무게 · 식) ≠ 블록 단위 성격이면 뺌 ·
|
||||
표지 = 번호 없음(머리 · 몸에 번호 없음) › 준용(머리 글 「준용」 · 후보 참고만) ›
|
||||
옛 번호 · 기초 없음(`no_basis`).
|
||||
자동 확정 없음 · M01 · 원본 로직은 읽기만.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import re
|
||||
from functools import lru_cache
|
||||
from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M01_MasterData import M01_MasterData_Store_Make as make
|
||||
from M02_MasterTemplete import M02_Hopyo_Formula as formula
|
||||
from M02_MasterTemplete import M02_Hopyo_ReadEst as est
|
||||
from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs
|
||||
|
||||
|
||||
def _flat(value: Any) -> str:
|
||||
return re.sub(r"\s+", "", str(value or ""))
|
||||
|
||||
|
||||
# ── 우리 쪽 조항 · 직종 ────────────────────────────────────────────────
|
||||
@lru_cache(maxsize=2)
|
||||
def _ours(whole_id: int) -> dict[str, dict[str, Any]]:
|
||||
"""{로직 키: {책, 부문, 구분, 조항: {절 번호}, 직종: {이름}}} — 조항 = `sources()` 묶음 ·
|
||||
직종 = 호표 인력(LB) 요소."""
|
||||
files, whole = m01.loaded()
|
||||
lb = {str(row.get("키")): str(row.get("이름")) for row in whole.numbers.get("LB", {}).values()}
|
||||
out = {}
|
||||
for data in files.values():
|
||||
if data.get("그룹") != "로직":
|
||||
continue
|
||||
for row in data.get("줄") or []:
|
||||
kind = str(row.get("구분") or "")
|
||||
book, _, part = kind.partition(" ")
|
||||
trades = {
|
||||
lb[str(h.get("요소"))]
|
||||
for h in row.get("호표") or []
|
||||
if isinstance(h, dict) and str(h.get("요소")) in lb
|
||||
}
|
||||
clauses = set(labor_inputs._NUMBER.findall(str(labor_inputs.sources(row) or "")))
|
||||
out[str(row.get("키"))] = {
|
||||
"책": book,
|
||||
"부문": part,
|
||||
"구분": kind,
|
||||
"조항": clauses,
|
||||
"직종": trades,
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
def ours() -> dict[str, dict[str, Any]]:
|
||||
return _ours(id(m01.loaded()[1]))
|
||||
|
||||
|
||||
def _head(number: str) -> str:
|
||||
return number.rsplit("-", 1)[0]
|
||||
|
||||
|
||||
def clause(theirs: list[list[str]], our: dict[str, Any]) -> tuple[float, list[str], str]:
|
||||
"""엑셀 인용 조항 `[[책, 번호]]` ↔ 우리 로직(`ours()` 한 줄) → (점수, 맞은 우리 조항 글,
|
||||
꼬리표) — 같은 책(부문을 알면 같은 구분 · 「(옛)」 = 없음) · 같음 1 ·
|
||||
윗마디 0.9(엑셀 6-2 ⊃ 우리 6-2-2) · 앞 마디 0.5."""
|
||||
nums = [n for book, n in theirs if book in (our["책"], our["구분"])]
|
||||
same = sorted(n for n in nums if n in our["조항"])
|
||||
if same:
|
||||
return 1.0, [f"{our['구분']} {n}" for n in same], "조항 같음"
|
||||
kids = sorted({c for c in our["조항"] for n in nums if c.startswith(n + "-")})
|
||||
if kids:
|
||||
return 0.9, [f"{our['구분']} {n}" for n in kids], "윗마디 같음"
|
||||
heads = {_head(n) for n in nums}
|
||||
near = sorted(n for n in our["조항"] if _head(n) in heads)
|
||||
if near:
|
||||
return 0.5, [f"{our['구분']} {n}" for n in near], "앞 마디 같음"
|
||||
return 0.0, [], ""
|
||||
|
||||
|
||||
# ── 출처 풀이(781-1 · 781-2) — 건설 부문 · 임도 옛 번호 대응 · 기초 없음 ───────────
|
||||
OLD = "(옛)"
|
||||
_ROAD = "임도품셈" + OLD
|
||||
_ROAD_MAP = make.BODY.parent / "경쟁사 프로그램/STmate 분석/30_원자료/임도품셈_판_대응표.csv"
|
||||
_OLD_NO = re.compile(r"^(\d+(?:-\d+)+)(\s+[가-하])?$")
|
||||
_STOP_GRAMS = frozenset("설치 해체 작업 인력 기계 장비 보완 신설 공사".split())
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def road_map() -> dict[str, str]:
|
||||
"""임도품셈(옛) 번호 → 현행 산림 번호 — 지식DB 대응표 「확정」 한 대응만(일대다 · 미확인 뺌) ·
|
||||
옛 번호가 가 · 나 항이면 현행 윗마디(9-13-7 → 9-13 · 그 항만 대응하므로)."""
|
||||
with open(_ROAD_MAP, encoding="utf-8-sig", newline="") as f:
|
||||
rows = list(csv.DictReader(f))
|
||||
out = {}
|
||||
for row in rows:
|
||||
old, new = _OLD_NO.match(row["과거_절"].strip()), row["현행_절"].strip()
|
||||
if row["판정"] != "확정" or not old or not _OLD_NO.fullmatch(new) or " " in new:
|
||||
continue
|
||||
out[old.group(1)] = new.rsplit("-", 1)[0] if old.group(2) and new.count("-") > 1 else new
|
||||
return out
|
||||
|
||||
|
||||
def _grams(text: Any) -> set[str]:
|
||||
"""뜻 낱말 — 낱말 안 한글 두 글자 묶음(괄호 글 포함 · 굴삭 = 굴착)."""
|
||||
words = re.findall(r"[가-힣]+", str(text or "").replace("굴삭", "굴착"))
|
||||
return {w[i : i + 2] for w in words for i in range(len(w) - 1)} - _STOP_GRAMS
|
||||
|
||||
|
||||
@lru_cache(maxsize=2)
|
||||
def _sections(book: str) -> dict[tuple[str, str], str]:
|
||||
"""{(부문, 절 번호): 제목(괄호 글 · 「:」 뒤 뺌)} — 원문 본문."""
|
||||
return {
|
||||
(s["division"], s["section"]): re.sub(r"\(.*?\)", "", s["title"]).split(":")[0].strip()
|
||||
for s in make._all_sections(book)
|
||||
}
|
||||
|
||||
|
||||
@lru_cache(maxsize=2)
|
||||
def _named(whole_id: int) -> dict[tuple[str, str], list[str]]:
|
||||
"""{(구분, 원문번호): [로직 이름]} — M01 로직."""
|
||||
out: dict[tuple[str, str], list[str]] = {}
|
||||
for data in m01.loaded()[0].values():
|
||||
if data.get("그룹") == "로직":
|
||||
for row in data.get("줄") or []:
|
||||
key = (str(row.get("구분") or ""), str(row.get("원문번호") or "").split(" ")[0])
|
||||
out.setdefault(key, []).append(str(row.get("이름") or ""))
|
||||
return out
|
||||
|
||||
|
||||
def _part(number: str, name: Any, text: Any = "") -> str:
|
||||
"""건설품셈(부문 모름) 번호 → 원문에 그 번호가 있는 부문 중 블록 이름(안 맞으면 블록 글 ·
|
||||
두 묶음)이 원문 제목 · 같은 번호 로직 이름과 뜻 낱말을 나누는 부문이 하나 = 그 부문 ·
|
||||
둘 이상 = 모름 · 없음 = 「(옛)」(번호 뜻 안 맞음)."""
|
||||
if not _grams(name):
|
||||
return "건설품셈"
|
||||
named = _named(id(m01.loaded()[1]))
|
||||
texts = {
|
||||
part: [title, *named.get((f"건설품셈 {part}", no), [])]
|
||||
for (part, no), title in _sections("건설품셈").items()
|
||||
if no == number
|
||||
}
|
||||
hits = []
|
||||
for mine, least in ((_grams(name), 1), (_grams(text), 2)): # 글은 두 묶음 이상
|
||||
hits = [p for p, ts in texts.items() if any(len(mine & _grams(t)) >= least for t in ts)]
|
||||
if hits:
|
||||
break
|
||||
if len(hits) == 1:
|
||||
return f"건설품셈 {hits[0]}"
|
||||
return "건설품셈" if hits else "건설품셈" + OLD
|
||||
|
||||
|
||||
def resolve(theirs: list[list[str]], name: Any, text: Any = "") -> list[list[str]]:
|
||||
"""블록 출처 → 부문 풀이 · 임도 옛 번호 대응(`road_map`) 뒤 출처."""
|
||||
out: list[list[str]] = []
|
||||
for book, number in theirs:
|
||||
one = [book, number]
|
||||
if book == _ROAD and number in road_map():
|
||||
one = ["산림품셈", road_map()[number]]
|
||||
elif book == "건설품셈":
|
||||
one = [_part(number, name, text), number]
|
||||
if one not in out:
|
||||
out.append(one)
|
||||
return out
|
||||
|
||||
|
||||
def settle(book: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Book 단산 블록 출처를 제자리에서 풀이(줄 `근거.출처` 가 같은 리스트를 가리킴)."""
|
||||
for block in book.get("단산") or []:
|
||||
if block.get("출처"):
|
||||
text = " ".join(str(t) for t in block.get("글") or [])
|
||||
block["출처"][:] = resolve(block["출처"], block.get("이름"), text)
|
||||
return book
|
||||
|
||||
|
||||
def has_logic(kind: str, number: str) -> bool:
|
||||
"""그 구분 · 조항(윗마디 · 아랫마디 포함)에 M01 로직이 있나."""
|
||||
return _has(id(m01.loaded()[1]), kind, number)
|
||||
|
||||
|
||||
@lru_cache(maxsize=512)
|
||||
def _has(whole_id: int, kind: str, number: str) -> bool:
|
||||
named = _named(whole_id)
|
||||
parts = number.split("-")
|
||||
ups = {"-".join(parts[:i]) for i in range(2, len(parts) + 1)}
|
||||
return any(k == kind and (no in ups or no.startswith(number + "-")) for k, no in named)
|
||||
|
||||
|
||||
def no_basis(theirs: list | None) -> dict[str, str] | None:
|
||||
"""출처 조항이 모두 우리 기초 밖이면 표지 `{판정, 글, 이름}` — 기초 없음(로직 0) › 옛 번호
|
||||
(「(옛)」 · 지금 원문에 없는 번호) · 로직 있는 조항 · 건설품셈(부문 모름)이 하나라도
|
||||
있으면 None."""
|
||||
none, old = [], []
|
||||
for book, number in theirs or []:
|
||||
base, _, part = book.partition(" ")
|
||||
if book == "건설품셈":
|
||||
return None
|
||||
if book.endswith(OLD) or base not in make.BOOKS:
|
||||
old.append((book.removesuffix(OLD), number))
|
||||
elif has_logic(book, number):
|
||||
return None
|
||||
elif (title := _sections(base).get((part, number))) is None:
|
||||
old.append((book, number))
|
||||
else:
|
||||
none.append((book, number, title))
|
||||
if none:
|
||||
book, number, title = none[0]
|
||||
where = f"{book.replace('품셈', '')} {number} {title}"
|
||||
return {
|
||||
"판정": "기초 없음",
|
||||
"글": f"근거 조항 우리 기초에 없음 — {book} {number} {title}",
|
||||
"이름": f"기초 없음 — {where}",
|
||||
}
|
||||
if old:
|
||||
return {
|
||||
"판정": "옛 번호",
|
||||
"글": f"옛 번호 — 대응 미확인 — {old[0][0]} {old[0][1]}",
|
||||
"이름": "옛 번호 — 근거 못 씀",
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
# ── 조항 색인 · 블록 근거(785-1) ──────────────────────────────────────────
|
||||
_NATURE = {
|
||||
**dict.fromkeys(("㎡", "m2", "ha"), "면적"),
|
||||
**dict.fromkeys(("㎥", "m3", "공㎥"), "체적"),
|
||||
**dict.fromkeys(("m", "km"), "길이"),
|
||||
**dict.fromkeys(("ton", "t", "톤", "kg", "㎏"), "무게"),
|
||||
"식": "식",
|
||||
}
|
||||
|
||||
|
||||
def nature(unit: Any) -> str | None:
|
||||
"""단위 → 성격(면적 · 체적 · 길이 · 무게 · 식) · 모름 = None ·
|
||||
「원/10㎡」 = ㎡ · 여러 단위 = 모름."""
|
||||
text = str(unit or "").strip().removeprefix("원/")
|
||||
if any(c in text for c in "·(또"):
|
||||
return None
|
||||
return _NATURE.get(re.sub(r"^[\d,]+", "", text).strip().lower())
|
||||
|
||||
|
||||
@lru_cache(maxsize=2)
|
||||
def _index(whole_id: int) -> dict[tuple[str, str], list[dict[str, Any]]]:
|
||||
out: dict[tuple[str, str], list[dict[str, Any]]] = {}
|
||||
for data in m01.loaded()[0].values():
|
||||
if data.get("그룹") != "로직":
|
||||
continue
|
||||
for row in data.get("줄") or []:
|
||||
key = (str(row.get("구분") or ""), str(row.get("원문번호") or "").split(" ")[0])
|
||||
one = {"키": str(row.get("키")), "이름": str(row.get("이름") or "")}
|
||||
one |= {"구분": key[0], "원문번호": key[1], "결과단위": row.get("결과단위")}
|
||||
out.setdefault(key, []).append(one)
|
||||
return out
|
||||
|
||||
|
||||
def index() -> dict[tuple[str, str], list[str]]:
|
||||
"""{(구분, 원문번호): [로직 키]} — M01 로직."""
|
||||
return {k: [o["키"] for o in v] for k, v in _index(id(m01.loaded()[1])).items()}
|
||||
|
||||
|
||||
def lookup(book: str, number: str) -> list[dict[str, Any]]:
|
||||
"""[책, 번호] → 로직 줄 + 맞음 같음 · 윗마디 — 같은 것이 있으면 같음만."""
|
||||
table = _index(id(m01.loaded()[1]))
|
||||
if book == "건설품셈":
|
||||
kinds = {k for k, _ in table if k.startswith("건설품셈 ")}
|
||||
elif book.endswith(OLD):
|
||||
return []
|
||||
else:
|
||||
kinds = {book}
|
||||
same = [o for k in kinds for o in table.get((k, number), [])]
|
||||
if same:
|
||||
return [{**o, "맞음": "같음", "출처": [book, number]} for o in same]
|
||||
return [
|
||||
{**o, "맞음": "윗마디", "출처": [book, number]}
|
||||
for (k, no), rows in table.items()
|
||||
if k in kinds and no.startswith(number + "-")
|
||||
for o in rows
|
||||
]
|
||||
|
||||
|
||||
def _paths(nodes: list[dict[str, Any]], up: str = "") -> list[tuple[str, dict[str, Any]]]:
|
||||
out = []
|
||||
for one in nodes:
|
||||
path = f"{up}.{one['번']}" if up else one["번"]
|
||||
out += [(path, one), *_paths(one["아래"], path)]
|
||||
return out
|
||||
|
||||
|
||||
def _sources(lines: list[str], name: Any, whole: str) -> list[list[str]]:
|
||||
raw: list[list[str]] = []
|
||||
for line in lines:
|
||||
raw += [one for one in est.sources(line) if one not in raw]
|
||||
return resolve(raw, name, whole)
|
||||
|
||||
|
||||
def _split(found: list[dict[str, Any]], nodes: list[dict[str, Any]]) -> tuple[list, list]:
|
||||
"""윗마디 후보 여럿 → 일 절 제목과 뜻 낱말 가장 많이 겹친 후보를 그 절에 · 남은 후보는 뺌."""
|
||||
works = {id(w) for w in formula.works(nodes)}
|
||||
picked: dict[str, str] = {}
|
||||
for path, one in _paths(nodes):
|
||||
if id(one) not in works:
|
||||
continue
|
||||
mine = _grams(_flat(one["제목"]))
|
||||
score = {o["키"]: len(mine & _grams(_flat(o["이름"]))) for o in found}
|
||||
best = max(score.values(), default=0)
|
||||
for key, n in score.items():
|
||||
if best and n == best:
|
||||
picked.setdefault(key, path)
|
||||
if not picked:
|
||||
return found, []
|
||||
keep = [{**o, "절": picked[o["키"]]} for o in found if o["키"] in picked]
|
||||
drop = [{"키": o["키"], "까닭": "절 이름 안 맞음"} for o in found if o["키"] not in picked]
|
||||
return keep, drop
|
||||
|
||||
|
||||
def basis(block: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Book 단산 블록 → {표지, 글, 출처, 후보 [{키, 이름, 구분, 원문번호, 결과단위, 맞음,
|
||||
출처, 절}], 뺌 [{키, 까닭}]}."""
|
||||
text = [str(t) for t in block.get("글") or []]
|
||||
name, whole = block.get("이름"), " ".join(text)
|
||||
nodes = block.get("절") or formula.tree(text, block.get("글행"))
|
||||
top = []
|
||||
for line in text:
|
||||
if formula._title(line):
|
||||
break
|
||||
top.append(line)
|
||||
head = " ".join(top)
|
||||
used = _sources(top, name, whole)
|
||||
if not used and not re.search(r"품[셈줌]", head):
|
||||
used = _sources([t for t in text if not formula._title(t)], name, whole)
|
||||
found = [{**o, "절": None} for book, no in used for o in lookup(book, no)]
|
||||
out: dict[str, Any] = {"표지": None, "글": "", "출처": list(used), "후보": [], "뺌": []}
|
||||
if len(found) > 1 and any(o["맞음"] == "윗마디" for o in found):
|
||||
found, out["뺌"] = _split(found, nodes)
|
||||
for path, one in _paths(nodes):
|
||||
for book, no in _sources([one["제목"]], one["제목"], whole):
|
||||
out["출처"] += [[book, no]] if [book, no] not in out["출처"] else []
|
||||
found += [{**o, "절": path} for o in lookup(book, no)]
|
||||
if not out["출처"]:
|
||||
out["표지"], out["글"] = "번호 없음", "품셈 번호 없음 — 이름 후보로"
|
||||
elif "준용" in head:
|
||||
out["표지"], out["글"] = "준용", "준용 — 후보 참고만"
|
||||
elif mark := no_basis(out["출처"]):
|
||||
out["표지"], out["글"] = mark["판정"], mark["글"]
|
||||
mine = nature(block.get("단위"))
|
||||
for o in found:
|
||||
theirs = nature(o["결과단위"])
|
||||
if any(o["키"] == c["키"] for c in out["후보"]):
|
||||
continue
|
||||
if mine and theirs and mine != theirs:
|
||||
why = f"결과단위 다름 — {o['결과단위']} ≠ {block.get('단위')}"
|
||||
out["뺌"].append({"키": o["키"], "까닭": why})
|
||||
else:
|
||||
out["후보"].append(o)
|
||||
if out["표지"] is None and not out["후보"]:
|
||||
out["표지"], out["글"] = "기초 없음", "결과단위 맞는 우리 기초 없음"
|
||||
return out
|
||||
@@ -17,7 +17,7 @@ STmate 꼴(「일위대가표」 · 「단가산출근거」 시트) = `M02_Hopy
|
||||
% 줄 = floor(바탕 × 율 / 100 × 10) / 10 · 호표 비목 = floor(줄 금액 합) · 총괄표 합계 —
|
||||
어긋남은 글 목록(막지 않음).
|
||||
행(329-1) = 호표 [제N호표 줄, 마지막 구성 줄] · 구성 줄 한 줄(엑셀 1부터) ·
|
||||
근거(330-1) = 단산N참조 줄에 「단가산출」 블록 `{출처, 직종, 행}`(`M02_Hopyo_Basis.read`) ·
|
||||
근거(330-1) = 단산N참조 줄에 「단가산출」 블록 `{출처, 직종, 행}`(`M02_Hopyo_ReadEst.read`) ·
|
||||
`단산` = 블록마다 `{번, 이름, 단위, 행, 출처, 직종, 글, 금액, 항목}`(338 · 글 = 머리 뺀 몸 줄) ·
|
||||
`꼴` = 「엑셀꼴」 · 두 형식 끝에 `UnitLine.settle`(단위 줄 하위 · 재현 · 덮음 · 일위대가 줄
|
||||
투영 · 778-3).
|
||||
@@ -30,8 +30,8 @@ import re
|
||||
from decimal import ROUND_FLOOR, Decimal, InvalidOperation
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Basis as source_basis # 출처 풀이(781-1)
|
||||
from M02_MasterTemplete import M02_Hopyo_ReadEst as hopyo_basis # EST 단가산출 어댑터(776-2)
|
||||
from M02_MasterTemplete import M02_Hopyo_Clause as hopyo_clause # 출처 풀이(781-1 · 785-1)
|
||||
from M02_MasterTemplete import M02_Hopyo_ReadEst as hopyo_est # EST 단가산출 어댑터(776-2)
|
||||
from M02_MasterTemplete import M02_Hopyo_Stmate as hopyo_stmate
|
||||
from M02_MasterTemplete import M02_Hopyo_UnitLine as unit_line
|
||||
|
||||
@@ -112,7 +112,7 @@ def opened(name: str, data: bytes, wanted: tuple[str, ...] = ()) -> tuple[dict[s
|
||||
def _open(
|
||||
name: str, data: bytes, wanted: tuple[str, ...] = ()
|
||||
) -> tuple[dict[str, list[list[Any]]], Any]:
|
||||
keep = (SHEET, SUMMARY, hopyo_basis.SHEET, *wanted)
|
||||
keep = (SHEET, SUMMARY, hopyo_est.SHEET, *wanted)
|
||||
if not data:
|
||||
raise ExcelError("빈 파일")
|
||||
if b"DRMONE" in data[:256] and data[:4] not in (b"PK\x03\x04", _OLE):
|
||||
@@ -184,7 +184,7 @@ def kind_of(line: dict[str, Any]) -> str:
|
||||
단위 · 단가) · 끝에 노임 올림(단위 「인」 + 우리 직종 · 괄호 글 풂)."""
|
||||
kind = _code_kind(line) if line.get("코드") else _cell_kind(line)
|
||||
if kind in ("기타", "자재", "노임") and _flat(line.get("단위")) == "인":
|
||||
return "노임" if hopyo_basis.trade(line.get("이름")) else kind
|
||||
return "노임" if hopyo_est.trade(line.get("이름")) else kind
|
||||
return kind
|
||||
|
||||
|
||||
@@ -215,7 +215,7 @@ def _cell_kind(line: dict[str, Any]) -> str:
|
||||
return "단가산출"
|
||||
if unit in _MACHINE_UNITS:
|
||||
return "기계"
|
||||
if unit == "인" and _flat(line.get("이름")) in hopyo_basis.trade_names():
|
||||
if unit == "인" and _flat(line.get("이름")) in hopyo_est.trade_names():
|
||||
return "노임"
|
||||
if not any(price.get(k) for k in ("계", *COSTS)):
|
||||
return "별도계상"
|
||||
@@ -303,7 +303,7 @@ def read_book(name: str, data: bytes) -> dict[str, Any]:
|
||||
sheets, xls = _open(name, data, LISTS)
|
||||
_OPENED = (data, sheets, xls) # 격자(`Grid.read`)가 이어 씀 — 한 번 열기
|
||||
if hopyo_stmate.is_stmate(sheets):
|
||||
return unit_line.settle(source_basis.settle(hopyo_stmate.read(sheets)))
|
||||
return unit_line.settle(hopyo_clause.settle(hopyo_stmate.read(sheets)))
|
||||
rows = sheets.get(SHEET)
|
||||
if rows is None:
|
||||
raise ExcelError(NO_SHEET)
|
||||
@@ -349,7 +349,7 @@ def read_book(name: str, data: bytes) -> dict[str, Any]:
|
||||
raw[-1].append(line)
|
||||
if not hopyos:
|
||||
raise ExcelError(f"{SHEET} 시트에 호표(제N호표)가 없음")
|
||||
blocks = hopyo_basis.read(sheets.get(hopyo_basis.SHEET))
|
||||
blocks = hopyo_est.read(sheets.get(hopyo_est.SHEET))
|
||||
_by_name(raw, blocks)
|
||||
for line in (one for lines in raw for one in lines):
|
||||
ref = line["참조"] or {}
|
||||
@@ -376,7 +376,7 @@ def read_book(name: str, data: bytes) -> dict[str, Any]:
|
||||
"검산": {"어긋남": wrong, "총괄표": len(totals)},
|
||||
}
|
||||
return unit_line.settle(
|
||||
source_basis.settle(book)
|
||||
hopyo_clause.settle(book)
|
||||
) # 출처 풀이(781-1) · 단위 줄 하위 · 재현 · 덮음 · 일위대가 줄 투영(778-3)
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from collections import Counter
|
||||
import pytest
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Hopyo_Basis as hb
|
||||
from M02_MasterTemplete import M02_Hopyo_Clause as hb # 785-1 출처 풀이 정본
|
||||
from M02_MasterTemplete import M02_Hopyo_Fit as fit
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
from M02_MasterTemplete import M02_Hopyo_Ours as ours
|
||||
@@ -115,5 +115,5 @@ def test_여덟_파일_표지_수():
|
||||
for tag in golden.FILES:
|
||||
for block in golden.read(tag)["단산"]:
|
||||
seen[(hb.no_basis(block["출처"]) or {}).get("판정")] += 1
|
||||
assert seen["기초 없음"] == 18 # 782-1 로 북부청 임도 단산10(공통 1-2-7) 하나 빠짐
|
||||
assert seen["옛 번호"] == 121
|
||||
assert seen["기초 없음"] == 3 # 782-1 · 782-2 · 782-3 새 기초로 줄어듦
|
||||
assert seen["옛 번호"] == 120
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
"""785-1(642-1) 지킴 — 출처 풀이 · 조항 색인(`M02_Hopyo_Clause`) · 블록 글 → 절 나무 · 상수 ·
|
||||
대입식 · 절 몫(`M02_Hopyo_Formula`) · 북부청 견본 엑셀(설계 642-A 5-3 첫 줄 기대).
|
||||
읽기만 · 서버 없음.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import warnings
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Basis as basis # noqa: E402
|
||||
from M02_MasterTemplete import M02_Hopyo_Clause as clause # noqa: E402
|
||||
from M02_MasterTemplete import M02_Hopyo_Excel as hx # noqa: E402
|
||||
from M02_MasterTemplete import M02_Hopyo_Formula as fm # noqa: E402
|
||||
|
||||
warnings.filterwarnings("ignore", category=UserWarning, module="openpyxl")
|
||||
XLS = (
|
||||
ROOT
|
||||
/ "resources/knowledge/original/실무문서/2026_실무일위대가 모음"
|
||||
/ "2026년_북부지방산림청_임도사업_설계내역서(공통단가).xls"
|
||||
)
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def blocks() -> dict[int, dict]:
|
||||
if not XLS.exists():
|
||||
pytest.skip("견본 엑셀 없음")
|
||||
book = hx.read_book(XLS.name, XLS.read_bytes())
|
||||
return {b["번"]: b for b in book["단산"]}
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def got(no: int) -> dict:
|
||||
return clause.basis(blocks()[no])
|
||||
|
||||
|
||||
def keys(no: int) -> set[str]:
|
||||
return {o["키"] for o in got(no)["후보"]}
|
||||
|
||||
|
||||
# ── 조항 색인 → 후보 ──────────────────────────────────────────────────
|
||||
@pytest.mark.parametrize(
|
||||
"no, want",
|
||||
[
|
||||
(10, {"GC001140", "GC001141", "GC001142"}),
|
||||
(46, {"GC000198"}),
|
||||
(24, {"GC000211"}),
|
||||
(47, {"GC000206", "GC000207"}),
|
||||
(39, {"GF000224", "GF000225"}),
|
||||
],
|
||||
)
|
||||
def test_후보(no, want):
|
||||
assert keys(no) == want
|
||||
assert got(no)["표지"] is None
|
||||
|
||||
|
||||
def test_윗마디_절마다():
|
||||
"""47 「6-2」 → 절 「1. 현장가공」 GC000206 · 「2. 조립」 GC000207 · 공장가공은 뺌."""
|
||||
by = {o["키"]: (o["맞음"], o["절"]) for o in got(47)["후보"]}
|
||||
assert by == {"GC000206": ("윗마디", "1"), "GC000207": ("윗마디", "2")}
|
||||
assert {o["키"] for o in got(47)["뺌"]} >= {"GC000208"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("no, mark", [(33, "준용"), (6, "옛 번호"), (3, "번호 없음")])
|
||||
def test_표지(no, mark):
|
||||
assert got(no)["표지"] == mark
|
||||
|
||||
|
||||
def test_준용_결과단위_거름_절_번호():
|
||||
"""33 「산림 12-12 준용」 → GF000177(원/식) 뺌 · 절 4 「표준품셈3-4-8」 → GC000110."""
|
||||
r = got(33)
|
||||
assert {o["키"]: o["까닭"][:7] for o in r["뺌"]} == {"GF000177": "결과단위 다름"}
|
||||
assert [(o["키"], o["절"]) for o in r["후보"]] == [("GC000110", "4")]
|
||||
|
||||
|
||||
def test_색인_옛이름():
|
||||
assert set(clause.index()[("건설품셈 공통", "1-2-7")]) == {"GC001140", "GC001141", "GC001142"}
|
||||
assert clause.nature("원/10㎡") == "면적" and clause.nature("원/자재 단위") is None
|
||||
assert basis.settle is clause.settle and basis.no_basis is clause.no_basis
|
||||
|
||||
|
||||
# ── 절 나무 ───────────────────────────────────────────────────────────
|
||||
def _works(no: int) -> list[str]:
|
||||
nodes = blocks()[no]["절"]
|
||||
ids = {id(w) for w in fm.works(nodes)}
|
||||
return [p for p, one in clause._paths(nodes) if id(one) in ids]
|
||||
|
||||
|
||||
def test_절_나무():
|
||||
assert _works(47) == ["1", "2"]
|
||||
assert _works(6) == ["1.가", "1.나", "1.다", "3"]
|
||||
assert [n["번"] for n in blocks()[6]["절"]] == ["1", "3", "4"] # 「2.」 빠짐 그대로
|
||||
assert _works(24) == [] # 재료비 · 인건비 · 합계뿐 = 블록 통째
|
||||
b = blocks()[47]
|
||||
assert len(b["글행"]) == len(b["글"])
|
||||
one = fm.find(b["절"], "1")
|
||||
assert one["소계"] == {"노무비": 217540.1, "재료비": 19578.6, "경비": None, "계": 237118.7}
|
||||
lo, hi = b["행"]
|
||||
assert all(lo < r <= hi for r in one["행"])
|
||||
|
||||
|
||||
# ── 상수 · 대입식 · 몫 ─────────────────────────────────────────────────
|
||||
def _consts(no: int, path: str) -> dict[str, dict]:
|
||||
return {c["기호"]: c for c in fm.find(blocks()[no]["절"], path)["상수"]}
|
||||
|
||||
|
||||
def test_상수():
|
||||
c = _consts(10, "1")
|
||||
assert (c["L"]["값"], c["L"]["단위"]) == (6, "m")
|
||||
assert (c["경사도"]["값"], c["경사도"]["단위"]) == (35, "°")
|
||||
assert (c["n"]["값"], c["n"]["이름"]) == (42, "운반회수")
|
||||
c = _consts(6, "1.나")
|
||||
assert {k: c[k]["값"] for k in ("q", "K", "f", "E", "Cm")} == {
|
||||
"q": 0.6,
|
||||
"K": 0.55,
|
||||
"f": 1,
|
||||
"E": 0.35,
|
||||
"Cm": 18,
|
||||
}
|
||||
assert (c["q"]["단위"], c["Cm"]["단위"], c["Cm"]["덧"]) == ("㎥", "sec", "90˚")
|
||||
assert isinstance(c["q"]["행"], int)
|
||||
|
||||
|
||||
def test_대입식():
|
||||
p = next(f for f in fm.find(blocks()[10]["절"], "1")["식"] if f["기호"] == "P")
|
||||
assert p["꼴"] == "곱" and p["값"] == 0.547619
|
||||
assert [(f["수"], f["단위"]) for f in p["분자"]] == [
|
||||
(1, "㎡"),
|
||||
(575, "㎏/㎡"),
|
||||
(1, "회"),
|
||||
(1, "일"),
|
||||
]
|
||||
assert p["분모"] == [{"수": 25, "단위": "㎏"}, {"기호": "n", "단위": ""}]
|
||||
assert p["대입"] == {"n": 42}
|
||||
|
||||
|
||||
def test_절_몫():
|
||||
nodes = blocks()[6]["절"]
|
||||
share = fm.find(nodes, "1.나")["몫"]
|
||||
assert share["글"] == "× 575 / 1750 × 90%"
|
||||
assert share["값"] == pytest.approx(575 / 1750 * 0.9, abs=1e-6)
|
||||
assert fm.find(nodes, "1.가")["몫"]["값"] == 0.1
|
||||
assert fm.find(nodes, "3")["몫"]["값"] == pytest.approx(575 / 1750, abs=1e-6)
|
||||
assert fm.find(blocks()[47]["절"], "1")["몫"] is None
|
||||
|
||||
|
||||
def test_인자_괄호():
|
||||
got_ = fm.factors("Cm × n / (60 × Es)")
|
||||
assert [(w, f.get("기호") or f.get("수")) for w, f in got_] == [
|
||||
("곱", "Cm"),
|
||||
("곱", "n"),
|
||||
("나눔", 60),
|
||||
("나눔", "Es"),
|
||||
]
|
||||
assert fm.factors("t1 + t2") is None and fm.factors("1 2") is None
|
||||
Reference in New Issue
Block a user