Resources Numerical accuracy Calculation policy

Costs on the schedule

Prices each element from its quantities, adds the cost up per scheduled task, spreads it over the task's days and totals the cash flow.

How the precision is kept

Money is kept as whole cents: each priced line is rounded to a cent once, and every sum after that is a sum of whole numbers, which 64-bit numbers hold exactly up to 2⁵³. Quantities priced per cubic yard or cubic foot use factors written to 9 significant digits. A task's cost per day is whole cents by the largest-remainder rule.

Open item. Write the cubic yard and cubic foot factors exactly, as 0.764554857984 and 0.028316846592.

Calculations under this policy

13 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.

cost5d.js BimCraft 0.8.1 64-bit 13 calculations Reviewed

Tests tools/bimcraft-test/eg-carbon-test.mjs, tools/bimcraft-test/datapacks-test.mjs

Unit factors for priced quantities

64-bit

The size of each pricing unit in m, m² or m³.

1 yd³ = 0.764554858 m³; 1 ft³ = 0.0283168466 m³; 1 ft² = 0.09290304 m²; 1 ft = 0.3048 m

The square foot (0.09290304) and the foot (0.3048) are exact. The cubic yard and the cubic foot are written to 9 significant digits; the exact values are 0.764554857984 and 0.028316846592, a relative difference of about 2 in 10¹¹ and 3 in 10¹⁰.

  • cost5d.js
  • line 29
  • BimCraft 0.8.1
export const UNITS = Object.freeze({ m3: ['volume', 1], m2: ['area', 1], m: ['length', 1], kg: ['mass', 1], item: ['count', 1], cy: ['volume', 0.764554858], cf: ['volume', 0.0283168466], sf: ['area', 0.09290304], lf: ['length', 0.3048] });

Dollars to whole cents

64-bit

Rounds an amount to a whole number of cents.

cents = round(dollars × 100)

Dollars times 100 is a binary product, so an amount that sits on half a cent in decimal can round either way: 1.005 × 100 is 100.49999999999999 in 64-bit and rounds to 100. After this step money is a whole number.

  • cost5d.js
  • line 37
  • BimCraft 0.8.1
const cents = (d) => Math.round(d * 100);

Whole cents to dollars

64-bit

The amount reported, in dollars.

dollars = round(cents) ÷ 100

One division of a whole number by 100; the result is the 64-bit number nearest that amount.

  • cost5d.js
  • line 38
  • BimCraft 0.8.1
const money = (c) => Math.round(c) / 100;

Quantity in the price's unit

64-bit

An element's volume, area, length, weight or count in the unit its price is per.

qty = quantity ÷ unit factor; weight = volume × density

The quantity in m, m² or m³ is divided by the row's unit factor in 64-bit. A price per kilogram uses the part's mass when it has one, else its volume times the row's density.

  • cost5d.js
  • lines 129 to 137
  • BimCraft 0.8.1
// ------------------------------------------------------------------------------------------------ pricing the model
function qtyFor(row, part) {
  const [kind, k] = UNITS[row.unit];
  let q;
  if (kind === 'mass') q = Number.isFinite(part.mass) ? part.mass : Number.isFinite(part.volume) ? part.volume * row.density : NaN;
  else if (kind === 'count') q = Number.isFinite(part.count) ? part.count : 1;
  else q = part[kind];
  return Number.isFinite(q) ? { qty: q / k, kind } : { qty: NaN, kind };
}

Cost of each element

64-bit

The material and labor cost of every element, and the total.

line = round(qty × (1 + waste) × material rate × 100) + round(qty × labor rate × 100) cents

Material and labor are each rounded to a whole cent per material part, then added as whole numbers. An element's cost and the total are sums of those whole cents, so they add up exactly; the rounding happens once per line.

  • cost5d.js
  • lines 139 to 156
  • BimCraft 0.8.1
