Resources Numerical accuracy Calculation policy

Typed lengths and item units

Reads a typed length (feet-inches with fractions, decimal feet, inches, meters, centimeters, millimeters) into feet, and converts feet and square feet into an item's unit.

How the precision is kept

Every conversion factor is the defined value: 12 inches to the foot, 0.3048 m to the foot, 0.09290304 m² to the square foot (0.3048² written out), 9 square feet to the square yard, 100 to the roofing square, 43,560 to the acre. A fraction is one integer divided by another. Results are 64-bit numbers in feet; nothing is rounded when it is stored.

Calculations under this policy

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

units.js TakeoffCraft 0.8.1 64-bit 7 calculations Reviewed

Tests bin/test-takeoffcraft.mjs, bin/test-takeoffcraft-cad.mjs

Typed length to feet

64-bit

Normalizes what was typed and reads it as one length, or as arithmetic over lengths.

The function only rewrites text and chooses a path. A single length goes to parseOne; text with + − × ÷ or brackets goes to lencalc.js, where a bare number is a plain factor until it is used as a length. The result must be finite and above zero.

  • units.js
  • lines 30 to 51
  • TakeoffCraft 0.8.1
/**
 * Parse a typed distance into feet.
 *   24'-0"  12' 6 1/2"  12'-6-1/2"  6 1/2"  3/4"  24.5'  24.5 (→ defaultUnit)  24 ft 6 in
 *   3.2 m   3200 mm   320 cm
 * and arithmetic over those (0.6 DF.3, lencalc.js): 4+7/16, 12'6" + 3/4", (24'-0")/3, 2*450mm; spaces round a dash
 * subtract (24' - 6" = 23'-6"), a dash written into a length is its own (24'-6", 6-1/2").
 * Returns { ok: true, ft } or { ok: false, error }. Negative and zero values are refused.
 */
