Resources Numerical accuracy Calculation policy

Supplier stock and lead-time check

Reads a supplier's stock list and checks each quantity the model needs against it: how much is short, and how long the wait is.

How the precision is kept

Stock figures, lead times and prices are read from the text of a supplier's file. Stock is kept as the 64-bit number written, lead time is converted to days and rounded to 0.1 day, and a price is rounded to a whole 1/10000 dollar. The shortfall is the needed quantity minus the stock, compared with no tolerance. Lead times use fixed conventions: a week is 7 days, a month is 30 days and business days are scaled by 7 ÷ 5 and rounded up.

Calculations under this policy

12 calculations in 2 source files. 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.

lib-supply/supply.js DetailCraft 0.8.1 64-bit 6 calculations Reviewed

Tests tools/detailcraft-test/test-supply.mjs

Stock figure from text

64-bit

Reads a stock cell such as 1,200 or Out of stock as a number, or as in stock or not.

Thousands commas are removed and the text is converted with Number, so the figure is the nearest 64-bit number to what is written. A leading <, >, ~, ≥ or ≤ and a trailing + are accepted and dropped, so 500+ reads as 500. Values above 1e12 are treated as unknown.

  • lib-supply/supply.js
  • lines 55 to 65
  • DetailCraft 0.8.1
/** "1,200" → 1200 · "Out of stock" / "OOS" → 0 · "In stock" → inStock · "" → unknown. */
export function parseStock(text) {
  const s = clean(text, 60).toLowerCase();
  if (!s) return { stock: null, inStock: null };
  if (/^(out of stock|oos|none|sold out|nil)$/.test(s)) return { stock: 0, inStock: false };
  if (/^(in stock|yes|available|y)$/.test(s)) return { stock: null, inStock: true };
  const m = /^([<>~≥≤]?\s*)?(\d[\d,]*(?:\.\d+)?)\s*\+?\s*([a-z.\/²³]*)$/.exec(s);
  if (!m) return { stock: null, inStock: null };
  const n = Number(m[2].replace(/,/g, ''));
  return Number.isFinite(n) && n >= 0 && n <= 1e12 ? { stock: n, inStock: n > 0 } : { stock: null, inStock: null };
}

Lead time in days

64-bit

Reads a lead time such as 2-3 weeks or 5 business days as a number of days.

weeks × 7; months × 30; business days → ⌈n × 7 ÷ 5⌉; result rounded to 0.1 day

A range uses the larger of its two numbers. The result is rounded with Math.round(days × 10) ÷ 10 and kept only from 0 to 3650 days. A month is taken as 30 days.

  • lib-supply/supply.js
  • lines 66 to 77
  • DetailCraft 0.8.1
/** "5 days" · "2 wks" · "2-3 weeks" (the longer) · "3 months" · "same day" / "stock" → 0 · "backorder" → null + backorder. `bare` = the unit of a plain number. */
export function parseLead(text, bare = 'day') {
  const s = clean(text, 60).toLowerCase();
  if (!s) return { lead: null, backorder: false };
  if (/back\s*order|tbd|call|unknown|n\/a|discontinued/.test(s)) return { lead: null, backorder: true };
  if (/^(same day|immediate|immediately|stock|in stock|now|next day)$/.test(s)) return { lead: /next/.test(s) ? 1 : 0, backorder: false };
  const m = /(\d+(?:\.\d+)?)(?:\s*[-–to]+\s*(\d+(?:\.\d+)?))?\s*(day|d\b|business day|week|wk|w\b|month|mo\b)?/.exec(s);
  if (!m) return { lead: null, backorder: false };
  const n = Math.max(Number(m[1]), m[2] ? Number(m[2]) : 0), u = (m[3] || bare).replace(/s$/, '');
  const days = /^(week|wk|w)/.test(u) ? n * 7 : /^(month|mo)/.test(u) ? n * 30 : /^business/.test(u) ? Math.ceil(n * 7 / 5) : n;
  return days >= 0 && days <= 3650 ? { lead: Math.round(days * 10) / 10, backorder: false } : { lead: null, backorder: false };
}

Price from text

64-bit

Reads a price cell as a whole number of 1/10000 dollar.

price = round(n × 10000)

