Resources Numerical accuracy Calculation policy

Cabinet job cost estimate

Prices a cabinet job from its sheets, edge banding, frame stock, hardware, countertops, labor and machine time, with markup and tax.

How the precision is kept

Quantities come from the cut list and the nest in 64-bit numbers. Money is held as 64-bit numbers, not integer cents; each row amount, some scaled rates, and the subtotal, markup, tax and total are rounded to 2 decimals by r2 (core.js: Math.round(v × 100) ÷ 100). The default rates are placeholders, as the file's header says, and fixed allowances are built in: 15 % on sheet area when no nest ran, 10 % on frame stock, and the banding waste rate.

Open item. Do not split a rate line on commas when a semicolon or tab separates its cells, so a rate typed with a decimal comma is not cut at the comma.

Calculations under this policy

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

addons/cabinetcraft/lib/estimate.js CadCraft 0.8.2 64-bit 10 calculations Reviewed

Tests None found by the review

Amount of one estimate row

64-bit

The amount of a row from its quantity and rate.

amount = round(qty × rate × 100) ÷ 100; shown qty = round(qty × 1000) ÷ 1000

The amount is computed from the unrounded quantity and then rounded to 2 decimals. The quantity kept on the row is rounded to 3 decimals, so quantity × rate recomputed from the row can differ from the amount in the last cent.

  • addons/cabinetcraft/lib/estimate.js
  • lines 126 to 130
  • CadCraft 0.8.2
const add = (key, what, detail, qty, unitKey, rate, path) => {
  const amount = r2(qty * rate);
  if (!(qty > 0)) return;
  rows.push({ key, what, detail, qty: Math.round(qty * 1000) / 1000, unit: unitKey, rate, amount, path, base: rateAt(R, path), placeholder: !changed.has(path) });
};

Price of one sheet

64-bit

The price of a sheet of the job's size from the price of the stock sheet.

rate = round(base × (w × h × k²) ÷ (2440 × 1220), 2 decimals), k = 25.4 for an inch job

The area ratio is exact products and one division. The scaled price is rounded to cents before it is multiplied by the number of sheets.

  • addons/cabinetcraft/lib/estimate.js
  • lines 131 to 136
  • CadCraft 0.8.2
const sheetRate = (name, sh) => {
  const id = build.materialNames ? build.materialNames.get(name) : null;      // the library id whose written name this is (a typed name has none)
  const known = id && id in R.sheet;
  const area = sh.w * sh.h * mmK * mmK;
  return { rate: r2((known ? R.sheet[id] : R.sheetOther) * (area / SHEET_AREA)), path: known ? 'sheet.' + id : 'sheetOther' };
};

Number of sheets

64-bit

How many sheets of a material the job needs.

nested: sheets used; else n = max(1, ⌈cut area × 1.15 ÷ (w × h)⌉)

When a nest ran the count is its number of sheets. Otherwise the cut area is raised by a fixed 15 % and divided by the sheet area, then rounded up; the quotient is not snapped before Math.ceil, so a value a rounding error above a whole number counts one more sheet.

  • addons/cabinetcraft/lib/estimate.js
  • lines 139 to 145
  • CadCraft 0.8.2
const sheetsFor = (g) => {
  const m = nestJob && nestJob.mats.find((x) => x.material === g.material && x.thickness === g.thickness);
  if (m) return { n: m.result.sheets.length, sh: m.sheet, nested: true };
  const sh = sheetOf(g.material, g.thickness);
  const area = g.cutArea * (1 + 0.15), n = Math.max(1, Math.ceil(area / (sh.w * sh.h)));
  return { n, sh, nested: false };
};

Edge banding length priced

64-bit

The meters of each edge banding, with waste, and its rate.

meters = length ÷ 1000 × k × (1 + waste), k = 25.4 for an inch job

The banding length from the cut list is already rounded to 6 decimals there. The conversion divides by 1000 and multiplies by 25.4 for inches, both exact factors, and the waste rate defaults to 0.1.

  • addons/cabinetcraft/lib/estimate.js
  • lines 151 to 156
  • CadCraft 0.8.2
for (const b of cl.banding) {
  const id = (BANDS.find((x) => (build.bandNames && build.bandNames.get(x.id) === b.code)) || {}).id;
  const perM = id && id in R.bandPerM ? R.bandPerM[id] : R.bandOther;
  const path = id && id in R.bandPerM ? 'bandPerM.' + id : 'bandOther';
  add('band:' + b.code + b.thickness, 'est.band', b.code + ' ' + b.thickness, (b.length / 1000 * mmK) * (1 + R.bandWaste), 'est.u.m', perM, path);
}

Frame stock priced

64-bit

The price per meter of face-frame stock scaled from a 38 × 19 mm board, and the meters needed.

rate = base × (width ÷ 38) × (thickness ÷ 19), in mm; meters = length ÷ 1000 × k × 1.1

The rate scales with the cross-section and is rounded to cents before it is multiplied by the length. A fixed 10 % is added to the length.

  • addons/cabinetcraft/lib/estimate.js
  • lines 159 to 160
  • CadCraft 0.8.2
const per = R.hardwoodPerM * (l.width / (38 / mmK)) * (l.thickness / (19 / mmK));
add('frame:' + l.material + l.width, 'est.frame', l.material + ' ' + l.thickness + ' × ' + l.width, l.length / 1000 * mmK * 1.1, 'est.u.m', r2(per), 'hardwoodPerM');