export function priceItems(items, rows) {
  const lines = [], unpriced = [], byId = new Map();
  for (const it of items) {
    for (const part of it.parts || []) {
      const row = matchRow(rows, { material: part.material, name: it.name, cls: it.cls });
      if (!row) { unpriced.push({ id: it.id, guid: it.guid, name: it.name, cls: it.cls, material: part.material || '', why: 'noprice' }); continue; }
      const q = qtyFor(row, part);
      if (!(q.qty >= 0)) { unpriced.push({ id: it.id, guid: it.guid, name: it.name, cls: it.cls, material: part.material || '', why: 'noqty', need: q.kind }); continue; }
      const matC = cents(q.qty * (1 + row.waste) * row.mat), labC = cents(q.qty * row.lab);
      const cost = money(matC + labC);
      lines.push({ id: it.id, guid: it.guid, name: it.name, cls: it.cls, storey: it.storey || '', price: row.id, qty: q.qty, unit: row.unit, mat: money(matC), lab: money(labC), cost });
      byId.set(it.id, (byId.get(it.id) || 0) + matC + labC);
    }
  }
  let total = 0;
  for (const [k, c] of byId) { byId.set(k, c / 100); total += c; }
  return { lines, byId, unpriced, total: money(total) };
}

Cost of each scheduled task

64-bit

Adds up the cost of a task's elements, sharing an element that is in several tasks.

task cents = round(Σ element cents ÷ n), n = the tasks that hold the element

An element in several tasks is divided evenly in 64-bit and the task's sum is rounded to a whole cent once. The rounded tasks need not add back to the elements' cents: the scheduled figure can be off by up to half a cent per task.

  • cost5d.js
  • lines 158 to 172
  • BimCraft 0.8.1
/** Each matched task's cost: the cost of its elements, an element in n tasks counted 1/n in each. `byKey` Map(key → dollars) with key = keyBase + id. */
export function taskCosts(matched, byKey) {
  const n = new Map();
  for (const t of matched) for (const k of t.keys || []) n.set(k, (n.get(k) || 0) + 1);
  const tasks = [];
  const inTask = new Set();
  for (const t of matched) {
    let c = 0;
    for (const k of t.keys || []) { const v = byKey.get(k); if (v) { c += cents(v) / n.get(k); inTask.add(k); } }
    tasks.push({ name: t.name, stage: t.stage || '', package: t.package || '', start: t.start, finish: t.finish, keys: t.keys || [], cents: Math.round(c), cost: money(Math.round(c)) });
  }
  let un = 0;
  for (const [k, v] of byKey) if (!inTask.has(k)) un += cents(v);
  return { tasks, scheduled: money(tasks.reduce((s, t) => s + t.cents, 0)), unscheduled: money(un) };
}

Spending profile of a task

64-bit

The weight each day of a task gets: even, front-loaded, back-loaded or bell-shaped.

even 1; front D − i; back i + 1; bell sin(π (i + 0.5) ÷ D)

Whole numbers except the bell profile, which uses Math.sin. Only the ratios matter: the weights are divided by their sum when the cost is spread.

  • cost5d.js
  • lines 174 to 178
  • BimCraft 0.8.1
// ------------------------------------------------------------------------------------------------ spreading over days
const weightsFor = (D, profile) => {
  if (D <= 1) return [1];
  return Array.from({ length: D }, (_, i) => (profile === 'front' ? D - i : profile === 'back' ? i + 1 : profile === 'bell' ? Math.sin(Math.PI * (i + 0.5) / D) : 1));
};

A task's cost per day

64-bit

Divides a task's cost over its days in whole cents.

dayᵢ = floor(cents × wᵢ ÷ W), then one more cent to the days with the largest remainders

The shares are floored to whole cents, and the cents left over, a whole number found by subtraction, go one each to the days with the largest remainders. The days therefore add up to the task's cents exactly, whatever the profile.

  • cost5d.js
  • lines 179 to 186
  • BimCraft 0.8.1
