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

340 lines
14 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, "");
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 };
})();