Countertop priced

64-bit

The price of each countertop from its length and width.

amount = (length ÷ 1000 × k) × round(base × (width × k ÷ 600), 2 decimals)

The per-meter rate scales with the width against 600 mm and is rounded to cents first. A corner countertop arrives with length = width = √area, so its amount is proportional to its area.

  • addons/cabinetcraft/lib/estimate.js
  • line 163
  • CadCraft 0.8.2
for (const c of build.counters) add('counter:' + c.no, 'est.counter', c.no, c.length / 1000 * mmK, 'est.u.m', r2(R.counterPerM * (c.width * mmK / 600)), 'counterPerM');

Labor and machine hours

64-bit

The labor hours from the count of each cabinet kind, and the machine hours from the number of sheets.

hours = Σ count × hours per kind + frames × extra; machine = sheets × minutes per sheet ÷ 60

Sums of whole counts times the hour rates, in the order of the kinds. Without a nest the sheet count is the sum of the cut list's rough estimates.

  • addons/cabinetcraft/lib/estimate.js
  • lines 174 to 180
  • CadCraft 0.8.2
let hrs = 0;
for (const [k, n] of Object.entries(hours)) hrs += n * (R.labour[keyOf[k]] ?? 0);
hrs += frames * R.labour.frameExtra;
add('labour', 'est.labour', '', hrs, 'est.u.hour', R.labour.perHour, 'labour.perHour');
if (install) add('install', 'est.install', '', build.cells.filter((c) => c.kind === 'cab').length * R.labour.install, 'est.u.hour', R.labour.perHour, 'labour.perHour');
const nSheets = nestJob ? nestJob.sheets : cl.sheets.reduce((s, g) => s + (g.estimate || 0), 0);
add('machine', 'est.machine', '', nSheets * R.machine.minPerSheet / 60, 'est.u.hour', R.machine.perHour, 'machine.perHour');

Subtotal, markup, tax and total

64-bit

The totals of the estimate and the cost per cabinet.

subtotal = Σ amounts; markup = subtotal × rate; tax = (subtotal + markup) × rate; total = subtotal + markup + tax

Each step is rounded to 2 decimals, so the total is the sum of three already rounded figures. The amounts are binary numbers near cents, not integers, and are added in row order. The cost per cabinet is the total divided by the count, rounded to 2 decimals.

  • addons/cabinetcraft/lib/estimate.js
  • lines 181 to 187
  • CadCraft 0.8.2
const subtotal = r2(rows.reduce((s, r) => s + r.amount, 0));
const markup = r2(subtotal * R.markup), tax = r2((subtotal + markup) * R.tax);
const cabinets = build.cells.filter((c) => c.kind === 'cab').length;
const total = r2(subtotal + markup + tax);
const placeholders = rows.filter((r) => r.placeholder).length;
return {
  currency: R.currency, rows, subtotal, markup, markupRate: R.markup, tax, taxRate: R.tax, total, cabinets, perCabinet: cabinets ? r2(total / cabinets) : 0,

A rate read from a shop's table

64-bit

Turns one cell of an imported rate table into a number.

A leading $ is removed and the first comma is replaced by a point before Number(). A value with two separators such as 1,234.50 becomes not-a-number and is listed as ignored. Only values from 0 to 1,000,000 are accepted.

  • addons/cabinetcraft/lib/estimate.js
  • lines 85 to 89
  • CadCraft 0.8.2
const take = (path, v) => {
  if (!known.has(path)) { ignored.push({ key: path, reason: 'unknown' }); return; }
  if (path === 'currency') { if (typeof v === 'string' && /^[A-Za-z]{3}$/.test(v.trim())) values[path] = v.trim().toUpperCase(); else ignored.push({ key: path, reason: 'bad' }); return; }
  const n = typeof v === 'number' ? v : Number(String(v).trim().replace(/^\$/, '').replace(',', '.'));
  if (ok(n)) values[path] = n; else ignored.push({ key: path, reason: 'bad' });

Rate table lines

64-bit

Splits each line of a text rate table into a path and a value.

A line is split on every comma, semicolon and tab, and only the first two cells are used. A value typed with a decimal comma is therefore cut at the comma: labour.perHour;45,5 is read as 45.

  • addons/cabinetcraft/lib/estimate.js
  • lines 98 to 104
  • CadCraft 0.8.2
for (const line of src.split(/\r?\n/).slice(0, 2000)) {
  const l = line.trim();
  if (!l || l.startsWith('#')) continue;
  const cells = l.split(/[,;\t]/).map((c) => c.trim().replace(/^"|"$/g, ''));
  if (cells.length < 2) continue;
  if (/^(path|rate|key|name)$/i.test(cells[0]) && !known.has(cells[0])) continue;
  take(cells[0], cells[1]);

In detail

Method
amount = quantity × rate per row; sheet price scaled by sheet area ÷ (2440 × 1220); banding and counters priced per meter; labor hours per cabinet kind; machine time per sheet; markup on the subtotal; tax on subtotal plus markup.
Number types
64-bit numbers. The stock sheet area 2440 × 1220 and the factor 25.4 are exact.
Limits
Math.round(v × 100) ÷ 100 works on the binary value, so an amount that is a half cent in decimal can round down (1.005 gives 1). Sheet and frame rates are rounded to cents before they are multiplied by the quantity.

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