340 lines
14 KiB
JavaScript
340 lines
14 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 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 };
|
||
})();
|