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