613 lines
27 KiB
JavaScript
613 lines
27 KiB
JavaScript
// 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 autoTotal = (row) =>
|
||
num(row.LinkIndex) !== 62 && String(row.LineData || "").includes("\x1a");
|
||
const stripLabels = (text) =>
|
||
String(text)
|
||
.replace(/\x0e[^\x0e]*\x0e|\x13[^\x13]*\x13|\x15[^\x15]*\x15|\([가-힣][^()]*\)/g, "")
|
||
.replace(/[;;]/g, ":");
|
||
const mathBraces = (text) =>
|
||
String(text).replace(/\{([^{}]*)\}/g, (_, content, offset, source) =>
|
||
(!source.slice(0, offset).trimEnd() ||
|
||
/[=+×÷*/(]$/.test(source.slice(0, offset).trimEnd())) &&
|
||
/\d/.test(content) &&
|
||
/[+×÷*/]/.test(content) &&
|
||
!/[가-힣]/.test(content)
|
||
? `(${content})`
|
||
: "",
|
||
);
|
||
function expression(text) {
|
||
return clean(
|
||
mathBraces(text)
|
||
.replace(///g, "/")
|
||
.replace(
|
||
/\x0e[^\x0e]*\x0e|\x13[^\x13]*\x13|\x15[^\x15]*\x15|\{[^}]*\}|[〔[][^〕]]*[〕]]|\[[가-힣][^\]]*\]|\([가-힣][^()]*\)|\(\s*\d+(?:\.\d+)?\s*[˚°]\s*\)|<[^>]*>/g,
|
||
"",
|
||
),
|
||
)
|
||
.replace(/^\s*[가-힣]+\s+\d+조당\s+[가-힣]+\s+(?=\d)/, "")
|
||
.replace(
|
||
/(?<![A-Za-z0-9_가-힣.])(\d[\d,.]*|\.\d+)\s*(?:공\/?㎥|공|단|면|겹|경간|개월|월|년|인|㎥|㎢|㎠|㎡|hr|sec|min|set|kwh|kw|km|knot|ton|kg|㎏|㎞|ℓ|구멍|구역|지점|마일|척|열|회|개소|개항목|개|대|분기|분|초|본|장|매|병|조|식|일|시간|원|포|동|주|톤|배|번|줄|권|μ|₩|₩|\\|EA|DM|㎜|㎝|mm|cm|m3|m2|m|g|L|a)(?:\/(?:개소|경간|구멍|지점|개|대|본|인|분|포|매|주|톤|공|회|조|줄|월|span|set|ton|\$|hr|일|㎥|㎡|m3|m2|m|a|EA))?/gi,
|
||
"$1",
|
||
)
|
||
.replace(/(?<=\d)\s*\/\s*(?:일|㎥|㎡|hr)(?=\s|$|[×*+)])/gi, "")
|
||
.replace(/[】]〕]\s*$/, "")
|
||
.replace(/(?<=\))\s*(?:개|인|본|면|단|회|㎥|㎡)(?:\/(?:㎡|㎥|hr))?/g, "")
|
||
.replace(/^\s*[-*•※⊙◎]?\s*[가-힣㎡㎥][가-힣㎡㎥\s]*\s+(?=[\d.(])/, "")
|
||
.replace(/^\s*[⊙◎∴]\s*/, "")
|
||
.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,
|
||
finalOverride = null,
|
||
totals = {},
|
||
finalLevel = -1,
|
||
plainTotals = {},
|
||
singleComponent = -1,
|
||
hasAutoTotals = false;
|
||
function finish() {
|
||
if (!head) return;
|
||
if (finalOverride) {
|
||
components.forEach((key) => (head[key] = num(finalOverride[key])));
|
||
head.TC = sum(head);
|
||
titles.push({ ...head });
|
||
return;
|
||
}
|
||
components.forEach(
|
||
(key) => (head[key] = !hasAutoTotals ? floating(num(plainTotals[key])) : num(totals[key])),
|
||
);
|
||
head.TC = sum(head);
|
||
titles.push({ ...head });
|
||
}
|
||
for (let index = 0; index < rows.length; index++) {
|
||
const row = rows[index];
|
||
currentSource = null;
|
||
delete row.Error;
|
||
for (const key of ["ResourceFactor", "ResourceCode", "ResourceKind", "ResourceComponent"])
|
||
delete row[key];
|
||
if (num(row.Index) > 0) {
|
||
finish();
|
||
head = row;
|
||
variables = Object.create(null);
|
||
changed = new Map();
|
||
currentSource = null;
|
||
finalOverride = null;
|
||
totals = {};
|
||
plainTotals = {};
|
||
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 = [],
|
||
componentRows = [];
|
||
for (let j = index + 1; j < rows.length && num(rows[j].Index) <= 0; j++)
|
||
if (autoTotal(rows[j])) levels.push(num(rows[j].Level));
|
||
else if (component[num(rows[j].SCheck)] && marker.test(String(rows[j].LineData || "")))
|
||
componentRows.push(j);
|
||
finalLevel = levels.length ? Math.min(...levels) : -1;
|
||
hasAutoTotals = levels.length > 0;
|
||
singleComponent =
|
||
!hasAutoTotals && num(head.SCheck) > 0 && componentRows.length === 1
|
||
? componentRows[0]
|
||
: -1;
|
||
continue;
|
||
}
|
||
if (!head || !row.LineData) continue;
|
||
if (num(row.Level) > 0) finalOverride = null;
|
||
if ([51, 52].includes(num(row.LinkIndex))) {
|
||
if (num(row.LinkIndex) === 52) {
|
||
const rate = String(row.LineData).match(/\x15([-\d.,]+)\x15/);
|
||
const multiplier = rate ? numeric(rate[1]) / 100 : 0;
|
||
const base = Object.fromEntries(components.map((key) => [key, 0]));
|
||
for (let j = index - 1; j >= 0; j--) {
|
||
const previous = rows[j],
|
||
key = component[num(previous.SCheck)];
|
||
if (num(previous.Level) > 0 || num(previous.Index) > 0) break;
|
||
if (!key) continue;
|
||
const values = [
|
||
...String(previous.LineData || "").matchAll(/([\x12\x0c])([-\d.,]+)\1/g),
|
||
];
|
||
const value = values.length ? numeric(values.at(-1)[2]) : num(previous.TC);
|
||
// f2c680/f2c74e overwrites NC while walking backward; the other components add.
|
||
base[key] = key === "NC" ? value : base[key] + value;
|
||
}
|
||
components.forEach(
|
||
(key) => (row[key] = EstMath.even(base[key] * multiplier * 10000) / 10000),
|
||
);
|
||
row.TC = sum(row);
|
||
}
|
||
finalOverride = row;
|
||
continue;
|
||
}
|
||
const original = String(row.LineData),
|
||
matched = original.match(marker),
|
||
sc = num(row.SCheck);
|
||
const oldValue = matched ? numeric(matched[1]) : null;
|
||
if (
|
||
[5, 6, 21, 101, 103, 104, 105].includes(num(row.LinkIndex)) &&
|
||
(row.Code || /[\x0e\x03]/.test(original))
|
||
) {
|
||
const name = original.match(/\x0e([^\x0e]+)\x0e/),
|
||
size = original.match(/\x13([^\x13]+)\x13/);
|
||
try {
|
||
currentSource = null;
|
||
currentSource = resolve({
|
||
...row,
|
||
Name: name?.[1] ?? row.Name,
|
||
Size: size?.[1] ?? row.Size,
|
||
});
|
||
} catch (error) {
|
||
errors.push(`${head.Code}:${index + 1} ${error.message}`);
|
||
}
|
||
}
|
||
let text = original.replace(/\{([+-])\}/g, "$1");
|
||
let selectionError = "";
|
||
if (num(row.LinkIndex) === 62) {
|
||
text = text.replace(/(\x07([A-Z])\x07\s*)\x1a[^\x1a]*\x1a/g, (whole, reference, group) => {
|
||
const values = [];
|
||
for (let j = index - 1; j >= 0 && !(num(rows[j].Index) > 0); j--)
|
||
for (const match of String(rows[j].LineData || "").matchAll(
|
||
/\x06([A-Z])\x06\s*([\x03\x12\x0c])([-\d.,]+)\2/g,
|
||
))
|
||
if (match[1] === group) values.unshift(numeric(match[3]));
|
||
if (!values.length) selectionError = `합계 구간 ${group}에 지정된 결과가 없습니다.`;
|
||
return values.length ? `${reference}\x1a${values.join(" + ")}\x1a` : whole;
|
||
});
|
||
}
|
||
if (num(row.LinkIndex) === 61 && text.includes("\x0c")) {
|
||
const values = [];
|
||
for (let j = index - 1; j >= 0; j--) {
|
||
const previous = rows[j];
|
||
if (num(previous.Index) !== -1) {
|
||
const results = [
|
||
...String(previous.LineData || "").matchAll(/([\x03\x12\x0c])([-\d.,]+)\1/g),
|
||
];
|
||
if (results.length) values.push(numeric(results.at(-1)[2]));
|
||
}
|
||
if (num(previous.Level) > 0 || num(previous.Index) > 0 || num(previous.Index) === -20)
|
||
break;
|
||
}
|
||
const precision =
|
||
num(row.DigetIndex) && num(row.Diget) > 0
|
||
? EstMath.precision(row)
|
||
: { digits: 1, method: 2 };
|
||
const subtotal = floating(
|
||
// f251d5-f251df accumulates Extended values before applying the row precision.
|
||
values.reduce((total, value) => total + value, 0),
|
||
options.CostValueTrunc && sc < 0 ? 0 : precision.digits,
|
||
precision.method,
|
||
);
|
||
text = text.replace(/\x0c([-\d.,]+)\x0c/g, (whole, value) => {
|
||
changed.set(numeric(value), subtotal);
|
||
return `\x0c${subtotal}\x0c`;
|
||
});
|
||
row.TC = subtotal;
|
||
row.LineData = text;
|
||
}
|
||
if (num(row.LinkIndex) === 58 && text.includes("\x0c")) {
|
||
let sourceKind = num(row.OCheck);
|
||
if (/노무|인건|품/.test(clean(text))) sourceKind = -1;
|
||
else if (/재료|자재/.test(clean(text))) sourceKind = -2;
|
||
else if (/경비/.test(clean(text))) sourceKind = -3;
|
||
let value;
|
||
for (let j = index - 1; j >= 0; j--) {
|
||
const previous = rows[j],
|
||
kind = num(previous.SCheck);
|
||
if ((!sourceKind && kind < 0 && kind > -4) || (sourceKind && kind === sourceKind)) {
|
||
const results = [
|
||
...String(previous.LineData || "").matchAll(/([\x12\x0c])([-\d.,]+)\1/g),
|
||
];
|
||
if (results.length) {
|
||
value = numeric(results.at(-1)[2]);
|
||
row.OCheck = kind;
|
||
break;
|
||
}
|
||
}
|
||
if (num(previous.Level) > 0 || num(previous.Index) > 0) break;
|
||
}
|
||
if (value !== undefined) text = text.replace(/\x0c[-\d.,]+\x0c/g, `\x0c${value}\x0c`);
|
||
}
|
||
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`);
|
||
row.LineData = text;
|
||
text = text.replace(
|
||
/\x11([-\d.,]+)\x11/g,
|
||
(whole, value) => `\x11${changed.get(numeric(value)) ?? numeric(value)}\x11`,
|
||
);
|
||
if (![58, 61].includes(num(row.LinkIndex)))
|
||
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();
|
||
let componentDeclaration;
|
||
try {
|
||
if (selectionError) throw Error(selectionError);
|
||
if (!matched)
|
||
for (const m of text.matchAll(/\(([A-Za-z][A-Za-z0-9]*)\)\s*[::]\s*([-+]?\d[\d,.]*)/g))
|
||
variables[m[1]] = numeric(m[2]);
|
||
if (
|
||
(matched || /^\s*[A-Za-z][A-Za-z0-9]*\s*=/.test(text)) &&
|
||
/─/.test(text) &&
|
||
!(!matched && /⇒.*─/.test(text))
|
||
) {
|
||
const above = clean(rows[index - 1]?.LineData),
|
||
below = clean(rows[index + 1]?.LineData);
|
||
const columns = (value) =>
|
||
value
|
||
.replace(/\s*([×÷·+*/−-])\s*/g, "$1")
|
||
.trim()
|
||
.split(/\s+/);
|
||
const tops = columns(above),
|
||
bottoms = columns(below);
|
||
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) {
|
||
const section = [];
|
||
for (let j = index - 1; j >= 0; j--) {
|
||
const previous = rows[j];
|
||
if (!previous.LineData) continue;
|
||
if (!autoTotal(previous) || !component[num(previous.SCheck)]) break;
|
||
section.push(num(previous.TC));
|
||
}
|
||
calculated = sumCurrency(section);
|
||
} 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);
|
||
const parts = body.split("=");
|
||
if (parts.length > 1 && /^[A-Za-z가-힣][A-Za-z0-9가-힣]*$/.test(parts[0].trim())) {
|
||
componentDeclaration = parts[0].trim();
|
||
body = parts[1];
|
||
} else body = parts[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+|(?<=hr)\s+|[::]\s*|['"]\s*)([A-Za-z\u0370-\u03ff][A-Za-z0-9\u0370-\u03ff]*)\s*=/g,
|
||
),
|
||
].filter((match) => !/[+*/÷×−-]\s*$/.test(assignmentText.slice(0, match.index)));
|
||
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);
|
||
if (!matched && /⇒.*─/.test(body)) body = body.split("⇒")[0];
|
||
if (!matched) body = body.split(/[≈≒]/)[0];
|
||
if (!matched && /^\s*\*.*\*\s*$/.test(text)) body = body.replace(/\*\s*$/, "");
|
||
body = mathBraces(body);
|
||
if (/['"]\s*[A-Za-z][A-Za-z0-9]*\s*=/.test(assignment[0]))
|
||
body = body.replace(/['"]\s*$/, "");
|
||
if (!matched) body = body.split("{")[0];
|
||
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);
|
||
if (!result && /\d\s*[~~]\s*\d/.test(body)) {
|
||
delete variables[assignment[1]];
|
||
continue;
|
||
}
|
||
const parts = body.split("=");
|
||
body = parts[0];
|
||
body = expression(body)
|
||
.trim()
|
||
.replace(/\s+(?:min|hr|sec)\s*$/i, "")
|
||
.split(/\s{2,}(?=\d+\s*[가-힣]|[가-힣])/)[0]
|
||
.replace(/[,,]\s*$/, "");
|
||
if (!result)
|
||
body = body
|
||
.replace(/\s*,\s+[가-힣].*$/, "")
|
||
.replace(/^([-+]?(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?)(?:\s{2,}|\s*,\s+).*$/, "$1");
|
||
// 단위가 붙은 상수와 주석은 수치 부분만 사용한다.
|
||
const literal = body.match(/^([-+]?\d[\d,.]*)(?:\s*[A-Za-z가-힣㎥㎡ℓ˚°()].*)$/);
|
||
if (literal && !/^[xX](?=[A-Za-z\d.(])/.test(body.slice(literal[1].length).trim()))
|
||
body = literal[1];
|
||
let value;
|
||
try {
|
||
value = EstExpression.evaluate(body, (key) => variables[key]);
|
||
} catch (error) {
|
||
if (!result && error.message.startsWith("참조 항목 없음:")) continue;
|
||
if (!result && parts.length > 1)
|
||
value = EstExpression.evaluate(expression(parts.at(-1)), (key) => variables[key]);
|
||
else 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);
|
||
if (/^\s*\{[\s\S]*\}\s*$/.test(text)) body = body.replace(/^\s*\{/, "");
|
||
body = expression(body)
|
||
.replace(/\s*([×÷·+*/−-])\s*/g, "$1")
|
||
.trim()
|
||
.split(/\s{2,}/)
|
||
.pop();
|
||
calculated = EstExpression.evaluate(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 && autoTotal(row)) ||
|
||
index === singleComponent ||
|
||
options.CostValueTrunc
|
||
? 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);
|
||
const results = [...text.matchAll(/\x12([-\d.,]+)\x12/g)];
|
||
const intermediate =
|
||
results.length > 1 && sc < 0 && !explicit && row.Precision === undefined;
|
||
calculated = floating(
|
||
calculated,
|
||
intermediate ? (options.MidCostDiget ?? 2) : digits,
|
||
intermediate ? 2 : method,
|
||
);
|
||
const firstValue = calculated;
|
||
let rendered = text.replace(marker, `\x12${calculated}\x12`);
|
||
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 ??
|
||
(intermediate && part < results.length - 1
|
||
? (options.MidCostDiget ?? 2)
|
||
: digits),
|
||
format?.method ?? (intermediate && part < results.length - 1 ? 2 : 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 (componentDeclaration) variables[componentDeclaration] = 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],
|
||
);
|
||
for (let part = 1; part < results.length; part++) {
|
||
const operation = expression(
|
||
text.slice(
|
||
results[part - 1].index + results[part - 1][0].length,
|
||
results[part].index,
|
||
),
|
||
);
|
||
row.ResourceFactor = EstExpression.evaluate(
|
||
expression(`${row.ResourceFactor}${operation}`),
|
||
(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 (autoTotal(row) && component[sc]) {
|
||
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 && autoTotal(row) && component[sc]) {
|
||
let value = clean(text).split(":").slice(1).join(":").trim();
|
||
// Bare final totals can contain an already truncated amount rather than an equation.
|
||
// Rebuild only when the original component rows establish that subtotal relationship.
|
||
const savedValue = clean(original).split(":").slice(1).join(":").trim();
|
||
if (num(row.Level) === finalLevel && /^[-\d.,]+$/.test(savedValue)) {
|
||
const before = [],
|
||
after = [];
|
||
for (let j = index - 1; j >= 0 && num(rows[j].Index) <= 0; j--) {
|
||
if (num(rows[j].SCheck) !== sc) continue;
|
||
if (autoTotal(rows[j])) break;
|
||
const results = [...String(input[j].LineData || "").matchAll(/\x12([-\d.,]+)\x12/g)];
|
||
if (!results.length && input[j].TC == null) continue;
|
||
before.push(results.length ? numeric(results.at(-1)[1]) : num(input[j].TC));
|
||
after.push(num(rows[j].TC));
|
||
if (num(rows[j].LinkIndex) === 61) break;
|
||
}
|
||
if (before.length && floating(sumCurrency(before), 0, 2) === numeric(savedValue))
|
||
value = String(sumCurrency(after));
|
||
}
|
||
const calculated = floating(
|
||
EstExpression.evaluate(expression(value || "0"), (key) => variables[key]),
|
||
num(row.Level) === finalLevel ? 0 : 1,
|
||
);
|
||
row.TC = calculated;
|
||
if (value !== clean(text).split(":").slice(1).join(":").trim()) {
|
||
const start = text.indexOf(":") + 1,
|
||
end = text.lastIndexOf("\x1a");
|
||
row.LineData = text.slice(0, start) + calculated + text.slice(end);
|
||
}
|
||
if (num(row.Level) === finalLevel) totals[component[sc]] = calculated;
|
||
}
|
||
} catch (error) {
|
||
errors.push(`${head.Code}:${index + 1} ${error.message}`);
|
||
row.Error = error.message;
|
||
}
|
||
if (!hasAutoTotals && component[sc])
|
||
plainTotals[component[sc]] = num(plainTotals[component[sc]]) + num(row.TC);
|
||
}
|
||
finish();
|
||
return { rows, titles, errors, checks };
|
||
}
|
||
const names = {
|
||
"\x06": "구간지정",
|
||
"\x07": "구간합계",
|
||
"\x03": "단가",
|
||
"\x0c": "다른줄",
|
||
"\x0e": "품명",
|
||
"\x11": "변수",
|
||
"\x12": "결과",
|
||
"\x13": "규격",
|
||
"\x15": "단위",
|
||
"\x1a": "합계",
|
||
};
|
||
const display = (text) =>
|
||
String(text || "").replace(
|
||
/[\x03\x06\x07\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,
|
||
);
|
||
function replaceWords(rows, search, replacement) {
|
||
if (
|
||
typeof search !== "string" ||
|
||
typeof replacement !== "string" ||
|
||
!search ||
|
||
/[\x00-\x1f]/.test(search + replacement)
|
||
)
|
||
throw Error("찾을 문구를 입력하고 제어 문자를 제외하세요.");
|
||
const protectedMarkers = new Set(["\x03", "\x06", "\x07", "\x0c", "\x11", "\x12"]);
|
||
return rows.flatMap(({ row, index }) => {
|
||
if (typeof row.LineData !== "string") return [];
|
||
let protectedMarker = "";
|
||
const next = row.LineData.split(/([\x00-\x1f])/)
|
||
.map((part, i) => {
|
||
if (i % 2) {
|
||
if (part === protectedMarker) protectedMarker = "";
|
||
else if (!protectedMarker && protectedMarkers.has(part)) protectedMarker = part;
|
||
return part;
|
||
}
|
||
return protectedMarker ? part : part.split(search).join(replacement);
|
||
})
|
||
.join("");
|
||
if (next.length > 200000) throw Error("변경 후 산출근거의 길이가 한도를 초과합니다.");
|
||
return next === row.LineData ? [] : [{ index, value: next }];
|
||
});
|
||
}
|
||
return { calculate, clean, display, encode, replaceWords };
|
||
})();
|