Each row's quantity with waste, its material and labor amounts, crew hours, and the totals of the estimate, of each bid area and of each alternate.
How the precision is kept
Rates are whole numbers of 1/10000 dollar and waste is whole basis points; normalItem replaces anything else with 0. A row's quantity stays an unrounded 64-bit number. Each extension is quantity × rate ÷ 100 rounded to a whole cent, and every total after that is a sum of whole cents, which 64-bit numbers add without error below 2^53. The cent rounding is roundTo in units.js, which adds 0.5 + 1e-9 × max(1, value) before taking the floor.
Open item. Stop the rounding allowance in roundTo (units.js) from growing with the amount: from 500,000,000 cents it is half a cent or more, and every extension of $5,000,000 or more is rounded one cent or more too high.
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 quantity with waste is one 64-bit product and is not rounded. Each amount is rounded to a whole cent by extendCents in units.js; the row total is a sum of two whole numbers. For an amount of 500,000,000 cents or more that rounding adds a cent or more (see the policy's open item).
model.js
lines 1113 to 1122
TakeoffCraft 0.8.1
function priced(r) {
const qtyW = withWaste(r.qty, r.waste);
const matExt = extendCents(qtyW, r.mat);
if (r.crew) { // T18: labor from the crew's hours; `lab` shows the rate it comes to
const work = crewWork(r.crew, qtyW);
return { ...r, lab: work.rate, qtyW, matExt, labExt: work.labExt, total: matExt + work.labExt, work };
}
const labExt = extendCents(qtyW, r.lab);
return { ...r, qtyW, matExt, labExt, total: matExt + labExt };
}
Crew hours and labor
64-bit
The crew hours, worker hours, crew days and labor amount for a quantity, and the labor rate per unit it comes to.
Hours are unrounded 64-bit quotients; crewOf accepts a production only when it is above zero, so the division is guarded. The labor amount is rounded to a whole cent with roundTo from units.js, with the same growing allowance as the other extensions. The rate shown is rounded to a whole 1/10000 dollar and is not used to compute the amount.
model.js
lines 100 to 104
TakeoffCraft 0.8.1
/** Hours and labor of a crew on a quantity (with waste). */
export function crewWork(crew, qtyW) {
const hours = qtyW / crew.prod, workerHours = hours * crew.size;
return { hours, workerHours, days: hours / CREW_DAY, labExt: Math.round(roundTo(workerHours * crew.wage / 100, 0)), rate: Math.round(crew.size * crew.wage / crew.prod) };
}
Estimate rows and totals
64-bit
A row for every item, second condition and assembly part, and the material, labor, hours and grand totals.
material total = Σ row material; labor total = Σ row labor; total = material + labor; days = hours ÷ 8
Money totals are sums of whole cents, added in item order. An assembly part's quantity is its formula evaluated on the item's totals (formula.js) and is priced like any row; a part whose formula fails gets no amount and is left out of the totals. Crew hours are added as unrounded 64-bit numbers.
model.js
lines 1020 to 1064
TakeoffCraft 0.8.1
/**
* The priced estimate: a row per item and per component, extended in cents.
* qty → qty × (1 + waste) → × material rate, × labor rate (each rounded to the cent) → total
* A component whose formula fails gets `error` and no amounts; the estimate is then `incomplete`.
*/
export function estimate(state, q = quantities(state)) {
const rows = [];
let mat = 0, lab = 0, errors = 0, unscaled = 0, hours = 0, workerHours = 0;
for (const it of state.items) {
const t = q.get(it.id);
const vars = itemVars(it, t);
const qty = itemQty(it, t);
const row = priced({ kind: 'item', itemId: it.id, name: it.name, unit: UNITS[it.unit].label, qty, waste: it.waste, mat: it.mat, lab: it.lab, type: it.type, ...(it.crew ? { crew: it.crew } : {}) });
if (row.work) { hours += row.work.hours; workerHours += row.work.workerHours; }
row.unscaled = t.unscaled;
row.marks = t.marks - Object.values(t.conds || {}).reduce((n, c) => n + c.marks, 0);
if (t.backoutSqft > 0) row.backout = { value: sqftTo(t.backoutSqft, it.unit), unit: UNITS[it.unit].label };
if (it.type === 'linear' && it.heightFt > 0) row.wallArea = { value: vars.A, unit: areaUnitOf(it.unit) };
if (it.type === 'area') row.perimeter = { value: vars.P, unit: lengthUnitOf(it.unit) === 'm' ? 'm' : 'LF' };
if (it.type === 'fixed' && it.origin) row.origin = it.origin;
unscaled += t.unscaled;
rows.push(row);
mat += row.matExt; lab += row.labExt;
for (const c of it.conds || []) { // T8: each second condition priced on its own row
const cr = priced({ kind: 'condition', itemId: it.id, cond: c.kind, name: it.name, unit: UNITS[c.unit].label, qty: condQty(c, t.conds?.[c.kind]), waste: c.waste, mat: c.mat, lab: c.lab, type: c.kind });
cr.marks = t.conds?.[c.kind]?.marks || 0;
rows.push(cr);
mat += cr.matExt; lab += cr.labExt;
}
for (const c of it.components) {
const p = parse(c.formula, VARS[it.type]);
let cr;
if (!p.ok) cr = { kind: 'component', itemId: it.id, compId: c.id, name: c.name, unit: c.unit, error: p.error, waste: c.waste, mat: c.mat, lab: c.lab };
else {
try { cr = priced({ kind: 'component', itemId: it.id, compId: c.id, name: c.name, unit: c.unit, qty: evaluate(p.ast, vars), waste: c.waste, mat: c.mat, lab: c.lab }); }
catch (err) { cr = { kind: 'component', itemId: it.id, compId: c.id, name: c.name, unit: c.unit, error: err.message, waste: c.waste, mat: c.mat, lab: c.lab }; }
}
cr.formula = c.formula;
if (cr.error) errors++;
else { mat += cr.matExt; lab += cr.labExt; }
rows.push(cr);
}
}
return { rows, matTotal: mat, labTotal: lab, total: mat + lab, errors, unscaled, incomplete: errors > 0 || unscaled > 0, hours, workerHours, days: hours / CREW_DAY };
}
Estimate of each bid area
64-bit
A separate estimate for each bid area, from that area's own marks.
Each area's quantities are measured and priced on their own, so each row is rounded to the cent per area and assembly formulas run on the area's totals. The code's comment notes that a formula which rounds per area can make the parts differ from the whole job; rounding each row to the cent per area can do the same.
model.js
lines 1066 to 1079
TakeoffCraft 0.8.1
/**
* The estimate of each bid area (T12): [{ area: {id, name} | null (marks in no bid area), est }], in the
* bid areas' order, then "no bid area" — only areas that hold marks, and "no bid area" only when the takeoff has areas
* or such marks. Assembly formulas run on each area's own totals. The rows of all areas add up to the job when no
* formula rounds (ceil/round per area can make the parts differ from the whole, as on a real bid).
*/
export function areaEstimates(state) {
const out = [];
const used = new Set(state.marks.map((m) => m.area || null));
for (const it of state.items) if (it.type === 'fixed' && it.qty > 0) used.add(it.area || null); // typed quantities (TK.5+) too
for (const a of state.areas || []) if (used.has(a.id)) out.push({ area: a, est: estimate(state, quantities(state, { area: a.id })) });
if (used.has(null) && (state.areas || []).length) out.push({ area: null, est: estimate(state, quantities(state, { area: null })) });
return out;
}
Alternates
64-bit
What taking each alternate adds to or takes from the base bid.
amount = + Σ row totals for an add; − Σ row totals for a deduct
Sums of whole cents over the rows of items that have marks (or a typed quantity) in the alternate's bid area. Rows with a formula error add zero.
model.js
lines 1095 to 1111
TakeoffCraft 0.8.1
/**
* The alternates (T17), in bid area order: [{ area, alt, est, amount }]. `amount` (cents) is what taking the alternate does
* to the base bid: + the add's cost, − the deduct's cost. Only items with marks in the area count (and their assemblies):
* an item's assembly is not priced in an alternate it has no marks in.
*/
export function alternates(state) {
const out = [];
for (const a of state.areas || []) {
if (!a.alt) continue;
const est = estimate(state, quantities(state, { area: a.id }));
const live = new Set(est.rows.filter((r) => (r.kind === 'item' || r.kind === 'condition') && (r.marks > 0 || (r.type === 'fixed' && r.qty > 0))).map((r) => r.itemId));
const rows = est.rows.filter((r) => live.has(r.itemId));
const total = rows.reduce((n, r) => n + (r.error ? 0 : r.total), 0);
out.push({ area: a, alt: a.alt, est: { ...est, rows, total, matTotal: rows.reduce((n, r) => n + (r.error ? 0 : r.matExt), 0), labTotal: rows.reduce((n, r) => n + (r.error ? 0 : r.labExt), 0) }, amount: a.alt === 'add' ? total : -total });
}
return out;
}
In detail
Method
quantity → quantity × (1 + waste ÷ 10000) → × rate ÷ 100, rounded half away from zero to the cent → sums of cents. Crew labor: quantity with waste ÷ production = crew hours, × crew size = worker hours, × wage.
Number types
Quantities and hours are 64-bit numbers. Rates, wages and waste are integers. Amounts are integer cents held in 64-bit numbers.
Tolerances
roundTo (units.js) allows 1e-9 × max(1, value) below a half before rounding up. The allowance is relative to the value being rounded, counted in the unit it is rounded to (cents here).
Limits
The rounding allowance grows with the amount. At 500,000,000 cents it reaches half a cent, so an extension of $5,000,000.00 or more comes out one cent high, and more above $15,000,000. Bid areas and alternates are estimated from their own quantities and rounded row by row, so their parts need not add up to the job to the cent.
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.