Files
Aislo/tools/est-test/native-text.js
T

408 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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, "")
.replace(/[;;]/g, ":");
function expression(text) {
return clean(
String(text).replace(
/\x0e[^\x0e]*\x0e|\x13[^\x13]*\x13|\x15[^\x15]*\x15|\{[^}]*\}|\([가-힣][^()]*\)|\(\s*\d+(?:\.\d+)?\s*[˚°]\s*\)|<[^>]*>/g,
"",
),
)
.replace(
/(\d)\s*(?:공\/?㎥|공|단|면|겹|경간|개월|월|년|인|㎥|㎢|㎡|hr|sec|min|kwh|kw|km|ton|kg|㎏|㎞|ℓ|회|개소|개|대|분|초|본|장|매|병|조|식|일|시간|원|포|동|주|톤|μ|₩|EA|mm|cm|m3|m2|m|g|L|a)(?:\/(?:개소|경간|개|대|본|인|분|포|매|주|톤|\$|hr|일|㎥|㎡|m|a|EA))?/gi,
"$1",
)
.replace(/(?<=\d)\s*\/(?:일|㎥|㎡)(?=\s|$)/g, "")
.replace(/(?<=\))\s*(?:개|인|본|면|단|회)(?:\/(?:㎡|㎥|hr))?/g, "")
.replace(/^\s*[-*•]?\s*[가-힣][가-힣\s]*\s+(?=[\d.(])/, "")
.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 = {},
finalLevel = -1,
plainTotals = {},
singleComponent = -1,
hasAutoTotals = false;
function finish() {
if (!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];
if (num(row.Index) > 0) {
finish();
head = row;
variables = Object.create(null);
changed = new Map();
currentSource = 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 (String(rows[j].LineData || "").includes("\x1a")) 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;
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();
let componentDeclaration;
try {
if (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{2,}/);
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 (!String(previous.LineData).includes("\x1a") || !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+|[::]\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);
if (!matched && /^\s*\*.*\*\s*$/.test(text)) body = body.replace(/\s+\*\s*$/, "");
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];
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);
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 && text.includes("\x1a")) ||
index === singleComponent
? 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 ? Math.max(2, matched[1].split(".")[1]?.length || 0) : digits,
intermediate ? 1 : 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
? Math.max(2, next[1].split(".")[1]?.length || 0)
: digits),
format?.method ?? (intermediate && part < results.length - 1 ? 1 : 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 (text.includes("\x1a") && 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 && 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,
);
row.TC = calculated;
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 = {
"\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 };
})();