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