Files
Aislo/tools/stmate-test/calc.js
T

567 lines
22 KiB
JavaScript

/* -*- coding: utf-8 -*- */
/**
* STMATE 계산 사슬 및 제어 옵션 시뮬레이터 엔진 (calc.js)
* 근거: resources/knowledge/original/경쟁사 프로그램/STmate/ (old/20_분석 · REA 분석)
*/
const StmateCalc = (() => {
const KINDS = ["재료비", "노무비", "경비"];
const KIND_KEYS = { 재료비: "M", 노무비: "L", 경비: "S" };
function num(val, def = 0) {
if (val === null || val === undefined || val === "") return def;
const n = Number(String(val).replace(/,/g, "").trim());
return Number.isFinite(n) ? n : def;
}
function fmt(val, maxDec = 0) {
return new Intl.NumberFormat("ko-KR", {
minimumFractionDigits: 0,
maximumFractionDigits: maxDec,
}).format(num(val, 0));
}
// 자리 맞춤 — 음수 자리(십·백·천원)는 나눗셈으로 정수 결과를 냄(10^-n 곱셈 오차 방지)
const scaleOf = (digits) =>
digits >= 0
? { mul: (v) => v * Math.pow(10, digits), back: (s) => s / Math.pow(10, digits) }
: { mul: (v) => v / Math.pow(10, -digits), back: (s) => s * Math.pow(10, -digits) };
function roundDown(val, digits = 0) {
const n = Number(val) || 0,
f = scaleOf(digits),
s = f.mul(n);
return f.back(Math.trunc(s + (s >= 0 ? 1e-9 : -1e-9)));
}
function roundUp(val, digits = 0) {
const n = Number(val) || 0;
if (n === 0) return 0;
const f = scaleOf(digits),
s = f.mul(n);
return f.back(n > 0 ? Math.ceil(s - 1e-9) : Math.floor(s + 1e-9));
}
function roundHalfUp(val, digits = 0) {
const n = Number(val) || 0,
f = scaleOf(digits);
const r = f.back(Math.floor(f.mul(Math.abs(n)) + 0.5 + 1e-9));
return n < 0 ? -r : r;
}
// 은행가 반올림(Delphi RoundTo · FPU 최근접-짝수 — 11번 §4.2)
function roundBanker(val, digits = 0) {
const n = Number(val) || 0,
f = scaleOf(digits),
s = f.mul(n),
fl = Math.floor(s);
if (Math.abs(s - fl - 0.5) < 1e-9) return f.back(fl % 2 === 0 ? fl : fl + 1);
return f.back(Math.round(s));
}
function applyRoundMode(val, mode, digits = 0) {
return mode === "up"
? roundUp(val, digits)
: mode === "half"
? roundHalfUp(val, digits)
: mode === "banker"
? roundBanker(val, digits)
: roundDown(val, digits);
}
// 원가계산서(제잡비) 계산 — kanjub_calc.js
const calcKanjub = (globalThis.STMATE_KANJUB_FACTORY || require("./kanjub_calc.js"))({
num,
fmt,
roundDown,
roundUp,
roundHalfUp,
roundBanker,
applyRoundMode,
});
function evalFormula(exprStr, vars = {}) {
let s = String(exprStr || "")
.replace(/'[^']*'/g, "")
.trim();
if (!s) return 0;
Object.keys(vars)
.sort((a, b) => b.length - a.length)
.forEach((k) => {
s = s.replace(new RegExp("\\b" + k + "\\b", "g"), String(vars[k]));
});
s = s.replace(/[^0-9.+\-*/()]/g, "");
if (!s) return 0;
try {
const v = Function('"use strict"; return (' + s + ");")();
return Number.isFinite(v) ? v : 0;
} catch (_e) {
return 0;
}
}
function evalWageFormulaLeftToRight(dailyWage, exprStr) {
const base = Number(dailyWage) || 0,
clean = String(exprStr || "").replace(/\s+/g, "");
if (!clean) return { seqResult: base, preCalcResult: base, steps: [] };
let acc = base,
coeff = 1;
const steps = [];
for (const tok of clean.match(/([*/])?(\d+(?:\.\d+)?)/g) || []) {
const op = tok[0] === "*" || tok[0] === "/" ? tok[0] : "*";
const n = parseFloat(op === tok[0] ? tok.slice(1) : tok);
if (!Number.isFinite(n) || n === 0) continue;
acc = op === "*" ? acc * n : acc / n;
coeff = op === "*" ? coeff * n : coeff / n;
steps.push(`${op}${n} → ${roundHalfUp(acc, 4)}`);
}
return { seqResult: roundDown(acc, 0), preCalcResult: Math.trunc(base * coeff), coeff, steps };
}
function calcResources(data) {
const sys = data.sysConfig || {},
rxFormula = sys.rxFormula || "1/8*16/12*25/20";
const condCodes = new Set(
String(sys.condCodes || "")
.split(/[,\s]+/)
.map((s) => s.trim())
.filter(Boolean),
);
const usdRate = Number(sys.useChangedUsd ? sys.usdRate2 : sys.usdRate1) || 1000,
ecdDigits = Number(sys.ecdDigits) || 0;
const map = {};
const list = (data.resources || []).map((res) => {
const cands = Array.isArray(res.candidates) ? res.candidates.map(Number) : [0, 0, 0, 0, 0];
const nonZero = cands.filter((v) => v > 0);
let basePrice = Number(res.manualPrice) || 0,
chosenLabel = "수동입력";
if (res.adoptMode === "min") {
basePrice = nonZero.length ? Math.min(...nonZero) : 0;
chosenLabel = nonZero.length ? `최저가(후보${cands.indexOf(basePrice) + 1})` : "0원";
} else if (res.adoptMode && res.adoptMode.startsWith("slot")) {
const sIdx = parseInt(res.adoptMode.replace("slot", ""), 10) - 1;
basePrice = cands[sIdx] || 0;
chosenLabel = `후보${sIdx + 1} 고정`;
}
const krwPrice = res.isUsd ? roundDown(basePrice * usdRate, ecdDigits) : basePrice;
const wageInfo =
res.kind === "노무비" && (res.convertHourly || condCodes.has(res.code))
? evalWageFormulaLeftToRight(krwPrice, rxFormula)
: null;
const adoptedPrice = wageInfo ? wageInfo.seqResult : krwPrice;
const item = Object.assign(
{},
res,
{ cands, basePrice, adoptedPrice },
{ appliedPrice: adoptedPrice, chosenLabel, wageInfo },
);
return ((map[res.id] = item), (map[res.code] = item), item);
});
return { list, map };
}
function calcMachines(data, resMap) {
const map = {};
const list = (data.machines || []).map((m) => {
let M = num(m.hourlyM),
L = num(m.hourlyL),
S = num(m.hourlyS),
opHourly = 0,
fuelPrice = 0;
if (m.hourlyT === undefined || m.operatorId !== undefined) {
opHourly = resMap[m.operatorId]?.adoptedPrice || 0;
fuelPrice = resMap[m.fuelId]?.adoptedPrice || 0;
M = roundDown(
(Number(m.fuelConsumption) || 0) *
fuelPrice *
(1 + (Number(m.miscRatePercent) || 0) / 100),
0,
);
L = roundDown(opHourly * (Number(m.operatorCount) || 1), 0);
S = roundDown((Number(m.acquisitionPrice) || 0) * (Number(m.hourlyRateFactor) || 0), 0);
}
const T = num(m.hourlyT) || M + L + S;
const item = Object.assign({}, m, { M, L, S, T, opHourly, fuelPrice });
return ((map[m.id] = item), (map[m.code] = item), item);
});
return { list, map };
}
function evalQFormula(dItem) {
const q = Number(dItem.q) || 0,
K = Number(dItem.K) || 0;
const f = dItem.f !== undefined ? Number(dItem.f) : evalFormula(dItem.fExpr || "1", { q, K });
const E =
dItem.E !== undefined ? Number(dItem.E) : evalFormula(dItem.EExpr || "0.55", { q, K, f });
const Cm = Number(dItem.Cm) || 1,
mult = dItem.formulaType === "60_qKfE_Cm" ? 60 : 3600;
const rawQ = Cm !== 0 ? (mult * q * K * f * E) / Cm : 0;
return { rawQ, Q: roundHalfUp(rawQ, 2) };
}
function calcSanCal(data, machineMap) {
const map = {};
const list = (data.sanItems || []).map((d) => {
const mach = machineMap[d.machineId],
{ rawQ, Q } = evalQFormula(d);
const M =
d.fixedM !== undefined ? num(d.fixedM) : mach && Q > 0 ? roundDown(mach.M / Q, 1) : 0;
const L =
d.fixedL !== undefined ? num(d.fixedL) : mach && Q > 0 ? roundDown(mach.L / Q, 1) : 0;
const S =
d.fixedS !== undefined ? num(d.fixedS) : mach && Q > 0 ? roundDown(mach.S / Q, 1) : 0;
const intM = roundDown(M, 0),
intL = roundDown(L, 0),
intS = roundDown(S, 0),
intT = intM + intL + intS;
const item = Object.assign({}, d, { mach, rawQ, Q, M, L, S, T: roundHalfUp(M + L + S, 1) });
Object.assign(item, { intM, intL, intS, intT });
return ((map[d.id] = item), (map[d.code] = item), item);
});
return { list, map };
}
function calcIlwi(data, resMap, machMap, sanMap) {
const map = {};
const list = (data.ilwiItems || []).map((ilwi) => {
let sumM = 0,
sumL = 0,
sumS = 0;
const lines = (ilwi.lines || []).map((ln) => {
const ref = resMap[ln.refId] || machMap[ln.refId] || sanMap[ln.refId],
rawQty = Number(ln.qty) || 0;
const baseQty = ln.devd && rawQty !== 0 ? roundHalfUp(1 / rawQty, 7) : rawQty;
const appliedQty = roundHalfUp(baseQty * (1 + (Number(ln.halPercent) || 0) / 100), 7);
const pickU = (k, prop) =>
!ref
? 0
: "adoptedPrice" in ref
? ref.kind === k
? ref.adoptedPrice
: 0
: ref[prop] || 0;
const uM = pickU("재료비", "M"),
uL = pickU("노무비", "L"),
uS = pickU("경비", "S");
const lineM = roundDown(uM * appliedQty, 2),
lineL = roundDown(uL * appliedQty, 2),
lineS = roundDown(uS * appliedQty, 2);
sumM += lineM;
sumL += lineL;
sumS += lineS;
const lineT = roundHalfUp(lineM + lineL + lineS, 2);
const out = Object.assign({}, ln, {
refName: ref?.name || "미연결",
refCode: ref?.code || "-",
});
return Object.assign(out, { appliedQty, uM, uL, uS, lineM, lineL, lineS, lineT });
});
const M = roundDown(sumM, 0),
L = roundDown(sumL, 0),
S = roundDown(sumS, 0);
const item = Object.assign({}, ilwi, { lines, M, L, S, T: M + L + S });
return ((map[ilwi.id] = item), (map[ilwi.code] = item), item);
});
return { list, map };
}
// 총액우선 끝전 배분(OPTIMIZ) — 성분 절사 · 총액 절사와의 차 1원씩 소수부 큰 성분부터(동률 = 순서 앞)
function optimize(raw, rawT, order) {
const out = { M: roundDown(raw.M, 0), L: roundDown(raw.L, 0), S: roundDown(raw.S, 0) };
const keys = String(order || "SML")
.toUpperCase()
.split("")
.filter((k) => "MLS".includes(k));
const frac = Object.fromEntries(keys.map((k) => [k, roundHalfUp(raw[k] - out[k], 6)]));
let rest = roundDown(rawT, 0) - out.M - out.L - out.S;
while (rest > 0) {
let pick = null;
for (const k of keys) if (frac[k] < 1 && (pick === null || frac[k] > frac[pick])) pick = k;
if (pick === null) break;
((out[pick] += 1), (frac[pick] += 1), (rest -= 1));
}
return out;
}
// 내역 비율 줄(PR · 조달수수료 등) — 밑수 비목(PRIN) 누적 × 율(QTY, %) → 대상 비목(PROUT) 원 미만 절사
// 단가 칸 = 밑수 · 수량 칸 = 율 (원본 BOQ MAMT1 · QTY1 · M_AMT1 저장 꼴) · PRIN 없는 줄은 일반 줄
const PR_COMP = { 1: "M", 2: "L", 3: "S", 4: "T" };
function prRow(row, slot, base) {
if (String(row.code || "").trim() !== "PR") return null;
const bk = PR_COMP[String(row.PRIN ?? "").trim()],
ok = PR_COMP[String(row.PROUT ?? "").trim()];
if (!bk || !ok) return null;
const pct = num((row.prQtys || [])[slot - 1]);
const bv = bk === "T" ? base.M + base.L + base.S : base[bk];
const to = ok === "T" ? "M" : ok;
const a = { M: 0, L: 0, S: 0 },
u = { M: 0, L: 0, S: 0 };
((a[to] = roundDown((bv * pct) / 100, 0)), (u[to] = bv));
const T = a.M + a.L + a.S;
const unitInfo = {
M: u.M,
L: u.L,
S: u.S,
T: bv,
name: row.name,
code: "PR",
spec: "",
unit: "%",
};
const out = {
unitInfo,
appliedQty: pct,
qty: pct,
mUnit: u.M,
lUnit: u.L,
sUnit: u.S,
tUnit: bv,
};
Object.assign(out, { mAmt: a.M, lAmt: a.L, sAmt: a.S, tAmt: T, M: a.M, L: a.L, S: a.S, T });
Object.assign(out, {
optimizDiff: 0,
optimizTarget: "",
aggM: a.M,
aggL: a.L,
aggS: a.S,
aggT: T,
});
return { M: a.M, L: a.L, S: a.S, T, out };
}
function calcBoq(data, resMap, machMap, sanMap, ilwiMap, useChangedQty = false) {
const slot = typeof useChangedQty === "number" ? useChangedQty : useChangedQty ? 2 : 1;
const sys = data.sysConfig || {},
chnMode = sys.chnMode || "bimok",
optOrder = (sys.optOrder || "SML").toUpperCase();
const lookupUnit = (refId) => {
const r = ilwiMap[refId] || sanMap[refId] || machMap[refId] || resMap[refId];
if (!r) return { M: 0, L: 0, S: 0, T: 0, name: "미연결", code: "-", spec: "", unit: "" };
const pick = (k, iProp, prop) =>
"adoptedPrice" in r
? r.kind === k
? r.adoptedPrice
: 0
: "intT" in r
? r[iProp]
: r[prop];
const M = pick("재료비", "intM", "M"),
L = pick("노무비", "intL", "L"),
S = pick("경비", "intS", "S");
const meta = { name: r.name, code: r.code, spec: r.spec || "", unit: r.unit || "" };
return Object.assign({ M, L, S, T: M + L + S }, meta);
};
let directM = 0,
directL = 0,
directS = 0,
govRawTotal = 0,
govFeeTotal = 0;
// 내역 비율 줄(PR) 밑수 — 표시(PRPT) 줄 금액 누적 · PR 줄을 지나면 비움(영월 조달수수료 4줄 대조)
let prBase = { M: 0, L: 0, S: 0 };
const rows = (data.boqItems || []).map((row) => {
const pr = prRow(row, slot, prBase);
if (pr) {
prBase = { M: 0, L: 0, S: 0 };
if (row.rowType === "gov_mat") govRawTotal += pr.T;
else if (row.rowType === "gov_fee") govFeeTotal += pr.T;
else ((directM += pr.M), (directL += pr.L), (directS += pr.S));
return Object.assign({}, row, pr.out);
}
const su = slot > 1 && row.units && row.units[slot];
const u = su
? Object.assign({}, lookupUnit(row.refId), { M: su[0], L: su[1], S: su[2] })
: lookupUnit(row.refId);
// 행 고정 단가(fixedUnit) · 내역 할증율(wBoqRate RATE/RM·RL·RS = 비목별 단가 가산, 원 단위 절사)
// — 수량 할증(BDQTY X_HAL)과 다른 금액 가산(07번 §4 · 34번 wBoqRate) · 저장 차수 단가는 이미 반영된 값이라 건너뜀
if (!su && row.fixedUnit)
["M", "L", "S"].forEach((c) => row.fixedUnit[c] != null && (u[c] = num(row.fixedUnit[c])));
const hr = row.halRate;
if (!su && hr != null) {
const pct = (c) => num(typeof hr === "object" ? hr[c] : hr);
["M", "L", "S"].forEach((c) => pct(c) && (u[c] = roundDown(u[c] * (1 + pct(c) / 100), 0)));
}
u.T = u.M + u.L + u.S;
const qty =
Number(
slot > 1 && row.qtys
? row.qtys[slot - 1]
: slot > 1 && row.changedQty !== undefined
? row.changedQty
: row.qty,
) || 0;
const rawM = u.M * qty,
rawL = u.L * qty,
rawS = u.S * qty,
rawT = u.T * qty;
let M = roundDown(rawM, 0),
L = roundDown(rawL, 0),
S = roundDown(rawS, 0),
T = M + L + S;
let optimizDiff = 0,
optimizTarget = "";
if (chnMode === "total") {
// 총액우선(CHNNM=0) — REA 15 OPTIMIZ: 총액 절사 후 남는 1원을 소수부 큰 성분부터 하나씩
// (동률은 OPTNM 순서 앞쪽) · 산근·일위대가(smt_eval finish)와 같은 규칙
const got = optimize({ M: rawM, L: rawL, S: rawS }, rawT, optOrder);
optimizDiff = got.M + got.L + got.S - (M + L + S);
if (optimizDiff) {
optimizTarget = "MLS"
.split("")
.filter((k) => got[k] !== { M, L, S }[k])
.map((k) => `${k}(+${got[k] - { M, L, S }[k]})`)
.join("");
((M = got.M), (L = got.L), (S = got.S), (T = M + L + S));
}
}
const aggM = roundHalfUp(rawM, 0),
aggL = roundHalfUp(rawL, 0),
aggS = roundHalfUp(rawS, 0);
if (String(row.PRPT ?? "0").trim() && String(row.PRPT ?? "0").trim() !== "0")
((prBase.M += M), (prBase.L += L), (prBase.S += S));
if (row.rowType === "gov_mat") govRawTotal += T;
else if (row.rowType === "gov_fee") govFeeTotal += T;
else ((directM += M), (directL += L), (directS += S));
const out = Object.assign({}, row, {
spec: row.spec || u.spec || "",
unit: row.unit || u.unit || "",
});
const u1 = {
unitInfo: u,
appliedQty: qty,
qty,
mUnit: u.M,
lUnit: u.L,
sUnit: u.S,
tUnit: u.T,
};
const u2 = { mAmt: M, lAmt: L, sAmt: S, tAmt: T, M, L, S, T, optimizDiff, optimizTarget };
return Object.assign(out, u1, u2, { aggM, aggL, aggS, aggT: aggM + aggL + aggS });
});
const directT = directM + directL + directS,
govCombinedRaw = govRawTotal + govFeeTotal;
const totals = Object.assign(
{ directM, directL, directS, directT },
{ govRawTotal, govMatTotal: govRawTotal, govFeeTotal, govCombinedRaw },
);
return Object.assign({ rows, directTotal: directT, totals }, totals);
}
function evalKOrdTransition(pclass, regMode, flag, currentK) {
const K = Number(currentK) || 1;
const nextK =
Number(regMode) === 6
? Number(pclass) === 6
? K + 1
: 1
: Number(regMode) === 7 && Number(pclass) === 7 && Number(flag) === 0
? K + 1
: 1;
return { nextK, downK: K - (K > 5 ? K - 4 : 1) };
}
// 새 공사 = 빈 상태 · 요율만 초기값(지식DB 06 · 2024 기준표) — 특정 공사 금액을 담지 않음
function createDefaultState() {
const ratePairs =
"KANNO:14.5,SANBO:3.56,KOBO:1.01,KUNBO:3.545,SLVBO:12.95,YUNBO:4.5,TBUGUM:0,ANJUN:1.86,ANJUN1:1.86,ANJUN2:1.86,KITA:6,HWAN:0.8,EQIP:0.4,ILBAN:6,IYUN:15,S_SUDO:0,S_BOKRI:0,S_SOMO:0,S_TONGSIN:0,S_SEGUM:0,S_PEGI:0,S_DOSEO:0,S_JIGUP:0";
const rates = Object.fromEntries(
ratePairs.split(",").map((p) => [p.split(":")[0], Number(p.split(":")[1])]),
);
const revKeys = "KANNO,SANBO,KOBO,KUNBO,SLVBO,YUNBO,TBUGUM,ANJUN,KITA,HWAN,EQIP".split(",");
const revModes = Object.fromEntries(
revKeys.map((k) => [
k,
["KUNBO", "SLVBO", "YUNBO"].includes(k) ? "carry_original" : "recalculated",
]),
);
const projectMeta = { pname: "새 공사", pnum: "", pcode: "11", pclass: 0, kOrd: 0 };
const sysConfig = {
rxFormula: "1/8*16/12*25/20",
condCodes: "",
usdRate1: 0,
usdRate2: 0,
useChangedUsd: false,
ecdDigits: 0,
chnMode: "bimok",
optOrder: "SML",
};
sysConfig.kOrdSim = { pclass: 0, regMode: 7, flag: 0, currentK: 1 };
// 원본 키 초기값 — 일반형식 · 안전비 관급포함 자동 · 고용 7등급 · 환경 기타토목 · 사급 재료비 포함 · 관급 천원 절상
const keys = Object.fromEntries(
"JAB_TYPE:0,CAL_ACTL:0,I_KONG_TYPE:0,I_SET_BASE:0,I_KONG_KIKAN:0,I_KOBO_TYPE:7,I_TBUGUM:3,I_ANJUN_TYPE:1,I_HWAN:14,I_EQIP:0,I_POS_SA:3,I_KN_SA_TYPE:0,I_ANJUN_K:1,I_ANJUN_B:0,I_ANJUNAUTO:1,I_BUGA:0,I_TOT_JEOL:3,I_KWN_JEOLSA:3,I_KWN_K_JEOLSA:3,I_KWN_UPDN:0,I_KWN_K_UPDN:0,I_KAN_JEOLSA:0,I_JEOLSA:0,I_DOG_JEOL:0,C_ANJUN_G:0,C_ANJUN1_G:0,C_ANJUN2_G:0,C_KWANGUP_D:0,C_KWANGUP_K:0,C_T_JODAL:0,C_IYUN_M:0"
.split(",")
.map((p) => [p.split(":")[0], Number(p.split(":")[1])]),
);
const kanjubConfig = {
slot: 1,
scheme: "T",
keys,
addRows: [],
addAmounts: [],
fixed: {},
anjunAuto: 1,
anjunBaseAdd: 0,
};
Object.assign(kanjubConfig, { rates, revModes });
return Object.assign(
{ projectMeta, sysConfig, kanjubConfig },
{ resources: [], machines: [], sanItems: [], ilwiItems: [], boqItems: [] },
);
}
function calculateAll(data) {
const resources = calcResources(data),
machines = calcMachines(data, resources.map),
sanCal = calcSanCal(data, machines.map);
const ilwi = calcIlwi(data, resources.map, machines.map, sanCal.map);
const boqOrig = calcBoq(data, resources.map, machines.map, sanCal.map, ilwi.map, false),
kanjubOrig = calcKanjub(data, boqOrig, null);
const st = data.kanjubStages || {};
const boqRev = calcBoq(data, resources.map, machines.map, sanCal.map, ilwi.map, true),
kanjubRev = st[2]
? calcKanjub(data, boqRev, kanjubOrig, st[2])
: calcKanjub(data, boqRev, kanjubOrig);
// 차수 n 원가계산서 — 변경 계열은 바로 앞 차수(n−1)를 당초로 씀(REA 18 직전 계열 = 현재−1)
const stages = {};
for (const n of Object.keys(st)
.map(Number)
.sort((a, b) => a - b)) {
const boq =
n === 1
? boqOrig
: n === 2
? boqRev
: calcBoq(data, resources.map, machines.map, sanCal.map, ilwi.map, n);
const prev = (stages[n - 1] && stages[n - 1].kanjub) || kanjubOrig;
stages[n] = {
boq,
kanjub: n === 1 ? kanjubOrig : n === 2 ? kanjubRev : calcKanjub(data, boq, prev, st[n]),
};
}
const revMap = Object.fromEntries((boqRev.rows || []).map((r) => [r.id, r]));
(boqOrig.rows || []).forEach((r) => {
const rv = revMap[r.id];
r.changedTAmt = rv ? rv.tAmt : r.tAmt;
r.diffTAmt = r.changedTAmt - r.tAmt;
});
const base = { resources, machines, sanCal, ilwi, boqOrig, kanjubOrig };
Object.assign(base, {
boqRev,
kanjubRev,
resList: resources.list,
revComputed: { revBoq: boqRev, revKj: kanjubRev },
stages,
});
return Object.assign(base, { boqComputed: boqOrig, kanjubComputed: kanjubOrig });
}
const evalLeftToRight = (exprStr) => evalWageFormulaLeftToRight(1, exprStr).coeff || 0;
const api = { KINDS, KIND_KEYS, num, fmt, evalFormula, evalLeftToRight };
Object.assign(api, { roundDown, roundUp, roundHalfUp, roundBanker });
Object.assign(api, { evalWageFormulaLeftToRight, evalQFormula, evalKOrdTransition });
return Object.assign(api, { createDefaultState, calculateAll, computeAll: calculateAll });
})();
if (typeof window !== "undefined") window.STMATE_CALC = StmateCalc;
if (typeof globalThis !== "undefined") globalThis.STMATE_CALC = StmateCalc;
if (typeof module !== "undefined" && module.exports) module.exports = StmateCalc;