The first number in the cell is read, commas removed, and rounded to a whole 1/10000 dollar with Math.round. A price with more than 4 decimals loses the rest. Prices of 1e9 dollars or more are dropped.

  • lib-supply/supply.js
  • line 78
  • DetailCraft 0.8.1
const money = (text) => { const m = /(\d[\d,]*(?:\.\d+)?)/.exec(String(text ?? '')); if (!m) return null; const n = Number(m[1].replace(/,/g, '')); return Number.isFinite(n) && n >= 0 && n < 1e9 ? Math.round(n * 10000) : null; };

A need in the supplier's unit

64-bit

The needed quantity in the unit the supplier's row uses.

tonnes = kilograms ÷ 1000

When the units match by name the quantity is used as it is, or the need's own figure for that unit is taken. The one conversion is kilograms to tonnes, a division by 1000. No other unit is converted; a row in another unit is reported as a unit mismatch.

  • lib-supply/supply.js
  • lines 149 to 155
  • DetailCraft 0.8.1
/** The quantity the need has in the supplier row's unit (the need's own unit, or one of `units`; tonnes from kilograms). { amount, unit } or null. */
function needIn(n, row) {
  if (sameUnit(n.unit, row.unit)) return { amount: n.need, unit: n.unit };
  for (const [u, v] of Object.entries(n.units || {})) if (v > 0 && sameUnit(u, row.unit)) return { amount: v, unit: u };
  if (n.units && n.units.kg > 0 && groupOf(row.unit) === groupOf('t')) return { amount: n.units.kg / 1000, unit: row.unit };
  return null;
}

Short, late or in stock

64-bit

For each need: the matching supplier row, the shortfall and the status.

short = max(0, need − stock); late: no stock figure and lead > limit

One 64-bit subtraction, clamped at zero. Any shortfall above zero counts as short: there is no tolerance, so a need that exceeds the stock only by rounding noise in the quantity is reported short. A row that says only out of stock is short by the whole need.

  • lib-supply/supply.js
  • lines 172 to 192
  • DetailCraft 0.8.1
/** Each need against the supplier rows. status: ok · short (stock below the need) · late (lead time over the person's limit) · unknown (no stock figure) · unit (the supplier's unit is not one of the need's). */
export function check(needs, supply) {
  const all = (supply?.sources || []).flatMap((s) => s.rows.map((r) => ({ ...r, source: s.name })));
  const ignore = new Set(supply?.ignore || []);
  const limit = supply?.leadDays ?? 14;
  const rows = [], unmatched = [];
  for (const n of needs) {
    if (ignore.has(n.id) || !(n.need > 0)) continue;
    const m = all.length ? matchRow(n, all) : null;
    if (!m) { unmatched.push(n); continue; }
    const r = m.row, q = needIn(n, r);
    const base = { ...n, row: r, exact: m.rank >= 2, stock: r.stock, lead: r.lead, backorder: r.backorder, supplier: r.supplier || r.source, price: r.price };
    if (!q) { rows.push({ ...base, status: 'unit' }); continue; }
    const short = r.stock !== null ? Math.max(0, q.amount - r.stock) : r.inStock === false ? q.amount : null;
    const status = short > 0 ? 'short' : (r.lead !== null && r.lead > limit) || r.backorder ? (r.inStock === true || (r.stock !== null && r.stock >= q.amount) ? 'ok' : 'late') : short === null && r.stock === null && r.inStock === null ? 'unknown' : 'ok';
    rows.push({ ...base, need: q.amount, unit: q.unit, short: short || 0, status });
  }
  const waiting = rows.filter((r) => (r.status === 'short' || r.status === 'late') && r.lead !== null);
  const critical = waiting.length ? waiting.reduce((a, b) => (b.lead > a.lead ? b : a)) : null;
  return { rows, unmatched, critical, leadDays: limit };
}

Values in the reported problems

64-bit

The required and found values of a shortage or a long lead time in the problems list.

The required quantity is rounded to 2 decimals with Math.round(need × 100) ÷ 100 before it is stored in the problem; the found stock is stored as read. The message text formats its numbers to 2 decimals the same way unless the app passes its own formatter.

  • lib-supply/supply.js
  • lines 194 to 210
  • DetailCraft 0.8.1
