Our own cost of each bid package, each subcontractor's bid leveled with plugs for the scope it leaves out, the low bid, and the base bid with the chosen bids in it.
How the precision is kept
Every money value here is a whole number of cents: cleanBid accepts a bid amount only as an integer from 0 to 1e13, and the estimate's row totals are whole cents. Costs, plugs, leveled amounts and differences are sums and differences of those integers, which 64-bit numbers hold without error below 2^53. The package file for a subcontractor carries quantities rounded to 4 decimals and no prices.
Calculations under this policy
5 calculations in 1 source file. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.
The sum of an item's estimate rows: the item, its second conditions and its assembly parts.
cost = Σ row totals
Whole cents added per item in row order. Rows with a formula error are left out.
level.js
lines 123 to 128
TakeoffCraft 0.8.1
/** Our own cost of each item: the sum of its estimate rows (item, conditions, assembly parts), errors left out. */
function itemCosts(est) {
const m = new Map();
for (const r of est.rows) if (!r.error) m.set(r.itemId, (m.get(r.itemId) || 0) + r.total);
return m;
}
Package scope and cost
64-bit
The items of each CSI division with their quantity, unit and cost, and the package's cost.
package cost = Σ item costs
The package cost is a sum of whole cents. Each scope line carries the item row's quantity as the unrounded 64-bit number the estimate holds, or none when the row has an error.
level.js
lines 130 to 145
TakeoffCraft 0.8.1
export function bidPackages(state, est, { title = (k) => k } = {}) {
const cost = itemCosts(est), qrow = new Map(est.rows.filter((r) => r.kind === 'item').map((r) => [r.itemId, r]));
const sheets = new Map();
for (const m of state.marks) { if (!sheets.has(m.item)) sheets.set(m.item, new Set()); sheets.get(m.item).add(m.sheet); }
const by = new Map();
for (const it of state.items) {
const k = pkgOf(it);
const p = by.get(k) || { key: k, title: title(k), items: [], sheets: new Set(), cost: 0 };
const r = qrow.get(it.id);
p.items.push({ itemId: it.id, name: it.name, csi: it.csi || '', qty: r && !r.error ? r.qty : null, unit: r?.unit || '', cost: cost.get(it.id) || 0, sheets: [...(sheets.get(it.id) || [])] });
p.cost += cost.get(it.id) || 0;
for (const s of sheets.get(it.id) || []) p.sheets.add(s);
by.set(k, p);
}
return [...by.values()].sort((a, b) => (a.key || '99').localeCompare(b.key || '99')).map((p) => ({ ...p, sheets: [...p.sheets] }));
}
Leveled bids and the low bid
64-bit
Each bid with plugs for what it leaves out, its difference from our cost, and which bid is low.
All terms are whole cents, so the sums and the comparison carry no rounding while they stay under 2^53. On a tie the first bid entered stays low, because the comparison is strictly less than.
level.js
lines 147 to 164
TakeoffCraft 0.8.1
export function leveling(state, est, opts = {}) {
const pk = new Map(bidPackages(state, est, opts).map((p) => [p.key, p]));
const out = [];
for (const [key, pkg] of pk) {
const bids = (state.bids || []).filter((b) => b.pkg === key);
if (!bids.length) continue;
const inPkg = new Map(pkg.items.map((x) => [x.itemId, x]));
const rows = bids.map((bid) => {
const plugs = bid.excl.filter((id) => inPkg.has(id)).map((id) => ({ itemId: id, name: inPkg.get(id).name, cost: inPkg.get(id).cost }));
const plug = plugs.reduce((n, x) => n + x.cost, 0), leveled = bid.amount + plug;
return { bid, plugs, plug, leveled, diff: leveled - pkg.cost };
});
let low = null;
for (const r of rows) if (!low || r.leveled < low.leveled) low = r;
out.push({ pkg, ours: pkg.cost, bids: rows, low: low.bid.id });
}
return out;
}
Package scope file
64-bit
The scope sent to a subcontractor: item, CSI number, quantity, unit and sheets, without prices.
quantity = round(q × 10000) ÷ 10000
The quantity is rounded to 4 decimals with Math.round on q × 10000, which rounds a half toward +∞ and makes no allowance for binary noise in the product. This is a different rounding from the estimate CSV in export.js.
level.js
lines 167 to 173
TakeoffCraft 0.8.1
/** A package's scope for a subcontractor: item, CSI number, quantity, unit, the sheets its marks are on. No prices. */
export function packageCsv(pkg, sheetLabel = (k) => k) {
const lines = [[t('pkg.csv.title', { pkg: pkg.title })], ['item', 'csi', 'qty', 'unit', 'sheets'].map((k) => t('pkg.csv.' + k))];
for (const x of pkg.items) lines.push([x.name, x.csi, x.qty === null ? '' : Math.round(x.qty * 10000) / 10000, x.unit, x.sheets.map(sheetLabel).join('; ')]);
lines.push([], [t('pkg.csv.sheets'), pkg.sheets.map(sheetLabel).join('; ')]);
return lines.map((l) => l.map(cell).join(',')).join('\r\n') + '\r\n';
}
Base bid with the chosen bids
64-bit
The estimate with each awarded package priced at its chosen bid instead of our own rates.
total = material + labor + Σ chosen bids
The awarded items' amounts are set to zero and one row per award carries the bid's whole cents; the totals are then added again from all rows. Items the bid leaves out keep our own cost. Crew days divide the hours by a literal 8, which the comment ties to CREW_DAY in model.js.
level.js
lines 188 to 217
TakeoffCraft 0.8.1
/** The base bid with the chosen bids in it (see the header). Without awards, est itself. */
export function applyAwards(state, est) {
const awards = cleanAwards(state.awards, state.bids);
if (!awards.length) return est;
const byPkg = new Map(awards.map((a) => [a.pkg, state.bids.find((b) => b.id === a.bid)]));
const items = new Map(state.items.map((i) => [i.id, i]));
const gone = new Set(); // items whose own pricing the bid replaced
for (const it of state.items) {
const b = byPkg.get(pkgOf(it));
if (b && !b.excl.includes(it.id)) gone.add(it.id);
}
const rows = est.rows.map((r) => {
if (r.error || !gone.has(r.itemId)) return r;
const bidder = byPkg.get(pkgOf(items.get(r.itemId))).bidder;
const { work, ...rest } = r; // our rates stay (the table shows them); only the extensions drop out
return { ...rest, matExt: 0, labExt: 0, total: 0, awarded: bidder, ownTotal: r.total };
});
let sub = 0;
for (const [pkg, b] of [...byPkg].sort((x, y) => (x[0] || '99').localeCompare(y[0] || '99'))) {
sub += b.amount;
rows.push({ kind: 'subcontract', itemId: null, pkg, bidId: b.id, bidder: b.bidder, name: b.bidder, unit: 'LS', qty: 1, qtyW: 1, waste: 0, mat: 0, lab: 0, matExt: 0, labExt: 0, subExt: b.amount, total: b.amount, type: 'sub', marks: 0 });
}
let mat = 0, lab = 0, errors = 0, hours = 0, workerHours = 0;
for (const r of rows) {
if (r.error) { errors++; continue; }
mat += r.matExt; lab += r.labExt;
if (r.work) { hours += r.work.hours; workerHours += r.work.workerHours; }
}
return { ...est, rows, matTotal: mat, labTotal: lab, subTotal: sub, total: mat + lab + sub, errors, incomplete: errors > 0 || est.unscaled > 0, hours, workerHours, days: hours / 8 }; // 8 = model.CREW_DAY (level.js can't import model.js: model imports it)
}
In detail
Method
Package cost = Σ item costs. Leveled bid = bid amount + Σ our cost of the items the bid leaves out. Low bid = the smallest leveled amount.
Number types
Integer cents in 64-bit numbers; quantities and crew hours are 64-bit numbers.
Limits
The package CSV rounds quantities with Math.round(q × 10000) ÷ 10000, not with the rounding the estimate CSV uses.
Approach: 64-bit, direct
Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.