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
+93
View File
@@ -0,0 +1,93 @@
// REA c9f8ec / c9d278 / c9f0e4: 수식 참조, 원 미만 절사, 도급액 고정 반복.
const EstWonga = (() => {
const { num } = EstMath;
const compact = (text) => String(text || "").replace(/\s/g, "");
function calculate(input, options = {}, bases = {}) {
const rows = structuredClone(input),
errors = [],
adjustments = [];
const key = (row) =>
String(options.VerFormat === false ? row.NumStr || row.Gubun : row.Gubun || row.NumStr || "");
const keys = new Map(rows.map((row, index) => [key(row), index]));
keys.delete("");
rows.forEach((row, index) => {
if (compact(row.Name)) keys.set(compact(row.Name), index);
});
const overrides = new Map();
function pass() {
const done = new Set(),
active = new Set();
function resolve(name) {
if (Object.hasOwn(bases, name)) return bases[name];
const index = keys.get(name) ?? keys.get(compact(name));
return index === undefined ? undefined : evaluate(index);
}
function range(start, end) {
let a = keys.get(start),
b = keys.get(end);
if (a === undefined || b === undefined) throw Error(`범위 참조 없음: ${start}:${end}`);
if (a > b) [a, b] = [b, a];
let sum = 0;
for (let i = a; i <= b; i++)
if ([0, 1, 4].includes(num(rows[i].CalcIndex))) sum += evaluate(i);
return sum;
}
function evaluate(index) {
const row = rows[index];
if (overrides.has(index)) return (row.Cost = overrides.get(index));
if (done.has(index)) return num(row.Cost);
if (active.has(index)) throw Error(`원가 수식 순환 참조: ${key(row)}`);
active.add(index);
if (row.BaseField && Object.hasOwn(bases, row.BaseField)) row.Cost = bases[row.BaseField];
else if (row.VSusic && ![1, 3].includes(num(row.CalcIndex)))
row.Cost = Math.trunc(
EstExpression.evaluate(row.VSusic, resolve, { numericReferences: true, range }),
);
else row.Cost = num(row.Cost);
active.delete(index);
done.add(index);
return row.Cost;
}
for (let i = 0; i < rows.length; i++) evaluate(i);
}
const targetIndex = rows.findIndex((row) => num(row.LinkIndex) === 55);
const target = targetIndex >= 0 ? num(rows[targetIndex].Cost) : 0;
try {
pass();
if (options.TotalFix && targetIndex >= 0) {
const profit = rows.findIndex(
(row) => compact(row.Name).includes("이윤") && ![1, 3].includes(num(row.CalcIndex)),
);
const vat = rows.findIndex((row) => compact(row.Name).includes("부가"));
if (profit < 0) throw Error("도급액 고정: 조정할 이윤 행이 없습니다.");
for (let iteration = 0; iteration < 100; iteration++) {
const delta = num(rows[targetIndex].Cost) - target;
adjustments.push({
iteration,
profit: num(rows[profit].Cost),
total: num(rows[targetIndex].Cost),
difference: delta,
});
if (delta === 0) break;
overrides.set(profit, num(rows[profit].Cost) - delta);
pass();
if (iteration === 99 && vat >= 0) {
overrides.set(vat, num(rows[vat].Cost) + target - num(rows[targetIndex].Cost));
pass();
}
}
if (num(rows[targetIndex].Cost) !== target)
throw Error("도급액 고정 계산이 수렴하지 않았습니다.");
}
} catch (error) {
errors.push(error.message);
}
return {
rows,
errors,
adjustments,
total: targetIndex >= 0 ? num(rows[targetIndex].Cost) : num(rows.at(-1)?.Cost),
};
}
return { calculate };
})();