/** Problems in the shared shape (rule 29). `t(key, vars)` words them; ids do not change with the language. `app` names the source; `keys` = { short, lead, back } message keys. */
export function problemsOf(res, { t = (k) => k, fmt = (n) => String(Math.round(n * 100) / 100), app = 'app', keys = { short: 'eg51.prob.short', lead: 'eg51.prob.lead', back: 'eg51.prob.back' } } = {}) {
  const out = [];
  for (const r of res.rows) {
    const about = r.about ? [r.about] : [];
    if (r.status === 'short') {
      const vars = { name: r.name, need: fmt(r.need), stock: r.stock === null ? '0' : fmt(r.stock), unit: r.unit, short: fmt(r.short) };
      out.push({ id: app + ':supply.short:' + r.id, code: 'supply.short', severity: 'warning', about, msg: { key: keys.short, vars }, text: t(keys.short, vars),
        req: { op: '>=', value: Math.round(r.need * 100) / 100, unit: r.unit }, found: { value: r.stock === null ? 0 : r.stock, unit: r.unit }, acceptable: true });
    } else if (r.status === 'late') {
      const vars = { name: r.name, lead: r.lead === null ? '?' : fmt(r.lead), limit: fmt(res.leadDays) };
      out.push({ id: app + ':supply.lead:' + r.id, code: 'supply.lead', severity: 'note', about, msg: { key: r.backorder ? keys.back : keys.lead, vars }, text: t(r.backorder ? keys.back : keys.lead, vars),
        ...(r.lead !== null ? { req: { op: '<=', value: res.leadDays, unit: 'days' }, found: { value: r.lead, unit: 'days' } } : {}), acceptable: true });
    }
  }
  return out;
}
supply.js TakeoffCraft 0.8.1 64-bit 6 calculations Reviewed

Tests None found by the review

Stock figure from text

64-bit

Reads a stock cell such as 1,200 or Out of stock as a number, or as in stock or not.

The same code as DetailCraft's copy: commas removed, converted with Number, a leading comparison sign and a trailing + dropped, values above 1e12 treated as unknown.

  • supply.js
  • lines 36 to 46
  • TakeoffCraft 0.8.1
/** "1,200" → 1200 · "Out of stock" / "OOS" → 0 · "In stock" → inStock · "" → unknown. */
export function parseStock(text) {
  const s = clean(text, 60).toLowerCase();
  if (!s) return { stock: null, inStock: null };
  if (/^(out of stock|oos|none|sold out|nil)$/.test(s)) return { stock: 0, inStock: false };
  if (/^(in stock|yes|available|y)$/.test(s)) return { stock: null, inStock: true };
  const m = /^([<>~≥≤]?\s*)?(\d[\d,]*(?:\.\d+)?)\s*\+?\s*([a-z.\/²³]*)$/.exec(s);
  if (!m) return { stock: null, inStock: null };
  const n = Number(m[2].replace(/,/g, ''));
  return Number.isFinite(n) && n >= 0 && n <= 1e12 ? { stock: n, inStock: n > 0 } : { stock: null, inStock: null };
}

Lead time in days

64-bit

Reads a lead time such as 2-3 weeks or 5 business days as a number of days.

weeks × 7; months × 30; business days → ⌈n × 7 ÷ 5⌉; result rounded to 0.1 day

A range uses the larger of its two numbers. The result is rounded with Math.round(days × 10) ÷ 10 and kept only from 0 to 3650 days. A month is taken as 30 days.

  • supply.js
  • lines 47 to 59
  • TakeoffCraft 0.8.1
