Files
Aislo/tools/stmate-test/jdl_format.py
T

185 lines
6.8 KiB
Python

# -*- coding: utf-8 -*-
"""조달청 JDL TEXT 묶음(JDL VERSION 3.3) — 줄 형식 · 열 이름 · 검사기 열 나눔 · 줄 만들기.
근거(지식DB STmate/REA 분석/37_파일입출력_잔여_작업자료_20261114 · 34 §17.9 바로잡음):
검사기 JDLCHK.Tab_33.dll(sha256 c5052736…)
· 0x5051e0 = 고정폭 자리에 탭 넣기 — 0x505218 jmp 가 고리를 건너뜀(죽은 표) → 줄은 손대지 않음
· 0x504d20 = 줄을 첫 \\x05 에서 둘로 — 앞쪽 + " " 를 탭(#9)으로 나눠 열마다 Trim(≤#32 걷음) = 열 배열,
뒤쪽 + " " 를 \\x07 로 나눔(STmate 덧붙임 칸) · 열 개수 = 앞쪽 배열 길이(0x405bfc 뒤 cmp)
· 열 → 필드: 단가 목록 8종 FieldDefs 차례(M 0x4ea818) · MainPart 0x4f7890 · UnitPart 0x4fa7fc · JPart 0x4fdd10
만드는 쪽 ST_U_Convert.CPM Conv_ST8_Jodal 0x4b9f2c → 코드 줄 0x4b0904(FUN_004a7cf0 = 값 TrimRight · 숫자는
FloatToStr+EXTTOSTR · dl=1 이면 뒤에 탭) · 덧붙임 칸 앞 \\x05 · 사이 \\x07
"""
from __future__ import annotations
from decimal import Decimal
EXT = "\x05" # 열 끝 · STmate 덧붙임 칸 시작
SUB = "\x07" # 덧붙임 칸 사이
TAB = "\t"
# 검사기 열 개수(0x405bfc 뒤 cmp) · 그 밖 오류 번호는 L.Txt 203002 만 확인
COUNTS: dict[str, int] = {
"Main.Txt": 3,
"M.Txt": 19,
"L.Txt": 7,
"E.Txt": 9,
"W.Txt": 9,
"J.Txt": 9,
"GongJong.Txt": 4,
"Unit.Txt": 10,
"JUnit.Txt": 10,
"Danga.Txt": 10,
"MainPart.Txt": 31,
"UnitPart.Txt": 28,
"JPart.Txt": 19,
}
ERR_CODE = {"L.Txt": "203002"}
_PRICE = ["F_CODE", "F_DES", "F_SIZE", "F_UNIT", "F_MPRICE", "F_LPRICE", "F_EPRICE", "F_SCODE"]
_LINK = [f"F_{k}{i}" for i in range(1, 6) for k in ("LCODE", "LQTY")]
# 열 차례 → 필드 · None = 검사기가 읽지 않거나 짝 미확인
COLUMNS: dict[str, list[str | None]] = {
"M.Txt": ["F_CODE", "F_DES", "F_SIZE", "F_UNIT", "F_MPRICE", "F_SCODE"]
+ [f"F_{k}{i}" for i in range(1, 7) for k in ("PRICE", "PAGE")]
+ ["F_BIGO"],
"L.Txt": ["F_CODE", "F_DES", "F_SIZE", "F_UNIT", "F_LPRICE", "F_SCODE", "F_BIGO"],
"E.Txt": _PRICE + ["F_BIGO"],
"W.Txt": _PRICE + ["F_BIGO"],
"J.Txt": _PRICE + ["F_BIGO"],
"Unit.Txt": _PRICE + ["F_TFNO", "F_BIGO"],
"JUnit.Txt": _PRICE + ["F_TFNO", "F_BIGO"],
"Danga.Txt": _PRICE + ["F_TFNO", "F_BIGO"],
# 세부 3종(Ghidra 0x4f7890 · 0x4fa7fc · 0x4fdd10) — F_CODE1 = 앞 열 묶음 + 1열(조건부)
"MainPart.Txt": ["F_CODE", "F_CODE1", "F_DES", "F_SIZE", "F_UNIT", "F_QQTY", "F_MPRICE", "F_LPRICE"]
+ ["F_EPRICE", None, "F_SPROPER", "F_LOADBM", "F_SAVEBM", "F_SRATE", "F_KWAN", "F_SCOST"]
+ ["F_SGUBUN", "F_SCODE", "F_SANQTY"]
+ _LINK
+ ["F_LRATE", "F_SAN"],
"UnitPart.Txt": ["F_CODE", "F_CODE1", "F_DES", "F_SIZE", "F_UNIT", "F_QQTY", "F_MPRICE", "F_LPRICE"]
+ ["F_EPRICE", None, "F_SPROPER", "F_LOADBM", "F_SAVEBM", "F_SRATE", "F_KWAN", "F_SANQTY"]
+ _LINK
+ ["F_LRATE", "F_SAN"],
"JPart.Txt": ["F_CODE", "F_CODE1", None, "F_DES", "F_SIZE", "F_UNIT", "F_QQTY", "F_MPRICE"]
+ ["F_LPRICE", "F_EPRICE", None, "F_SPROPER", "F_LOADBM", "F_SAVEBM", "F_SRATE", "F_KWAN"]
+ [None, None, None],
}
# 검사기가 따로 채우는 필드 — F_SEQ(줄 번호 10자리 0 채움) · F_ORICODE(0열) · F_CLASS(MainPart·UnitPart)
# 묶음 파일 차례(검사기 파일 목록 0x4e6a20~0x4e6b58) · 설정 = Global.Ini
FILES = [
"M.Txt",
"L.Txt",
"E.Txt",
"W.Txt",
"J.Txt",
"GongJong.Txt",
"Unit.Txt",
"Danga.Txt",
"MainPart.Txt",
"UnitPart.Txt",
"JPart.Txt",
"Main.Txt",
"JUnit.Txt",
"CostAccount.Txt",
]
CODE_CHARS = set("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_/^")
def dtrim(s: str) -> str:
"""Delphi Trim — 양끝 #0~#32 걷음."""
i, j = 0, len(s)
while i < j and ord(s[i]) <= 32:
i += 1
while j > i and ord(s[j - 1]) <= 32:
j -= 1
return s[i:j]
def num_text(v) -> str:
"""FloatToStr(유효 15자리) → EXTTOSTR(DP_Common 0x453e4c · 지수꼴을 풀어 씀)."""
x = float(v or 0)
if x == 0:
return "0"
d = Decimal(f"{x:.15g}")
s = format(d, "f")
if "." in s:
s = s.rstrip("0").rstrip(".")
return s
class Raw(str):
"""값 글 손질(TrimRight) 없이 그대로 넣는 조각(낱글 리터럴)."""
def val_text(v) -> str:
"""FUN_004a7cf0 값 글 — 숫자는 num_text · 참거짓은 Delphi 6 Variant 글(조건부) · 글은 TrimRight."""
if isinstance(v, Raw):
return str(v)
if isinstance(v, bool):
return "True" if v else "False"
if isinstance(v, (int, float)):
return num_text(v)
return ("" if v is None else str(v)).rstrip(" ")
def split_line(line: bytes | str) -> tuple[list[str], list[str]]:
"""검사기 0x504d20 꼴 — (열 배열, 덧붙임 칸 배열)."""
s = line.decode("cp949", "replace") if isinstance(line, bytes) else line
s = s.rstrip("\r\n")
p = s.find(EXT)
left, right = (s, "") if p < 0 else (s[:p], s[p + 1 :])
def cut(t: str, sep: str) -> list[str]:
t += " "
out = []
while t:
k = t.find(sep)
if k < 0:
k = len(t) - 1 # 덧붙인 " " 앞까지
out.append(dtrim(t[:k]))
break
out.append(dtrim(t[:k]))
t = t[k + 1 :]
return out
return cut(left, TAB), (cut(right, SUB) if p >= 0 else [])
def build_line(cols: list, ext: list | None = None) -> bytes:
"""열 → 탭 줄 · ext 가 있으면 \\x05 뒤 \\x07 사이 · cp949."""
s = TAB.join(val_text(c) for c in cols)
if ext is not None:
s += EXT + SUB.join(val_text(c) for c in ext)
return s.encode("cp949", "replace")
def columns_of(name: str, line: bytes | str) -> dict:
"""열 배열 → {필드: 값}(이름 모르는 열은 #n)."""
cols, ext = split_line(line)
names = COLUMNS.get(name, [])
out = {(names[i] if i < len(names) and names[i] else f"#{i}"): v for i, v in enumerate(cols)}
if ext:
out["_EXT"] = ext
return out
def check_lines(name: str, text: bytes) -> list[str]:
"""검사기 꼴 — 줄마다 열 개수 · 0열 코드 글자(A~Z 0~9 - _ / ^)."""
errs = []
want = COUNTS.get(name)
for no, raw in enumerate(text.splitlines(), 1):
if not raw.strip():
continue
cols, _ = split_line(raw)
if want is not None and len(cols) != want:
code = ERR_CODE.get(name, "")
errs.append(f"{code} {name} - Line : {no} 열 개수 {len(cols)}(필요 {want})".strip())
names = COLUMNS.get(name)
if names and names[0] == "F_CODE" and cols:
bad = set(cols[0]) - CODE_CHARS
if bad:
errs.append(f"{name} - Line : {no} 코드 글자 {''.join(sorted(bad))}")
return errs