fix(est): propagate text subtotals and bid rates

This commit is contained in:
2026-11-05 13:17:44 +09:00
parent 1454b3c315
commit 216e2ae2a7
7 changed files with 236 additions and 13 deletions
+113 -6
View File
@@ -41,6 +41,7 @@ const EstText = (() => {
variables = Object.create(null),
changed = new Map(),
currentSource = null,
finalOverride = null,
totals = {},
finalLevel = -1,
plainTotals = {},
@@ -48,6 +49,12 @@ const EstText = (() => {
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])),
);
@@ -56,12 +63,14 @@ const EstText = (() => {
}
for (let index = 0; index < rows.length; index++) {
const row = rows[index];
delete row.Error;
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 [
@@ -87,6 +96,32 @@ const EstText = (() => {
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);
@@ -101,6 +136,71 @@ const EstText = (() => {
}
}
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*([\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(/([\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(
sumCurrency(values),
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,
@@ -112,10 +212,11 @@ const EstText = (() => {
/\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 (![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;
@@ -135,6 +236,7 @@ const EstText = (() => {
const assignedNames = new Map();
let componentDeclaration;
try {
if (selectionError) throw Error(selectionError);
if ((matched || /^\s*[A-Za-z][A-Za-z0-9]*\s*=/.test(text)) && /─/.test(text)) {
const above = clean(rows[index - 1]?.LineData),
below = clean(rows[index + 1]?.LineData);
@@ -374,7 +476,7 @@ const EstText = (() => {
} 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),
EstExpression.evaluate(expression(value || "0"), (key) => variables[key]),
num(row.Level) === finalLevel ? 0 : 1,
);
row.TC = calculated;
@@ -391,6 +493,8 @@ const EstText = (() => {
return { rows, titles, errors, checks };
}
const names = {
"\x06": "구간지정",
"\x07": "구간합계",
"\x03": "단가",
"\x0c": "다른줄",
"\x0e": "품명",
@@ -401,7 +505,10 @@ const EstText = (() => {
"\x1a": "합계",
};
const display = (text) =>
String(text || "").replace(/[\x03\x0c\x0e\x11\x12\x13\x15\x1a]/g, (ch) => `⟦${names[ch]}⟧`);
String(text || "").replace(
/[\x03\x06\x07\x0c\x0e\x11\x12\x13\x15\x1a]/g,
(ch) => `⟦${names[ch]}⟧`,
);
const encode = (text) =>
String(text).replace(
/⟦([^⟦⟧]+)⟧/g,