fix(est-test): 구버전 EST Plus 저장 — cp949 밖 글자·기계경비 Index·% 수량, 경쟁사 실물 시험
- cp949 에 없는 글자는 「?」 로 바꿔 씀(22MB 공사 저장 중 멈춤) - 기계경비 본문 Index(−3·−2·−1) 유지 - % 단위 수량은 소수로 써서 음수·큰 값도 되돌아옴 - 경쟁사 예제 estx 2건 저장값 대조·구버전 왕복 시험 추가 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -51,3 +51,47 @@ def test_pair_flags_sum_mismatch_in_order():
|
||||
})()""",
|
||||
)
|
||||
assert result == [["U1", False], ["U2", True]]
|
||||
|
||||
|
||||
def test_percent_quantity_machine_index_and_non_cp949_text():
|
||||
result = node(
|
||||
"""
|
||||
const L=require('./legacy-estplus.cjs');
|
||||
const p={name:'t',options:{},tables:{EstMainDat:[],HeadMainDat:[{Index:1,Name:'a'},
|
||||
{Index:0,LinkIndex:0,Name:'사용고재',Unite:'%',GN:-500},{Index:0,LinkIndex:0,Name:'공구손료',Unite:'%',GN:3},
|
||||
{Index:0,LinkIndex:0,Name:'x'+String.fromCodePoint(0x1F600)+'y',GN:1}],
|
||||
MachMainDat:[{Index:1,Name:'굴삭기',TC:10},{Index:-3,Selected:1,Name:'손료',GN:2,JC:5},{Index:-1,Selected:11,Name:'운전사',GN:1,NC:7}],
|
||||
CostMainDat:[],LaborcDat:[],StuffcDat:[],SilJukDat:[],Wonga:[]}};
|
||||
const b=L.read(L.write(p),'t');
|
||||
console.log(JSON.stringify({gn:b.tables.HeadMainDat.slice(1,3).map(r=>r.GN),name:b.tables.HeadMainDat[3].Name,
|
||||
mach:b.tables.MachMainDat.map(r=>[r.Index,r.Selected,r.TC])}));
|
||||
""")
|
||||
assert result["gn"] == [-500, 3]
|
||||
assert result["name"] == "x?y"
|
||||
assert result["mach"] == [[1, 0, 10], [-3, 1, 5], [-1, 11, 7]]
|
||||
|
||||
|
||||
def test_competitor_examples_keep_stored_values_and_round_trip_to_legacy():
|
||||
folder = ROOT / "resources/knowledge/original/경쟁사 프로그램/EST Plus 2/example"
|
||||
files = sorted(folder.glob("*.estx"))
|
||||
assert len(files) >= 2
|
||||
for file in files:
|
||||
parity = json.loads(
|
||||
subprocess.check_output(
|
||||
["node", str(ROOT / "resources/tester/helper_est_archive.cjs"), str(file)],
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=300,
|
||||
)
|
||||
)
|
||||
assert parity["differences"] == [], file.name
|
||||
result = node(
|
||||
f"""
|
||||
const L=require('./legacy-estplus.cjs'),{{importEstx}}=require('./import-estx.cjs');
|
||||
const p=importEstx(require('fs').readFileSync({json.dumps(str(file))}),'x'),b=L.read(L.write(p),'t');
|
||||
const diff=(t,ks)=>p.tables[t].filter((r,i)=>ks.some(k=>Math.abs((Number(r[k])||0)-(Number(b.tables[t][i][k])||0))>1e-6)).length;
|
||||
console.log(JSON.stringify({{est:diff('EstMainDat',['GN','TC','NC','GC','JC']),head:diff('HeadMainDat',['GN','TC','NC','GC','JC']),
|
||||
cmp:L.compare(L.write(p),2).length===p.tables.HeadMainDat.filter(r=>r.Index>0).length}}));
|
||||
"""
|
||||
)
|
||||
assert result == {"est": 0, "head": 0, "cmp": True}, file.name
|
||||
|
||||
@@ -515,6 +515,9 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD
|
||||
- [x] 구버전 EST Plus 자료 불러오기·저장·단가비교(spec_misc §2 · 원본 역분석 spec_estplus — 레코드 394/565 바이트, Extended·ShortString) — 불러오기: .est/.ebk 를 풀어 내역·일위·단산·기계·노임·자재·표준·견적(565)·원가와 설계변경 c* 를 표로, 저장 금액 유지(자동계산 끔), 호표 머리에 코드를 매기고 본문 연결 행은 같은 품명·규격 원천 코드를 붙임(웹 보충). 저장(Ctrl+Shift+F12): 394 판 .est. 단가비교(Ctrl+Alt+E): 기계경비·단가산출·일위대가·내역서 중 골라 구형 파일 머리 레코드와 차례 짝, 합계 다르면 표시, 더블클릭 이동. 메모(.MEM)·기계 기준표(.mot)는 읽지 않음.
|
||||
- 미결: 실제 구버전 .est 표본이 없어 웹이 만든 파일로만 왕복 확인 — 원본 파일 대조 필요(문자 코드 cp949 가정).
|
||||
- 검증완료: 구버전 시험 2개 통과(Extended 왕복, 샘플 공사 쓰기→읽기 금액 일치, 단가산출 글줄·머리 꼬리표, 품명·규격 연결, 단가비교 자리). ORCA 브라우저에서 .est 저장 17,864 바이트 → 다시 불러오기 76행, 단가비교 창·일위 8건 일치, 원복.
|
||||
- 경쟁사 실물 시험(지식DB 경쟁사 폴더 estx 3건 — 예제 2 · 다산소프트 main 22MB, 구버전 .est 실물은 없음): 저장값 대조 차이 0(예제1 원본 괄호 오류 1건은 원본 그대로) · ESTX 그대로 저장 = 원본과 같은 바이트 · 근거 메모 125건 글자 일치·그림 42개 보존·고친 메모 ESTX 저장 왕복 · 구버전 .est 쓰기→읽기 내역·일위 금액 차이 0 · 단가비교 불일치 0 · 화면 13종·환경설정·인쇄 미리보기·작업모드 전환 오류 없음.
|
||||
- 고친 것: 구버전 저장 때 cp949 에 없는 글자는 「?」, 기계경비 본문 Index(−3·−2·−1) 유지, % 단위 수량은 소수로 써서 음수·큰 값도 되돌아옴.
|
||||
- 남은 관찰: 동원 확인에서 단산·일위 일부(예제 15·1건, main 133·57건)가 10원 넘게 어긋남 — 웹 동원 계산의 줄별 절사 차이로 보임, 원본 확인 탭도 같은지 미확인.
|
||||
- [x] 다른 공사 불러오기 창(원본 barbtnSelfLoad → TfrmOtherLoad, 찾기 popSerch) — 저장된 다른 작업을 골라 같은 표를 띄우고 찾기(품명·규격, A&B 둘 다)·고른 줄을 하위 자료와 함께 가져오기(내역서는 고른 줄 아래에 줄, 나머지는 자료·호표 합침, 같은 코드는 이 공사 것). 검증완료: 바인딩 통과, ORCA 브라우저에서 산불진화임도 작업의 일위대가 3개 → 「취수장」 1개 → 가져오기 호표 8→9·하위 41건, 샘플 복구.
|
||||
- [x] 사용위치 검색 창(원본 TfrmUsesDataSerch btnSerch·btnAllDisplay) — 현재 표 항목을 품명·규격(빈칸·|·괄호 무시)으로 거르고 골라 사용위치 보기. 검증완료: 바인딩 통과, ORCA 브라우저에서 노임 9개 → 「보 통」 1개 → 보통인부 사용위치 11곳.
|
||||
- [x] 인쇄양식 불러오기·저장하기(원본 mnuPrintFormatLoad·Save 0x10ed318·0x10ed000 — spec_pc §3) — .pfot(인쇄정보표 MIDAS 이진) 읽기·쓰기 `print-format.cjs`(설명 BLOB·빈 칸 표지까지 보존, 원본 파일 그대로 다시 쓰면 바이트 같음). 불러오면 공사 인쇄표를 그 값으로, 저장은 지금 인쇄표를 원본 설치 main.pfot 필드 설명자 위에(`EST_PFOT_TEMPLATE`). .gfot(cxGrid 열 배치)는 웹 격자와 호환되지 않아 하지 않음.
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
// 구버전 EST Plus 레코드 형식(spec_estplus.md): 머리·표별 가변부 자리, Extended·ShortString 읽기·쓰기, 레코드 풀기.
|
||||
const { cp949 } = require("./export-estx.cjs");
|
||||
const { cp949: strict } = require("./export-estx.cjs");
|
||||
// cp949 에 없는 글자(제어 기호 밖 특수 문자 등)는 「?」 로 바꿔 씀 — 구형 레코드는 cp949 만 담음
|
||||
function cp949(text) {
|
||||
try {
|
||||
return strict(text);
|
||||
} catch {
|
||||
return Buffer.concat(
|
||||
[...String(text)].map((ch) => {
|
||||
try {
|
||||
return strict(ch);
|
||||
} catch {
|
||||
return Buffer.from("?");
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const HEAD = {
|
||||
394: {
|
||||
@@ -206,6 +222,7 @@ function decode(rec, size, type) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
cp949,
|
||||
HEAD,
|
||||
AMOUNTS,
|
||||
BODY,
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
// 쓰기(ActionConvertToEstPlus → 0xd96354/0xd960b8): 394 판으로 위 표 파일 + .ini 를 ZIP 으로.
|
||||
// 단가비교(TfrmCompare 0xf8998c): 고른 종류 파일의 Index > 0 레코드 노무·재료·경비 금액을 차례로.
|
||||
const { pack, unpack } = require("./archive.cjs");
|
||||
const { cp949 } = require("./export-estx.cjs");
|
||||
|
||||
const num = (v) => Number(v) || 0;
|
||||
const {
|
||||
@@ -22,6 +21,7 @@ const {
|
||||
writeS,
|
||||
records,
|
||||
decode,
|
||||
cp949,
|
||||
} = require("./legacy-estplus-layout.cjs");
|
||||
|
||||
const link = (v) => LINK_IN[v] ?? v;
|
||||
@@ -221,7 +221,7 @@ function convert(files, name) {
|
||||
} else if (r.Index === -3)
|
||||
t.MachMainDat.push({
|
||||
...base,
|
||||
Index: 0,
|
||||
Index: r.Index,
|
||||
Selected: 1,
|
||||
Size: "",
|
||||
GN: r.X200,
|
||||
@@ -233,7 +233,7 @@ function convert(files, name) {
|
||||
else if (r.Index === -2)
|
||||
t.MachMainDat.push({
|
||||
...base,
|
||||
Index: 0,
|
||||
Index: r.Index,
|
||||
Selected: 6,
|
||||
Size: clean(r.Size),
|
||||
GN: r.X190,
|
||||
@@ -248,7 +248,7 @@ function convert(files, name) {
|
||||
else if (r.Index === -1)
|
||||
t.MachMainDat.push({
|
||||
...base,
|
||||
Index: 0,
|
||||
Index: r.Index,
|
||||
Selected: 11,
|
||||
Size: clean(r.Size),
|
||||
GN: r.X190,
|
||||
@@ -502,8 +502,17 @@ function write(project) {
|
||||
out = [];
|
||||
const add = (ext, type, rows) =>
|
||||
rows.length && out.push([`data.${ext}`, Buffer.concat(rows.map((r) => record(type, r)))]);
|
||||
add("dtm", "dtm", t.EstMainDat || []);
|
||||
add("hdt", "hdt", t.HeadMainDat || []);
|
||||
// % 단위 수량은 소수로 씀(읽을 때 1 미만이면 ×100 — 음수·100 넘는 값도 되돌아오게)
|
||||
const pct = (rows) =>
|
||||
rows.map((r) =>
|
||||
["%", "%"].includes(String(r.Unite ?? "").trim()) &&
|
||||
![51, 52, 57, 58, 62, 64].includes(num(r.LinkIndex)) &&
|
||||
!(num(r.Index) > 0)
|
||||
? { ...r, GN: num(r.GN) / 100 }
|
||||
: r,
|
||||
);
|
||||
add("dtm", "dtm", pct(t.EstMainDat || []));
|
||||
add("hdt", "hdt", pct(t.HeadMainDat || []));
|
||||
add(
|
||||
"cct",
|
||||
"cct",
|
||||
|
||||
Reference in New Issue
Block a user