"""재료 자재품목 열 한 벌 · 지역 · 상하차(요구 828 · 830 · 831 · 834 · 836 · 837 · 839 · PLAN 830-10). build_재료_자재품목 이 부름 · 시험 = test_830_2_material_merge. 줄 목록 = EST 그대로(지역 · 관급 · 사급 · 제조사 · 조합 줄마다 따로) — 합치지 않음. 열 한 벌 = COLUMNS — 지역 · 상하차 = 단위 바로 뒤 · 가격 열 = 조달청(관급) · 물가지 넷 · 일반가격(사급). 지역 칸 = 이름 · 규격에 적힌 지역 글 그대로(`split_region` 꼴 일곱 · 표기 안 고침 · 없음 = -) — 그 글은 이름 · 규격에서 뺌 · 관급 · 사급 글은 이름에 남김 · 상표 · 산지 · 수종 글(「보령웅천석」 · 「농산개발(홍성)」)은 지역 아님 · EST 이름 칸 30 자에서 잘린 지역 글은 잘린 그대로 · 비고에 CUT. 사급 / 관급 고르기는 재료 표 열이 아님 — 단가산출 쪽 몫(요구 837). 상하차 칸 = 상차 · 하차 · -(기본 · 모름) — 이름 · 규격 끝 인도 조건 글로 채우고 그 글은 이름 · 규격에서 뺌(요구 836) 「도착도」 · 「하차도」 = 하차 · 「채취장상차(도)」 · 「하치장 · 생산공장 상차도」 · 「(상차도)」 = 상차 · 「(상차포함)」 = 상차 · 「(상차제외)」 = -(포함 안 됨 · 같은 이름의 상차 줄과 상하차 칸으로 갈림) 조달청 칸 값은 모두 비움(EST 「가격정보」 — 요구 827 · 831) · 일반가격 = 시스템 관리자가 넣는 칸. 지역 · 상하차 · 일반가격 손 값은 빌더가 이어 받음(지역 · 상하차 「-」 = 손 값 아님 — 판독 값). """ import re MARKET = ("물가자료", "유통물가", "물가정보", "거래가격") REGION = "지역" # 표지 칸 — 적힌 지역 글 그대로 · 없음 = NONE NONE = "-" LOAD = "상하차" # 표지 칸(값 칸 아님) LOADS = ("-", "상차", "하차") # 첫째 = 기본(모름) GOVT = "조달청" # 관급 가격 칸 — 가장 낮은 값 고르기에 안 넣음(요구 826 · 830) OWN = "일반가격" # 시스템 관리자 손 입력 칸(사급) HAND = (REGION, LOAD, OWN) # 빌더가 이어 받는 손 값 칸 COLUMNS = ( # 열 한 벌 · 차례(요구 830 · 831) "키", "구분", "상세구분", "이름", "규격", "단위", REGION, LOAD, GOVT, *MARKET, OWN, "출처", "비고", "면수", ) NAME_LOAD = re.compile( r",\s*(도착도|하차도|채취장상차도|채취장상차)$|\(((?:상|하)차(?:포함|제외))\)$" ) # 모 래, 서울, 도착도 · 폐기물운반비(상차포함) SPEC_LOAD = re.compile( r"\s*(?:\((상차도)\)|((?:하치장|생산공장)\s*상차도))$" ) # HD=10mm 하치장 상차도 def split_load(row: dict) -> tuple[str, str, str, str | None]: """(인도 조건 글을 뺀 이름, 규격, 상하차 값, 뺀 글) — 도착 · 하차 = 하차 · 상차 = 상차 · 글 없음 = -.""" name, spec = row.get("이름") or "", row.get("규격") or "" m = NAME_LOAD.search(name) if m: name, text = name[: m.start()], m[1] or m[2] if text.endswith("제외"): return name, spec, LOADS[0], text else: m = SPEC_LOAD.search(spec) if not m: return name, spec, LOADS[0], None spec, text = spec[: m.start()], m[1] or m[2] return name, spec, "하차" if text.startswith(("도착", "하차")) else "상차", text def place(row: dict) -> dict: """COLUMNS 차례의 줄 — 첫 칸(열쇠 · 키)은 그대로 · 인도 조건 글은 상하차 칸 · 지역 글은 지역 칸으로 · 손 값(지역 · 상하차 — 「-」 아님)이 있으면 그 값 · 조달청 = 빔 · 옛 칸(관급)은 뺌.""" name, spec, load, _ = split_load(row) name, spec, region = split_region(name, spec) first = next(iter(row)) note = row.get("비고") if ( region != NONE and len(row.get("이름") or "") >= 30 and region.count("(") > region.count(")") ): note = " · ".join(x for x in (note, CUT) if x) fix = { "이름": name, "규격": spec, REGION: row.get(REGION) if row.get(REGION) not in (None, "", NONE) else region, LOAD: row.get(LOAD) if row.get(LOAD) not in (None, "", NONE) else load, GOVT: None, "비고": note, } return {first: row[first], **{c: fix[c] if c in fix else row.get(c) for c in COLUMNS[1:]}} # ── 지역 글(요구 839 · 조사 818 2 절 꼴 일곱) ───────────────────────────── CUT = "지역 글 잘림 — EST 이름 칸 30 자" PLACES = frozenset( """전국 서울 부산 대구 인천 광주 대전 울산 세종 제주 경기 강원 충북 충남 전북 전남 경북 경남 청주 전주 포항 창원 춘천 충주 강릉 천안 원주 여수 구미 순천 수원 목포 금산 영천 논산 삼척 홍성 익산 청도 양양 남원 공주 군산 김천 가평 용인 포천 경주 아산 제천 여주 보령 부여 당진 청원 음성 서산 영동 진천 예산 장성 고흥 고창 광양 고령 성주 영주 안동 칠곡 상주 문경 합천 화성 경산""".split() ) # 콤마 · 괄호 꼴에 적힌 지역 낱말(자료에서) — 관급 꼬리(F3)는 낱말 표 없이 꼬리 전부 _P = "|".join(sorted(PLACES, key=len, reverse=True)) _F2 = re.compile(rf"^(.*?)\(({_P})\)-사급$") # 레미콘(서울)-사급 _F4 = re.compile(rf"^(.*?)\((SD\d+),({_P})\)-관급$") # 이형철근(SD400,제주)-관급 _F3 = re.compile( r"^(.*?\(관급\))-(?!거양|SD\d)(.+)$" ) # 레미콘(관급)-경상북도(칠곡군) · 거양 = 제조사 _F1 = re.compile(rf"^({_P})\s*(\(관급\))?$") # 이름 콤마 조각 「, 서울」 · 「, 제주 (관급)」 _F5 = re.compile( rf"\((?:([^()·]+)·)?((?:{_P})(?:지역)?)(,관급)?\)" ) # (서울) · (건조상태·전국) · (충남지역,관급) _F6 = re.compile( rf"(? tuple[str, str, str]: """(지역 글을 뺀 이름, 규격, 지역 글 | NONE) — 꼴 일곱(F1 이름 끝 콤마 · F2 (지역)-사급 · F3 (관급)-꼬리 · F4 (강종,지역)-관급 · F5 이름 속 괄호 · F6 규격 속 괄호 · F7 규격 전체) · 관급 · 사급 글은 남김.""" name, spec = name or "", spec or "" if m := _F2.match(name): return f"{m[1]}-사급", spec, m[2] if m := _F4.match(name): return f"{m[1]}({m[2]})-관급", spec, m[3] if m := _F3.match(name): return m[1], spec, m[2] parts = name.split(",") for i, part in enumerate(parts[1:], 1): if m := _F1.match(part.strip()): rest = ",".join(parts[:i] + parts[i + 1 :]) return (rest.rstrip() + m[2] if m[2] else rest), spec, m[1] if m := _F5.search(name): inner = ",".join(x for x in (m[1], (m[3] or "")[1:]) if x) return name[: m.start()] + (f"({inner})" if inner else "") + name[m.end() :], spec, m[2] if m := _F6.search(spec): return name, spec[: m.start()] + spec[m.end() :], m[1] if spec.strip() in PLACES: return name, "", spec.strip() return name, spec, NONE