/** "5 days" · "2 wks" · "2-3 weeks" (the longer) · "3 months" · "same day" / "stock" → 0 · "backorder" → null + backorder. `bare` = the unit of a plain number. */
export function parseLead(text, bare = 'day') {
  const s = clean(text, 60).toLowerCase();
  if (!s) return { lead: null, backorder: false };
  if (/back\s*order|tbd|call|unknown|n\/a|discontinued/.test(s)) return { lead: null, backorder: true };
  if (/^(same day|immediate|immediately|stock|in stock|now|next day)$/.test(s)) return { lead: /next/.test(s) ? 1 : 0, backorder: false };
  const re = /(\d+(?:\.\d+)?)(?:\s*[-–to]+\s*(\d+(?:\.\d+)?))?\s*(day|d\b|business day|week|wk|w\b|month|mo\b)?/;
  const m = re.exec(s);
  if (!m) return { lead: null, backorder: false };
  const n = Math.max(Number(m[1]), m[2] ? Number(m[2]) : 0), u = (m[3] || bare).replace(/s$/, '');
  const days = /^(week|wk|w)/.test(u) ? n * 7 : /^(month|mo)/.test(u) ? n * 30 : /^business/.test(u) ? Math.ceil(n * 7 / 5) : n;
  return days >= 0 && days <= 3650 ? { lead: Math.round(days * 10) / 10, backorder: false } : { lead: null, backorder: false };
}

Price from text

64-bit

Reads a price cell as a whole number of 1/10000 dollar, the unit the takeoff's rates use.

price = round(n × 10000)

The first number in the cell is read, commas removed, and rounded to a whole 1/10000 dollar with Math.round. A price with more than 4 decimals loses the rest. Prices of 1e9 dollars or more are dropped.

  • supply.js
  • line 60
  • TakeoffCraft 0.8.1
const money = (text) => { const m = /(\d[\d,]*(?:\.\d+)?)/.exec(String(text ?? '')); if (!m) return null; const n = Number(m[1].replace(/,/g, '')); return Number.isFinite(n) && n >= 0 && n < 1e9 ? Math.round(n * 10000) : null; };   // the model's rate units (1/10000 $)

Short, late or in stock

64-bit

For each estimate row with a quantity: the matching supplier row, the shortfall and the status.

short = max(0, need − stock); late: no stock figure and lead > limit

The need is the estimate row's quantity with waste, unrounded (needsOf). The shortfall is one subtraction clamped at zero with no tolerance, so a need above the stock by rounding noise is reported short. A supplier row in another unit is reported as a unit mismatch; nothing is converted.

  • supply.js
  • lines 158 to 178
  • TakeoffCraft 0.8.1
/** Each need against the supplier rows. status: ok · short (stock below the need) · late (lead time over the person's limit) · unknown (no stock figure) · unit (the supplier's unit is not the estimate's). */
export function check(est, supply) {
  const all = (supply?.sources || []).flatMap((s) => s.rows.map((r) => ({ ...r, source: s.name })));
  const ignore = new Set(supply?.ignore || []);
  const limit = supply?.leadDays ?? 14;
  const rows = [], unmatched = [];
  for (const n of needsOf(est)) {
    if (ignore.has(n.id)) continue;
    const m = all.length ? matchRow(n, all) : null;
    if (!m) { unmatched.push(n); continue; }
    const r = m.row;
    const base = { ...n, row: r, exact: m.rank >= 2, stock: r.stock, lead: r.lead, backorder: r.backorder, supplier: r.supplier || r.source, price: r.price };
    if (!sameUnit(n.unit, r.unit)) { rows.push({ ...base, status: 'unit' }); continue; }
    const short = r.stock !== null ? Math.max(0, n.need - r.stock) : r.inStock === false ? n.need : null;
    const status = short > 0 ? 'short' : (r.lead !== null && r.lead > limit) || r.backorder ? (r.inStock === true || (r.stock !== null && r.stock >= n.need) ? 'ok' : 'late') : short === null && r.stock === null && r.inStock === null ? 'unknown' : 'ok';
    rows.push({ ...base, short: short || 0, status });
  }
  const waiting = rows.filter((r) => (r.status === 'short' || r.status === 'late') && r.lead !== null);
  const critical = waiting.length ? waiting.reduce((a, b) => (b.lead > a.lead ? b : a)) : null;
  return { rows, unmatched, critical, leadDays: limit };
}

Values in the reported problems

64-bit

The required and found values of a shortage or a long lead time in the problems list.

The required quantity is rounded to 2 decimals with Math.round(need × 100) ÷ 100 before it is stored in the problem; the found stock is stored as read.

  • supply.js
  • lines 180 to 196
  • TakeoffCraft 0.8.1