/** A task's cents over Start..Finish (inclusive days): whole cents by the largest-remainder rule, so the days add up to the task exactly. */
export function spread(task, profile = 'even') {
  const D = task.finish - task.start + 1, w = weightsFor(D, PROFILES.includes(profile) ? profile : 'even'), W = w.reduce((s, x) => s + x, 0);
  const raw = w.map((x) => task.cents * x / W), base = raw.map(Math.floor);
  let left = task.cents - base.reduce((s, x) => s + x, 0);
  raw.map((x, i) => [x - Math.floor(x), i]).sort((a, b) => b[0] - a[0] || a[1] - b[1]).forEach(([, i]) => { if (left > 0) { base[i]++; left--; } });
  return base.map((c, i) => ({ day: task.start + i, cents: c }));
}

Cash flow curve

64-bit

The cost per day, week or month and the running total.

period cost = Σ day cents; cumulative = running sum

Day cents are whole numbers, added per day, per period and cumulatively as whole numbers, and divided by 100 only for the reported points. Weeks start on Monday, found from the day number.

  • cost5d.js
  • lines 190 to 204
  • BimCraft 0.8.1
export function cashFlow(tasks, { profile = 'even', per = 'day' } = {}) {
  const days = new Map();
  for (const t of tasks) for (const s of spread(t, profile)) days.set(s.day, (days.get(s.day) || 0) + s.cents);
  if (!days.size) return { points: [], total: 0, first: null, last: null };
  const first = Math.min(...days.keys()), last = Math.max(...days.keys());
  const key = (d) => (per === 'week' ? monday(d) : per === 'month' ? monthOf(d) : d);
  const groups = new Map();
  for (let d = first; d <= last; d++) {
    const k = key(d), g = groups.get(k) || { from: d, to: d, c: 0 };
    g.to = d; g.c += days.get(d) || 0; groups.set(k, g);
  }
  let cum = 0;
  const points = [...groups.values()].map((g) => { cum += g.c; return { label: per === 'month' ? monthOf(g.from) : dayText(per === 'week' ? monday(g.from) : g.from), from: g.from, to: g.to, cost: money(g.c), cum: money(cum) }; });
  return { points, total: money(cum), first, last };
}

Planned, built and earned value on a day

64-bit

What the schedule plans to have spent by a day, what is built, running and remaining, and the earned value from typed progress.

earned = Σ task cents × progress, progress clamped to 0…1

Planned, built, running and remaining are sums of whole cents. Earned value multiplies by the typed progress in 64-bit and is rounded to a cent once, at the end.

  • cost5d.js
  • lines 210 to 219
  • BimCraft 0.8.1
/** On `day`: what the schedule plans to have spent, what is built (tasks finished), running (tasks in progress), the rest; and earned value when the person types progress. */
export function valueAt(tasks, day, { profile = 'even', progress = null } = {}) {
  let planned = 0, built = 0, running = 0, remaining = 0, earned = 0;
  for (const t of tasks) {
    for (const s of spread(t, profile)) if (s.day <= day) planned += s.cents;
    if (day > t.finish) built += t.cents; else if (day >= t.start) running += t.cents; else remaining += t.cents;
    if (progress) earned += t.cents * Math.max(0, Math.min(1, Number(progress[t.name]) || 0));
  }
  return { planned: money(planned), built: money(built), running: money(running), remaining: money(remaining), ...(progress ? { earned: money(Math.round(earned)) } : {}) };
}

Grand total

64-bit

Runs the whole calculation and states the total cost.

grand = scheduled + unscheduled

The grand total is rebuilt from the scheduled and unscheduled figures in whole cents, not taken from the priced total, so it carries the per-task rounding of the scheduled part.

  • cost5d.js
  • lines 221 to 227
  • BimCraft 0.8.1
export function calcCost5d(items, rows, matched, { keyBase = 0, profile = 'even', per = 'week' } = {}) {
  const priced = priceItems(items, rows);
  const byKey = new Map([...priced.byId].map(([id, c]) => [keyBase + id, c]));
  const tc = taskCosts(matched, byKey);
  const flow = cashFlow(tc.tasks, { profile, per });
  return { ...priced, ...tc, flow, groups: byGroup(tc.tasks), profile, per, grand: money(cents(tc.scheduled) + cents(tc.unscheduled)) };
}

