feat(est): reproduce native tables and calculations
This commit is contained in:
@@ -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 };
|
||||
})();
|
||||
Reference in New Issue
Block a user