/** Problems in the shared shape (rule 29). `t(key, vars)` words them; ids do not change with the language. */
export function problemsOf(res, { t = (k) => k, fmt = (n) => String(Math.round(n * 100) / 100) } = {}) {
  const out = [];
  for (const r of res.rows) {
    const about = [{ kind: 'item', id: r.itemId, label: r.name }];
    if (r.status === 'short') {
      const vars = { name: r.name, need: fmt(r.need), stock: r.stock === null ? '0' : fmt(r.stock), unit: r.unit, short: fmt(r.short) };
      out.push({ id: 'takeoffcraft:supply.short:' + r.id, code: 'supply.short', severity: 'warning', about, msg: { key: 'eg51.prob.short', vars }, text: t('eg51.prob.short', vars),
        req: { op: '>=', value: Math.round(r.need * 100) / 100, unit: r.unit }, found: { value: r.stock === null ? 0 : r.stock, unit: r.unit }, acceptable: true });
    } else if (r.status === 'late') {
      const vars = { name: r.name, lead: r.lead === null ? '?' : fmt(r.lead), limit: fmt(res.leadDays) };
      out.push({ id: 'takeoffcraft:supply.lead:' + r.id, code: 'supply.lead', severity: 'note', about, msg: { key: r.backorder ? 'eg51.prob.back' : 'eg51.prob.lead', vars }, text: t(r.backorder ? 'eg51.prob.back' : 'eg51.prob.lead', vars),
        ...(r.lead !== null ? { req: { op: '<=', value: res.leadDays, unit: 'days' }, found: { value: r.lead, unit: 'days' } } : {}), acceptable: true });
    }
  }
  return out;
}

Supplier prices into the price library

64-bit

Sets a library entry's material rate to the matching supplier row's price.

The supplier's price, already a whole number of 1/10000 dollar, is copied into the entry; no arithmetic is done on it. Between two equally good matches the lower price is taken.

  • supply.js
  • lines 214 to 234
  • TakeoffCraft 0.8.1
/**
 * Supplier prices into the price Library. For each library entry with a priced supplier row of the same name (and a unit that is the same, or not
 * said), the entry's material rate becomes the supplier's price; labour, waste, height and assembly stay. Components are not priced (a supplier lists
 * products, not formulas). The entry's note says where the price came from. Nothing is changed that the file did not name.
 * → { lib (a new list), updated: [{ name, from, to, supplier, file, exact }], skipped: [{ name, why: 'noprice' | 'unit' | 'similar' }] }
 * `only` = 'exact' leaves a similar-name match (rank 1) out. Rates are in the model's units (1/10000 of a dollar).
 */
export function libraryPrices(lib, supply, { only = 'any', at = new Date().toISOString().slice(0, 10) } = {}) {
  const all = (supply?.sources || []).flatMap((s) => s.rows.map((r) => ({ ...r, file: s.name })));
  const updated = [], skipped = [];
  const out = (lib || []).map((e) => {
    const m = priced(e.name, all);
    if (!m) { skipped.push({ name: e.name, why: 'noprice' }); return e; }
    if (!sameUnit(e.unit, m.row.unit)) { skipped.push({ name: e.name, why: 'unit' }); return e; }
    if (only === 'exact' && m.rank < 2) { skipped.push({ name: e.name, why: 'similar' }); return e; }
    if (m.row.price === e.mat) return e;
    updated.push({ name: e.name, from: e.mat, to: m.row.price, supplier: m.row.supplier || m.row.file, file: m.row.file, exact: m.rank >= 2 });
    return { ...e, mat: m.row.price, note: clean('Material price from ' + (m.row.supplier || m.row.file) + ' (' + m.row.file + ', ' + at + ').', 1000) };   // i18n-ignore: a note kept with the entry
  });
  return { lib: out, updated, skipped };
}

In detail

Method
short = max(0, need − stock). A row with no stock figure whose lead time is above the person's limit (14 days unless set), or which is on backorder, is late. The longest lead time among short or late rows is reported as critical.
Number types
64-bit numbers; prices are integers of 1/10000 dollar.
Tolerances
None: need and stock are compared directly. The value of a reported requirement is rounded to 2 decimals.
Limits
A month counts as 30 days. A lead time written as a range uses the larger number. Only the first number in a price cell is read. Units are matched by name; the only conversion is kilograms to tonnes in DetailCraft's copy.

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