- bdqty.py — BDQTY.DBF 자리별 고정 변환(회전 + 더하기) 표 283자리로 복호(사용자 승인) · /api/load 에 bdqty_rows - smt_eval.js evaluateComposition — 행 소수1 버림 · CHBNM 총액우선(행 합계액 기준) · PR 줄(PRPT 밑수 → PROUT) · % 단위 자원 · C* 뒤 % = 전체 비율 · 중기 운전원 CONLD 시간 환산 · '$' 단위 환율 · SBMOK 경비 - 연쇄 재계산 = X·D·B 참조 순서 · 봉화 기준 표준 레시피 이름 맞추기(자기 대조) 걷음 - 원설계 5건 일위대가·중기 291개 + 영덕 변경 31개 1원 · 기초단가부터 다시 셈해도 전 단가·총공사비 그대로 - 지킴 시험 test_stmate_ilwi_eval.py · 장부 32.2% → 33.5% Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
99 lines
5.0 KiB
Python
99 lines
5.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""BDQTY.DBF(일위대가·중기 구성표) 복호 — STMATE 가 레코드 본문을 자리별 고정 변환으로 숨김(삭제표시 0x45).
|
|
|
|
변환 = 자리(레코드 안 바이트 위치)마다 고정된 (꼴, 회전 r, 값 k):
|
|
A 암호 = rol(평문, r) + k B 암호 = rol(평문 + k, r) X 암호 = rol(평문, r) ^ k
|
|
표는 실무 STC 7건에서 코드·수량 형식으로 맞춘 값(사용자 승인 하에 복호) — 글자 칸(명칭·규격)은 표에 없어 읽지 않음.
|
|
대조: resources/tester/test_stmate_ilwi_eval.py (일위대가·중기 재계산 = 저장 단가)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import struct
|
|
|
|
_TABLE = (
|
|
"1A3.53 2A0.238 3A7.233 4A4.228 5A7.17 6A3.133 7A7.33 8A5.43 9A6.170 10A5.123 11A4.156 "
|
|
"12A1.167 133X4.229 134X0.87 135A7.198 136A5.155 137A2.254 138A7.217 139A5.123 140A7.193 "
|
|
"141B1.167 142A4.132 143A3.133 144B0.8 145B3.150 146B1.155 147A1.63 148B7.226 149A6.130 "
|
|
"150A3.253 151B2.85 152A5.235 153B6.91 154X0.231 155X0.231 156X4.0 157X1.223 158X6.130 "
|
|
"159A5.27 160B3.245 161B1.107 162X5.200 163B3.230 164B5.74 165X6.94 166X1.151 167B0.207 "
|
|
"168B5.10 169B7.206 170A0.166 171A7.198 172X6.98 173X1.63 174X0.103 175X0.233 176X1.223 "
|
|
"177X3.107 178A3.173 179B0.120 180X5.145 181B5.24 182X2.225 183A7.119 184X0.144 185A2.239 "
|
|
"186X4.27 187X4.180 188X4.229 189A5.241 190A2.41 191A5.232 192A6.154 193X3.143 194A7.70 "
|
|
"195X2.62 196X3.6 197X0.215 198A0.232 199B1.207 200X5.136 201A5.179 202B7.78 203X2.33 "
|
|
"204A6.154 205B3.214 206B5.74 207A1.176 208B6.174 209B6.110 210X5.249 211A6.154 212B0.194 "
|
|
"213B7.99 214A2.174 215A5.107 216B7.206 217X7.228 218B1.239 219B3.181 220X7.173 221A2.209 "
|
|
"222A1.155 223A1.155 224A1.191 225B2.62 226X4.128 227A6.209 228B6.64 229X0.105 230X6.167 "
|
|
"231X0.215 232A3.128 233B6.107 234A2.209 235A6.90 236B5.154 237A5.107 238A1.185 239B6.20 "
|
|
"240B2.85 241X1.22 242X1.166 243B7.66 244B7.114 245X7.22 246X3.167 247A2.70 248B3.167 "
|
|
"249B6.168 250A1.39 251X2.0 252A5.147 253A3.85 254A6.106 255A3.93 256X0.208 257A7.65 "
|
|
"258A5.203 259A7.41 260A3.117 261A6.34 262A3.181 263B5.101 264A7.177 265A7.1 266A1.55 "
|
|
"267B1.151 268B7.133 269A0.175 270X6.98 271B1.151 272B1.139 273A5.203 274A7.113 275A2.238 "
|
|
"276X7.193 277A6.74 278A5.83 279A7.97 280B5.154 281B3.174 282A2.70 283A7.233 284B6.104 "
|
|
"285B5.24 286X1.63 287B2.94 288B5.92 289B4.201 290A3.197 291B6.168 292B1.203 293B5.26 "
|
|
"294A5.123 295A3.253 296X6.178 297X2.241 298X5.136 299A3.197 300A4.12 301B5.89 302B6.15 "
|
|
"303A3.183 304X6.167 305B0.112 306B5.209 307A4.165 308X0.215 309B7.142 310B1.49 311A3.253 "
|
|
"312A2.74 313X7.230 314X2.161 315A1.231 316B3.165 317A0.230 318B7.14 319A4.115 320A3.128 "
|
|
"321B5.74 322A2.41 323A1.159 324A4.100 325A7.17 326B3.4 327X6.159 328A0.175 329B1.239 "
|
|
"330A0.38 331B1.239 332X1.93 333X2.58 334X5.63 335X3.96 336X0.247 337X6.183 338X1.102 "
|
|
"339A0.150 340X0.96 341X2.55 342A5.125 343A2.209 344B4.137 345A3.119 346B6.247 347X5.54 "
|
|
"348A6.26 349X2.225 350X5.127 351A5.177 352B2.103 353A6.99 354B2.85 355B6.183 356X6.167 "
|
|
"357X3.136 358A6.238 359A6.154 360X1.38 361A4.124 362X0.41 374A1.239 375A4.92 376X6.55 "
|
|
"377B2.62 378X7.49 379B7.226 380B2.129 381A7.145 382X1.214 383A6.94 384B4.222 385B5.152 "
|
|
"386A7.17 387B2.125 388A3.64 389A6.222 390A7.198 391A1.119 392B1.99 393A3.213 394A5.134 "
|
|
"395B5.81 396A7.198 397B4.95 398A5.107 399B7.178 400X2.55 401X5.74 402B3.174 403A0.134 "
|
|
"404A3.53 405A1.191 406X4.229 407X1.63 408X5.182 409B7.178 410B6.42 411B6.137 412A2.218 "
|
|
"413A5.235 414B7.162 "
|
|
)
|
|
|
|
|
|
def _params() -> dict[int, tuple[str, int, int]]:
|
|
out = {}
|
|
for tok in "".join(_TABLE).split():
|
|
i = next(j for j, ch in enumerate(tok) if ch in "ABX")
|
|
r, k = tok[i + 1 :].split(".")
|
|
out[int(tok[:i])] = (tok[i], int(r), int(k))
|
|
return out
|
|
|
|
|
|
PARAMS = _params()
|
|
|
|
|
|
def _ror(v: int, r: int) -> int:
|
|
return ((v >> r) | (v << (8 - r))) & 0xFF if r else v
|
|
|
|
|
|
def _dec(c: int, p: tuple[str, int, int]) -> int:
|
|
kind, r, k = p
|
|
if kind == "A":
|
|
return _ror((c - k) & 0xFF, r)
|
|
if kind == "B":
|
|
return (_ror(c, r) - k) & 0xFF
|
|
return _ror(c ^ k, r)
|
|
|
|
|
|
def decode_bdqty(b: bytes) -> list[dict]:
|
|
"""복호 가능한 칸만 담은 행 목록(레코드 순서 유지)."""
|
|
if len(b) < 32:
|
|
return []
|
|
n, hlen, rlen = struct.unpack_from("<IHH", b, 4)
|
|
fields, o, off = [], 32, 1
|
|
while o < hlen and b[o] != 0x0D:
|
|
name = b[o : o + 11].split(b"\x00")[0].decode("ascii", "ignore")
|
|
fields.append((name, off, b[o + 16]))
|
|
off += b[o + 16]
|
|
o += 32
|
|
rows = []
|
|
for i in range(n):
|
|
rec = b[hlen + i * rlen : hlen + (i + 1) * rlen]
|
|
if len(rec) < rlen:
|
|
break
|
|
row = {}
|
|
for name, foff, ln in fields:
|
|
span = range(foff, foff + ln)
|
|
if all(j in PARAMS for j in span):
|
|
raw = bytes(_dec(rec[j], PARAMS[j]) for j in span)
|
|
row[name] = raw.decode("cp949", "replace").strip()
|
|
rows.append(row)
|
|
return rows
|