feat(est): reproduce native tables and calculations
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
// 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(/[①-⑳]/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(/\{[^}]*\}/g, (value) => " ".repeat(value.length));
|
||||
const assignments =
|
||||
sc < 0
|
||||
? []
|
||||
: [
|
||||
...assignmentText.matchAll(
|
||||
/(?:^| {2,}|[,,]\s*|(?<=[가-힣])\s+|[::]\s*)([A-Za-z][A-Za-z0-9]*)\s*=/g,
|
||||
),
|
||||
].filter((a) => !text.includes("\x0e") || a.index < text.indexOf("\x0e"));
|
||||
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 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],
|
||||
),
|
||||
digits,
|
||||
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 };
|
||||
})();
|
||||
Reference in New Issue
Block a user