Rates read from TakeoffCraft

64-bit

Turns TakeoffCraft's stored rates into prices per unit.

rate = stored whole number ÷ 10000

TakeoffCraft's rates are whole numbers of ten-thousandths; each is divided by 10000 once in 64-bit. The waste allowance is converted the same way.

  • cost5d.js
  • lines 105 to 127
  • BimCraft 0.8.1
/**
 * TakeoffCraft's own rates → price rows. Reads a takeoff project (`{ format: 'takeoffcraft-project', items }`) or its `items` object: each item with a
 * material or labour rate (integers in ten-thousandths of a dollar; waste in ten-thousandths of one) becomes a row whose keyword is the item's name.
 * Those names rarely match a model's materials: edit the keywords (the mapping) before pricing. linear → lf, area → sf, count → item (TakeoffCraft's feet).
 */
export function fromTakeoff(json) {
  const src = typeof json === 'string' ? (() => { try { return JSON.parse(json); } catch { return null; } })() : json;
  const items = src && typeof src === 'object' ? (src.items && typeof src.items === 'object' ? src.items : src) : null;
  if (!items || Array.isArray(items)) return { rows: [], skipped: [{ name: '', code: 'format' }] };
  const rows = [], skipped = [], seen = new Set();
  for (const [key, it] of Object.entries(items).slice(0, LIMIT.rows)) {
    const name = String(it?.name ?? key);
    const unit = { linear: 'lf', area: 'sf', count: 'item', fixed: 'item' }[it?.type];
    const mat = Number(it?.mat) / 10000, lab = Number(it?.lab) / 10000, waste = (Number(it?.waste) || 0) / 10000;
    if (!unit || !(mat > 0 || lab > 0)) { skipped.push({ name, code: 'norate' }); continue; }
    let id = slug(name), n = 2;
    while (seen.has(id)) id = slug(name) + '-' + n++;
    const c = cleanPriceRow({ id, match: [name], unit, mat: mat || 0, lab: lab || 0, waste, source: 'TakeoffCraft rates' });
    if (!c.row) { skipped.push({ name, code: c.errors[0] }); continue; }
    seen.add(id); rows.push(c.row);
  }
  return { rows, skipped };
}

Costs in the CSV export

64-bit

The amounts written to the exported table.

Amounts are written with toFixed(2). They are whole cents divided by 100, so the two decimals reproduce them.

  • cost5d.js
  • lines 229 to 235
  • BimCraft 0.8.1
export function costCsv(res, currency = '') {
  const lines = ['stage,package,task,start,finish,cost' + (cleanCurrency(currency) ? ' (' + cleanCurrency(currency) + ')' : '')];
  for (const t of res.tasks) lines.push([cell(t.stage), cell(t.package), cell(t.name), dayText(t.start), dayText(t.finish), t.cost.toFixed(2)].join(','));
  lines.push(['', '', 'scheduled', '', '', res.scheduled.toFixed(2)].join(','), ['', '', 'not in any task', '', '', res.unscheduled.toFixed(2)].join(','), '', 'period,cost,cumulative');
  for (const p of res.flow.points) lines.push([p.label, p.cost.toFixed(2), p.cum.toFixed(2)].join(','));
  return lines.join('\r\n') + '\r\n';
}

In detail

Method
cost = qty × (1 + waste) × material rate + qty × labor rate per material part; an element in n tasks counts 1 ÷ n in each; a task's cents are spread over its days by weights (even, front, back, bell) with the largest-remainder rule.
Number types
64-bit numbers; money as whole cents held in 64-bit numbers.
Limits
The cubic yard and cubic foot factors are rounded: 0.764554858 for 0.764554857984 and 0.0283168466 for 0.028316846592. Each task's share of split elements is rounded to a cent, so scheduled plus unscheduled can differ from the priced total by up to half a cent per task.

Approach: 64-bit, stated limit

The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.

Every policy that takes this approach