// REA f2a1fc/f254c8: LineData 표지와 일반·성분·마지막 합계의 정밀도. const EstText = (() => { const { num, components, floating, sum } = EstMath; const marker = /\x12([-\d.,]+)\x12/; const component = { [-1]: "NC", [-2]: "GC", [-3]: "JC", [-4]: "SC" }; const clean = (s) => String(s || "") .replace(/[\x06\x07][^\x06\x07]*[\x06\x07]/g, "") .replace(/[\x03\x0c\x0e\x11\x12\x13\x15\x1a]/g, ""); const numeric = (s) => Number(String(s).replaceAll(",", "")); const sumCurrency = (values) => values.reduce((n, value) => n + EstMath.even(num(value) * 10000), 0) / 10000; const stripLabels = (text) => String(text).replace(/\x0e[^\x0e]*\x0e|\x13[^\x13]*\x13|\x15[^\x15]*\x15/g, ""); function expression(text) { return clean( String(text).replace( /\x0e[^\x0e]*\x0e|\x13[^\x13]*\x13|\x15[^\x15]*\x15|\{[^}]*\}|\([가-힣][^()]*\)/g, "", ), ) .replace( /(\d)\s*(?:공\/?㎥|공|단|면|경간|개월|월|인|㎥|㎢|㎡|hr|sec|min|km|ton|kg|㎏|㎞|ℓ|회|개소|개|대|분|초|본|장|매|병|조|식|일|시간|원|포|동|μ|EA|mm|cm|m|g|L)(?:\/(?:개소|개|대|본|인|분|포|\$|hr|일|㎥|㎡|m|a))?/gi, "$1", ) .replace(/(?<=\d)\s*\/(?:일|㎥|㎡)(?=\s|$)/g, "") .replace(/(?<=\))\s*(?:개|인|본|면|단)(?:\/(?:㎡|㎥|hr))?/g, "") .replace(/[①-⑳]/g, "") .replace(/=\s*$/, ""); } function calculate(input, options = {}, resolve = () => null) { const rows = structuredClone(input), titles = [], errors = [], checks = []; let head = null, variables = Object.create(null), changed = new Map(), currentSource = null, totals = {}, levelTotals = new Map(), finalLevel = -1; function finish() { if (!head) return; components.forEach((key) => (head[key] = num(totals[key]))); head.TC = sum(head); titles.push({ ...head }); } for (let index = 0; index < rows.length; index++) { const row = rows[index]; if (num(row.Index) > 0) { finish(); head = row; variables = Object.create(null); changed = new Map(); currentSource = null; totals = {}; levelTotals = new Map(); for (const [key, tag] of [ ["Name", "\x0e"], ["Size", "\x13"], ["Unite", "\x15"], ]) { const match = String(row.LineData || "").match(new RegExp(`${tag}([^${tag}]+)${tag}`)); if (match) row[key] = match[1]; } const levels = []; for (let j = index + 1; j < rows.length && num(rows[j].Index) <= 0; j++) if (String(rows[j].LineData || "").includes("\x1a")) levels.push(num(rows[j].Level)); finalLevel = levels.length ? Math.min(...levels) : -1; continue; } if (!head || !row.LineData) continue; const original = String(row.LineData), matched = original.match(marker), sc = num(row.SCheck); const oldValue = matched ? numeric(matched[1]) : null; if (num(row.LinkIndex) > 0 && /\x0e/.test(original)) { const name = original.match(/\x0e([^\x0e]+)\x0e/), size = original.match(/\x13([^\x13]+)\x13/); try { currentSource = resolve({ ...row, Name: name?.[1], Size: size?.[1] }); } catch (error) { errors.push(`${head.Code}:${index + 1} ${error.message}`); } } let text = original; if (currentSource && sc < 0 && component[sc]) text = text.replace( /\x03([-\d.,]+)\x03/g, () => `\x03${num(currentSource[component[sc]])}\x03`, ); else if (currentSource && sc === 0 && /\x0e/.test(original)) text = text.replace(/\x03([-\d.,]+)\x03/g, () => `\x03${num(currentSource.TC)}\x03`); text = text.replace( /\x11([-\d.,]+)\x11/g, (whole, value) => `\x11${changed.get(numeric(value)) ?? numeric(value)}\x11`, ); text = text.replace( /\x0c([-\d.,]+)\x0c/g, (whole, value) => `\x0c${changed.get(numeric(value)) ?? numeric(value)}\x0c`, ); if (text.includes("\x1a") && text.includes(":") && sc !== -5) { const start = text.indexOf(":") + 1, end = text.includes("\x12") ? text.indexOf("\x12") : text.length; text = text.slice(0, start) + text .slice(start, end) .replace(/\d[\d,]*\.?\d*/g, (value) => String(changed.get(numeric(value)) ?? numeric(value)), ) + text.slice(end); row.LineData = text; } let calculated = null, digits = 0; const assignedResults = new Map(); const assignedNames = new Map(); try { if (matched && /─/.test(text)) { const above = clean(rows[index - 1]?.LineData), below = clean(rows[index + 1]?.LineData); const tops = above.trim().split(/\s{2,}/), bottoms = below.trim().split(/\s{2,}/); let part = 0; const formula = text .slice(text.indexOf("=") + 1) .split("=")[0] .replace(/─+/g, () => { const i = part++; return `(${expression(tops[i] || "")})/(${expression(bottoms[i] || "")})`; }); calculated = EstExpression.evaluate(expression(formula), (key) => variables[key]); } else if (matched && text.includes("\x1a")) { if (sc === -5) calculated = sumCurrency(Object.values(levelTotals.get(num(row.Level)) || totals)); else { let body = clean(text.slice(0, text.indexOf("\x12"))) .replace(/=\s*$/, "") .trim(); if (body.includes(":")) body = body.slice(body.indexOf(":") + 1); calculated = body ? EstExpression.evaluate(body, (key) => variables[key]) : 0; } } else if (matched && sc === -5) { const section = []; for ( let j = index - 1; j >= 0 && num(rows[j].Index) <= 0 && num(rows[j].Level) <= 0 && num(rows[j].SCheck) !== -5; j-- ) if (component[num(rows[j].SCheck)]) section.push(num(rows[j].TC)); calculated = sumCurrency(section); } else if (matched && sc < 0) { let body = stripLabels(text.slice(0, text.indexOf("\x12"))).replace(/=\s*$/, ""); if (body.includes(":")) body = body.slice(body.indexOf(":") + 1); body = body.split("=")[0]; calculated = EstExpression.evaluate(expression(body), (key) => variables[key]); } const assignmentText = text.replace( /\{[^}]*\}|\x0e[^\x0e]*\x0e|\x13[^\x13]*\x13|\x15[^\x15]*\x15/g, (value) => " ".repeat(value.length), ); const assignments = sc < 0 ? [] : [ ...assignmentText.matchAll( /(?:^\s*| {2,}|[,,]\s*|(?<=[가-힣])\s+|[::]\s*)([A-Za-z][A-Za-z0-9]*)\s*=/g, ), ]; for (let j = 0; j < assignments.length; j++) { const assignment = assignments[j], end = assignments[j + 1]?.index ?? text.length; let body = text.slice(assignment.index + assignment[0].length, end); body = body.split(/\{(?!%)|\s+[::]\s*/)[0].replace(/[},,]\s*$/, ""); if (body.includes("─") && calculated !== null) { variables[assignment[1]] = floating(calculated, 2, 1); continue; } const result = body.match(marker); const parts = body.split("="); body = parts[0]; body = expression(body) .trim() .replace(/\s+(?:min|hr|sec)\s*$/i, "") .split(/\s{2,}(?=\d+\s*[가-힣]|[가-힣])/)[0]; // 단위가 붙은 상수와 주석은 수치 부분만 사용한다. const literal = body.match(/^([-+]?\d[\d,.]*)(?:\s*[A-Za-z가-힣㎥㎡ℓ˚°()].*)$/); if (literal) body = literal[1]; let value; try { value = EstExpression.evaluate(body, (key) => variables[key]); } catch (error) { if (!result && error.message.startsWith("참조 항목 없음:")) continue; if (parts.length > 1) value = EstExpression.evaluate( expression(parts[parts.length - 2]), (key) => variables[key], ); else throw Error(`${error.message} (${assignment[1]} = ${body})`); } const savedDigits = result?.[1].includes(".") ? result[1].split(".")[1].length : 0; const precision = result ? Math.max(2, savedDigits) : 8; const explicit = num(row.DigetIndex) ? EstMath.precision(row) : null; variables[assignment[1]] = result ? floating(value, row.Precision ?? explicit?.digits ?? precision, explicit?.method ?? 1) : value; if (result) { const position = text.indexOf("\x12", assignment.index + assignment[0].length); assignedResults.set(position, variables[assignment[1]]); assignedNames.set(position, assignment[1]); if (position === text.indexOf("\x12")) calculated = variables[assignment[1]]; } } if (matched && calculated === null && sc === 0 && text.includes("=")) { let body = stripLabels(text.slice(0, text.indexOf("\x12"))) .replace(/\x06[^\x06]*\x06/g, "") .split("=") .filter((x) => x.trim()) .pop(); if (body.includes(":")) body = body.slice(body.indexOf(":") + 1); calculated = EstExpression.evaluate(expression(body), (key) => variables[key]); } if (matched && calculated !== null) { const explicit = num(row.DigetIndex) ? EstMath.precision(row) : null; digits = row.Precision ?? explicit?.digits ?? (sc < 0 || num(row.LinkIndex) !== 0 ? num(row.Level) === finalLevel && text.includes("\x1a") ? 0 : 1 : Math.max(2, matched[1].split(".")[1]?.length || 0)); const method = row.RoundMode || explicit?.method || (sc < 0 || num(row.LinkIndex) !== 0 ? 2 : 1); calculated = floating(calculated, digits, method); const firstValue = calculated; let rendered = text.replace(marker, `\x12${calculated}\x12`); const results = [...text.matchAll(/\x12([-\d.,]+)\x12/g)]; let assignmentName = assignedNames.get(results[0]?.index); for (let part = 1; part < results.length; part++) { const previous = results[part - 1], next = results[part]; const format = row.ResultFormats?.[part]; const operation = expression( text.slice(previous.index + previous[0].length, next.index), ).trim(); calculated = assignedResults.has(next.index) ? assignedResults.get(next.index) : floating( EstExpression.evaluate( expression(`${calculated}${operation}`), (key) => variables[key], ), format?.digits ?? digits, format?.method ?? method, ); assignmentName = assignedNames.get(next.index) || assignmentName; if (assignmentName) variables[assignmentName] = calculated; const old = numeric(next[1]); let occurrence = 0; rendered = rendered.replace(/\x12([-\d.,]+)\x12/g, (value) => occurrence++ === part ? `\x12${calculated}\x12` : value, ); changed.set(old, calculated); checks.push({ slot: index, part, stored: old, calculated }); } row.TC = calculated; if (currentSource && text.includes("\x03")) { let body = stripLabels(text.slice(0, text.indexOf("\x12"))).split("=")[0]; if (body.includes(":")) body = body.slice(body.indexOf(":") + 1); try { row.ResourceFactor = EstExpression.evaluate( expression(body.replace(/\x03[-\d.,]+\x03/g, "1")), (key) => variables[key], ); row.ResourceCode = currentSource.Code; row.ResourceKind = currentSource.SourceKind; row.ResourceComponent = component[sc] || ""; } catch { row.ResourceFactor = null; } } row.LineData = rendered; changed.set(oldValue, firstValue); checks.push({ slot: index, stored: oldValue, calculated: firstValue }); if (text.includes("\x1a") && component[sc]) { if (sc === -1 || !levelTotals.has(num(row.Level))) levelTotals.set(num(row.Level), {}); levelTotals.get(num(row.Level))[component[sc]] = calculated; if (num(row.Level) === finalLevel) totals[component[sc]] = calculated; } } else if (!matched && component[sc] && text.includes("\x03") && !text.includes("\x1a")) { const body = stripLabels(text.slice(text.indexOf(":") + 1)); row.TC = floating( EstExpression.evaluate(expression(body), (key) => variables[key]), 1, ); row.LineData = text; } else if (!matched && text.includes("\x1a") && component[sc]) { const value = clean(text).split(":").slice(1).join(":").trim(); const calculated = floating( changed.get(numeric(value)) ?? numeric(value), num(row.Level) === finalLevel ? 0 : 1, ); if (sc === -1 || !levelTotals.has(num(row.Level))) levelTotals.set(num(row.Level), {}); levelTotals.get(num(row.Level))[component[sc]] = calculated; if (num(row.Level) === finalLevel) totals[component[sc]] = calculated; } } catch (error) { errors.push(`${head.Code}:${index + 1} ${error.message}`); row.Error = error.message; } } finish(); return { rows, titles, errors, checks }; } const names = { "\x03": "단가", "\x0c": "다른줄", "\x0e": "품명", "\x11": "변수", "\x12": "결과", "\x13": "규격", "\x15": "단위", "\x1a": "합계", }; const display = (text) => String(text || "").replace(/[\x03\x0c\x0e\x11\x12\x13\x15\x1a]/g, (ch) => `⟦${names[ch]}⟧`); const encode = (text) => String(text).replace( /⟦([^⟦⟧]+)⟧/g, (whole, name) => Object.keys(names).find((key) => names[key] === name) || whole, ); return { calculate, clean, display, encode }; })();