export function parseLength(text, { defaultUnit = 'ft' } = {}) {
  let s = String(text ?? '').trim().toLowerCase();
  if (!s) return { ok: false, error: t('len.err.empty') };
  s = s.replace(/[′’‘`]/g, "'").replace(/[″“”]/g, '"').replace(/''/g, '"').replace(/[−–—]/g, '-').replace(/,/g, '');
  for (const [re, to] of WORDS) s = s.replace(re, to);
  s = s.replace(/\s+/g, ' ').trim();
  if (hasArithmetic(s)) {
    // a bare number is plain (a factor) until it is used as a length: then it is in the default unit (plain: its feet)
    const r = calc(s, (x) => { const bare = /^[\d.\s/-]+$/.test(x), one = parseOne(x, bare ? 'ft' : defaultUnit, x); return one.ok ? { value: one.ft, unit: !bare } : null; }, { plain: toFt(1, defaultUnit) });
    if (!r.ok) return { ok: false, error: t('calc.err.' + r.code, { pos: r.pos + 1, text: r.text }) };
    return positive(r.value);
  }
  return parseOne(s, defaultUnit, String(text));
}

One length with a metric unit, or feet and inches

Exact

Reads one normalized length into feet.

ft = meters ÷ 0.3048

Metric input is parseFloat, then ÷ 1000 (mm) or ÷ 100 (cm), then ÷ 0.3048. The factors are the defined ones; each division is rounded once in 64-bit. Any other unit mark is passed to the shared table's parseTo and its value in feet is used.

  • units.js
  • lines 53 to 76
  • TakeoffCraft 0.8.1
/** One length, already normalized (lower case, marks for the words): → { ok, ft } | { ok: false, error }. `raw` = as typed
 *  (0.7 UNITS: any other length mark of the shared table, 3 yd, 2 ch, 10 US ft, 1.5 km; inside a sum the marks are read
 *  in lower case, so there nm is the nanometre — type nmi for the nautical mile). */
function parseOne(s, defaultUnit, raw = null) {
  if (s.startsWith('-')) return { ok: false, error: t('len.err.positive') };

  // metric: one number and a unit
  const m = s.match(/^(\d+(?:\.\d*)?|\.\d+)\s*(mm|cm|m)$/);
  if (m) {
    const v = parseFloat(m[1]);
    const meters = m[2] === 'mm' ? v / 1000 : m[2] === 'cm' ? v / 100 : v;
    return positive(meters / M_PER_FT);
  }
  if (/[a-z]/.test(s)) {
    const u = raw === null ? null : parseTo(String(raw).replace(/,/g, ''), 'length', 'ft');
    return u && u.ok ? positive(u.value) : { ok: false, error: t('len.err.units') };
  }

  const tokens = tokenize(s);
  if (!tokens) return { ok: false, error: t('len.err.read') };
  const r = imperial(tokens, defaultUnit);
  if (!r.ok) return r;
  return positive(r.ft);
}

Numbers and fractions in a typed length

Exact

Splits the text into numbers, fractions and foot or inch marks.

fraction = a ÷ b (integers)

A fraction must be two whole numbers; it is parseInt ÷ parseInt, one rounded division, and a zero denominator is refused. A decimal number goes through parseFloat. A number with a decimal point cannot be part of a fraction.

  • units.js
  • lines 84 to 103
  • TakeoffCraft 0.8.1
/** Numbers, fractions (a/b), foot and inch marks. Dashes and spaces only separate. */
function tokenize(s) {
  const out = [];
  let i = 0;
  while (i < s.length) {
    const c = s[i];
    if (c === ' ' || c === '-') { i++; continue; }
    if (c === "'" || c === '"') { out.push({ t: c }); i++; continue; }
    const m = s.slice(i).match(/^(\d+(?:\.\d*)?|\.\d+)(?:\s*\/\s*(\d+(?:\.\d*)?|\.\d+))?/);
    if (!m) return null;
    if (m[2] !== undefined) {
      if (m[1].includes('.') || m[2].includes('.')) return null;
      const den = parseInt(m[2], 10);
      if (den === 0) return null;
      out.push({ t: 'frac', v: parseInt(m[1], 10) / den });
    } else out.push({ t: 'num', v: parseFloat(m[1]), int: !m[1].includes('.') });
    i += m[0].length;
  }
  return out;
}

Feet and inches combined

Exact

Combines a feet part and an inches part (whole number plus fraction) into feet.

ft = feet + (inches + fraction) ÷ 12

The whole inches and the fraction are added first, then divided by 12 once and added to the feet. Inches ÷ 12 is not exact in binary for most values, so the sum carries one rounded division and one rounded addition.

  • units.js
  • lines 105 to 143
  • TakeoffCraft 0.8.1
/** [num]['] [num][frac]["]  — a feet part, an inches part, or a bare value in the default unit. */
function imperial(tk, defaultUnit) {
  let i = 0;
  const value = () => {
    // a number, optionally followed by a fraction (whole + fraction), or a lone fraction
    const a = tk[i];
    if (!a) return null;
    if (a.t === 'frac') { i++; return a.v; }
    if (a.t !== 'num') return null;
    i++;
    if (tk[i]?.t === 'frac') {
      if (!a.int) return null;              // 6.5 1/2 is not a number
      return a.v + tk[i++].v;
    }
    return a.v;
  };
  const bad = { ok: false, error: t('len.err.read') };
  const first = value();
  if (first === null) return bad;
  if (tk[i]?.t === "'") {
    i++;
    let inches = 0;
    if (i < tk.length) {
      const v = value();
      if (v === null) return bad;
      inches = v;
      if (tk[i]?.t === '"') i++;
    }
    if (i !== tk.length) return bad;
    return { ok: true, ft: first + inches / 12 };
  }
  if (tk[i]?.t === '"') {
    i++;
    if (i !== tk.length) return bad;
    return { ok: true, ft: first / 12 };
  }
  if (i !== tk.length) return bad;
  return { ok: true, ft: toFt(first, defaultUnit) };
}

Bare number in the default unit to feet

Exact

Converts a number in feet, inches, meters, centimeters or millimeters to feet.

ft = v ÷ 12; ft = v ÷ 0.3048; ft = v ÷ 1000 ÷ 0.3048

Inches ÷ 12; meters ÷ 0.3048; centimeters ÷ 100 ÷ 0.3048; millimeters ÷ 1000 ÷ 0.3048. The last two round twice where a single division by 30.48 or 304.8 would round once.

  • units.js
  • lines 145 to 154
  • TakeoffCraft 0.8.1
export function toFt(v, unit) {
  switch (unit) {
    case 'ft': return v;
    case 'in': return v / 12;
    case 'm': return v / M_PER_FT;
    case 'cm': return v / 100 / M_PER_FT;
    case 'mm': return v / 1000 / M_PER_FT;
    default: throw new Error('Unknown unit ' + unit);   // i18n-ignore: a programming error
  }
}

Item unit factors

Exact

The factor from feet or square feet to each unit an item can be reported in.

perFt for meters is 0.3048. perSqFt is 1 ÷ 9 (square yard), 1 ÷ 100 (roofing square), 1 ÷ 43560 (acre), 0.09290304 (m²) and 0.09290304 ÷ 10000 (hectare). All are the defined values; the three reciprocals are rounded once when the table is built.

  • units.js
  • lines 206 to 221
  • TakeoffCraft 0.8.1
export const UNITS = {
  LF:   { kind: 'linear', label: 'LF', get name() { return t('unit.lf.name'); }, perFt: 1, system: 'imperial' },
  m:    { kind: 'linear', label: 'm', get name() { return t('unit.m.name'); }, perFt: M_PER_FT, system: 'metric' },
  SF:   { kind: 'area', label: 'SF', get name() { return t('unit.sf.name'); }, perSqFt: 1, system: 'imperial' },
  SY:   { kind: 'area', label: 'SY', get name() { return t('unit.sy.name'); }, perSqFt: 1 / 9, system: 'imperial' },
  'm²': { kind: 'area', label: 'm²', get name() { return t('unit.m2.name'); }, perSqFt: SQM_PER_SQFT, system: 'metric' },
  EA:   { kind: 'count', label: 'EA', get name() { return t('unit.ea.name'); }, system: 'any' },
  // typed quantities only (0.6 TK.5+): a lump sum, and volumes (CivilCraft's cut and fill arrive in these)
  LS:   { kind: 'fixed', label: 'LS', get name() { return t('unit.ls.name'); }, system: 'any' },
  CY:   { kind: 'fixed', label: 'CY', get name() { return t('unit.cy.name'); }, system: 'imperial' },
  'm³': { kind: 'fixed', label: 'm³', get name() { return t('unit.m3.name'); }, system: 'metric' },
  // 0.7 UNITS: roofing squares, acres and hectares (names are the shell's words, shell.units.*)
  SQ:   { kind: 'area', label: 'SQ', get name() { return unitName('square'); }, perSqFt: 1 / 100, system: 'imperial' },
  ac:   { kind: 'area', label: 'ac', get name() { return unitName('acre'); }, perSqFt: 1 / 43560, system: 'imperial' },
  ha:   { kind: 'area', label: 'ha', get name() { return unitName('ha'); }, perSqFt: SQM_PER_SQFT / 10000, system: 'metric' },
};

Feet and square feet into an item's unit

Exact

Converts a length in feet or an area in square feet to the unit that goes with an item.

m = ft × 0.3048; area = sf × perSqFt

One multiplication by the table factor: feet × 0.3048 for meters, square feet × perSqFt for areas. A unit with no area factor falls back to 1.

  • units.js
  • lines 258 to 259
  • TakeoffCraft 0.8.1
export const ftTo = (ft, lu) => (lu === 'm' ? ft * M_PER_FT : ft);
export const sqftTo = (sf, au) => sf * (UNITS[au]?.perSqFt ?? 1);

In detail

Method
The text is normalized (quote marks, the words feet and inches, dashes), then read by a small tokenizer: numbers, integer fractions, foot and inch marks. Sums and products of lengths are handed to lencalc.js; other unit marks to the shared unit table's parseTo.
Number types
64-bit numbers. Decimal text goes through parseFloat; fraction parts through parseInt.
Limits
0.3048 and 0.09290304 are not exactly representable in binary, so the nearest 64-bit value is used. Feet from millimeters or centimeters take two divisions (÷ 1000 or ÷ 100, then ÷ 0.3048), each rounded once. Zero and negative lengths are refused.

Approach: Exact values

Constants and factors are held as exact fractions from their definitions. The arithmetic is ordered so that no rounded factor is ever used.

Every policy that takes this approach