feat(est): reproduce native tables and calculations

This commit is contained in:
2026-11-05 06:07:05 +09:00
parent 646eab6245
commit 60ccedcd9a
30 changed files with 5249 additions and 22 deletions
+190
View File
@@ -0,0 +1,190 @@
// REA f1a078, f17c10, f1a8c4, f3242c, f36074, f34970.
const EstRows = (() => {
const { num, components, units, sum, fixed, floating, amounts, add } = EstMath;
const target = { 1: "GC", 2: "NC", 3: "JC", 4: "SC" };
function baseKind(row) {
const text = String(row.Size || "");
if (/[노품인]/.test(text)) return "NC";
if (/[재자]/.test(text)) return "GC";
if (text.includes("경")) return "JC";
if (/계|총|합/.test(text)) return "TC";
if (text.includes("비목")) return null;
return { 1: "GC", 2: "NC", 3: "JC", 4: "TC" }[num(row.OCheck) % 5] || null;
}
function percent(row, preceding, digits = 0) {
if ([1, 3].includes(num(row.CalcIndex))) return row;
// f1a8c4 matches Chr(64 + source.SKind) against the percentage row's Code.
const selectedGroups = String(row.Code || "").toUpperCase();
const eligible =
num(row.LinkIndex) === 62
? preceding.filter(
(r) =>
num(r.SKind) > 0 && selectedGroups.includes(String.fromCharCode(64 + num(r.SKind))),
)
: preceding;
const base = baseKind(row),
sc = num(row.SCheck),
p = EstMath.precision(row, digits);
components.forEach((key) => (row[key] = 0));
if (base) {
const amount = eligible.reduce((n, r) => n + num(r[base]), 0);
row.TU = amount;
row[target[sc] || "JC"] = floating((amount * num(row.GN)) / 100, p.digits, p.method);
} else {
components.forEach(
(key) =>
(row[key] = floating(
(eligible.reduce((n, r) => n + num(r[key]), 0) * num(row.GN)) / 100,
p.digits,
p.method,
)),
);
row.TU = eligible.reduce((n, r) => n + num(r.TC), 0);
}
row.TC = sum(row);
return row;
}
function procurement(row, preceding) {
if ([1, 3].includes(num(row.CalcIndex))) return row;
const key = { 1: "GC", 2: "NC", 3: "JC", 4: "TC" }[num(row.OCheck)] || "TC";
const base = preceding
.filter((r) => num(r.LinkIndex) !== 57)
.reduce((n, r) => n + num(r[key]), 0);
const fee = Math.trunc((base * num(row.GN)) / 100),
step = 10 ** num(row.Gangub),
quotient = (base + fee) / step;
const rounded =
num(row.Index) === 1
? EstMath.half(quotient)
: num(row.Index) === 2
? Math.trunc(quotient)
: EstMath.half(quotient + 0.5);
components.forEach((k) => (row[k] = 0));
row[target[num(row.SCheck)] || "JC"] = rounded * step - base;
row.TU = base;
row.TC = sum(row);
return row;
}
function subtotal(row, preceding, digits = 0) {
const eligible = preceding.filter((r) => num(r.CalcIndex) < 2 && num(r.LinkIndex) !== 51);
const values = Object.fromEntries(
components.map((k) => [k, eligible.reduce((n, r) => n + num(r[k]), 0)]),
);
const kind = baseKind(row),
sc = target[num(row.SCheck)];
units.forEach((key) => (row[key] = 0));
if (sc) row[units[components.indexOf(sc)]] = kind === "TC" ? sum(values) : num(values[kind]);
else
components.forEach(
(key, i) => (row[units[i]] = kind && kind !== "TC" && kind !== key ? 0 : values[key]),
);
return amounts(row, digits);
}
function head(rows, options = {}, resolve = () => null) {
const output = structuredClone(rows),
titles = [],
errors = [];
let current = null,
body = [];
function finish() {
if (!current) return;
Object.assign(current, add(body));
titles.push({ ...current });
}
for (const row of output) {
if (num(row.Index) > 0) {
finish();
current = row;
body = [];
continue;
}
if (!current) continue;
const link = num(row.LinkIndex);
if ([58, 62].includes(link)) percent(row, body, options.HeadDigits || 0);
else if ([51, 68].includes(link)) {
subtotal(row, body, options.HeadDigits || 0);
if (link === 68) {
// f3242c resets its accumulators to the converted subtotal at LinkIndex 68.
body = [row];
continue;
}
} else if (link === 52) {
const before = add(body),
ratio = num(row.GN) / 100;
components.forEach((key) => (row[key] = floating(before[key] * ratio)));
row.TC = sum(row);
row.TU = before.TC;
body = [row];
continue;
} else {
if (options.SourceRefresh && ![1, 3].includes(num(row.CalcIndex))) {
try {
const price = resolve(row);
if (price) units.forEach((key, i) => (row[key] = num(price[components[i]])));
} catch (error) {
errors.push(`${current.Name}: ${error.message}`);
}
}
amounts(row, options.HeadDigits || 0, options.TotalFit);
}
body.push(row);
}
finish();
return { rows: output, titles, errors };
}
function estimate(rows, options = {}, resolve = () => null) {
const output = structuredClone(rows),
errors = [];
let segment = [],
prior = [],
boundary = false;
for (const row of output) {
const level = num(row.Level),
link = num(row.LinkIndex);
if (level > 0) {
segment = [];
continue;
}
if (link === 54) {
Object.assign(row, add(prior));
boundary = true;
continue;
}
if (link === 59) {
boundary = true;
continue;
}
if (boundary && [55, 58, 59].includes(link)) continue;
if (link === 57) procurement(row, segment);
else if ([62, 67, 58].includes(link)) percent(row, segment, options.EstDigits || 0);
else {
if (options.SourceRefresh && ![1, 3].includes(num(row.CalcIndex))) {
try {
const price = resolve(row);
if (price) units.forEach((key, i) => (row[key] = num(price[components[i]])));
} catch (error) {
errors.push(`${row.Name}: ${error.message}`);
}
}
amounts(row, options.EstDigits || 0, options.TotalFit);
}
segment.push(row);
if (!boundary) prior.push(row);
}
for (let i = output.length - 1; i >= 0; i--) {
const row = output[i],
level = num(row.Level);
if (level <= 0) continue;
const children = [];
for (let j = i + 1; j < output.length; j++) {
const next = output[j];
if (num(next.Level) >= level || [54, 58, 59].includes(num(next.LinkIndex))) break;
if (num(next.Level) === 0) children.push(next);
}
Object.assign(row, add(children));
}
const direct = output.find((r) => num(r.LinkIndex) === 54) || add(prior);
return { rows: output, direct, errors };
}
return { head, estimate, percent, procurement, subtotal, baseKind };
})();