205 lines
7.5 KiB
JavaScript
205 lines
7.5 KiB
JavaScript
// 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.Gangub) < 1
|
|
? 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 = num(row.LinkIndex) === 68 ? null : baseKind(row),
|
|
sc = num(row.LinkIndex) === 68 ? undefined : 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, refreshRows = []) {
|
|
const output = structuredClone(rows),
|
|
titles = [],
|
|
errors = [],
|
|
refreshErrors = [];
|
|
let current = null,
|
|
body = [];
|
|
function finish() {
|
|
if (!current) return;
|
|
Object.assign(current, add(body));
|
|
titles.push({ ...current });
|
|
}
|
|
for (const [index, row] of output.entries()) {
|
|
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 {
|
|
const requested = refreshRows.includes(index);
|
|
if ((options.SourceRefresh || requested) && ![1, 3].includes(num(row.CalcIndex))) {
|
|
try {
|
|
const price = resolve(row);
|
|
if (requested && !price) throw Error("원천 단가를 찾을 수 없습니다.");
|
|
if (price) units.forEach((key, i) => (row[key] = num(price[components[i]])));
|
|
} catch (error) {
|
|
errors.push(`${current.Name}: ${error.message}`);
|
|
if (requested) refreshErrors.push(error.message);
|
|
}
|
|
}
|
|
amounts(row, options.HeadDigits || 0, options.TotalFit);
|
|
}
|
|
body.push(row);
|
|
}
|
|
finish();
|
|
return refreshRows.length
|
|
? { rows: output, titles, errors, refreshErrors }
|
|
: { rows: output, titles, errors };
|
|
}
|
|
function estimate(rows, options = {}, resolve = () => null, refreshRows = []) {
|
|
const output = structuredClone(rows),
|
|
errors = [],
|
|
refreshErrors = [];
|
|
let segment = [],
|
|
prior = [],
|
|
boundary = false;
|
|
for (const [index, row] of output.entries()) {
|
|
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 {
|
|
const requested = refreshRows.includes(index);
|
|
if ((options.SourceRefresh || requested) && ![1, 3].includes(num(row.CalcIndex))) {
|
|
try {
|
|
const price = resolve(row);
|
|
if (requested && !price) throw Error("원천 단가를 찾을 수 없습니다.");
|
|
if (price) units.forEach((key, i) => (row[key] = num(price[components[i]])));
|
|
} catch (error) {
|
|
errors.push(`${row.Name}: ${error.message}`);
|
|
if (requested) refreshErrors.push(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 refreshRows.length
|
|
? { rows: output, direct, errors, refreshErrors }
|
|
: { rows: output, direct, errors };
|
|
}
|
|
return { head, estimate, percent, procurement, subtotal, baseKind };
|
|
})();
|