Files

613 lines
27 KiB
JavaScript
Raw Permalink 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 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 };
})();