Engineering units: factors, dimensions and conversion
Reads a unit text such as W/(m^2 K), checks it is the right kind of quantity and converts values to and from SI.
How the precision is kept
Factors are written from their definitions: 0.0254 m, 0.3048 m, 0.45359237 kg, 9.80665 m/s², 231 in³, 101325 Pa. Derived factors are products and powers of these, evaluated in 64-bit, so each carries the rounding of one or a few operations. A unit's dimension is a vector of whole exponents, compared exactly.
Calculations under this policy
9 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.
The four base constants are the defined values. The pound-force and the gallon are computed from them in 64-bit when the file loads (one multiplication; one cube and one multiplication). The IT British thermal unit is typed as 1055.05585262 J.
engineer/eng-units.js
lines 20 to 21
PartCraft 0.8.1
const LBM = 0.45359237, GN = 9.80665, IN = 0.0254, FT = 0.3048;
const LBF = LBM * GN, GAL = 231 * IN ** 3, BTU = 1055.05585262;
Unit table
Exact
Each unit's factor to SI and its dimension.
Entries are written as their definitions: psi = lbf ÷ in², hp = 550 ft·lbf/s, torr = 101325 ÷ 760, mmHg = 13595.1 × g_n × 1e-3, deg = π ÷ 180, °R = 5 ÷ 9, eV = 1.602176634e-19 J, cal = 4.184 J. The products and quotients run in 64-bit when the file loads.
A prefixed unit such as kPa or mm from its base unit.
factor = prefix × unit factor
One 64-bit multiplication of the prefix (1e-9 to 1e9) onto the unit's factor. Prefixes apply only to the units listed in PREFIXABLE.
engineer/eng-units.js
lines 62 to 69
PartCraft 0.8.1
// ---- parsing ---------------------------------------------------------------------------------------------------------------
function symbol(sym) {
if (U[sym]) return U[sym];
for (const p of Object.keys(PREFIX).sort((a, b) => b.length - a.length)) {
if (sym.startsWith(p) && sym.length > p.length) { const rest = sym.slice(p.length); if (PREFIXABLE.has(rest) && U[rest]) return [PREFIX[p] * U[rest][0], U[rest][1]]; }
}
return null;
}
Combining units
64-bit
The factor and dimension of a product or quotient of two units.
f = f_a × f_bᵉ; dimension = d_a + e × d_b
A quotient is formed as f_a × f_b ** −1, a power and a multiplication, rather than one division. Exponents are whole numbers and are added exactly.
engineer/eng-units.js
line 70
PartCraft 0.8.1
const mul = (a, b, e = 1) => ({ f: a.f * b.f ** e, d: a.d.map((x, i) => x + e * b.d[i]) });
Reading a unit text
64-bit
Turns a text such as W/(m^2 K) into one factor and one dimension.
Products and quotients are applied left to right as written, each with mul; exponents are whole numbers only. The factor of a compound unit therefore carries one rounding per operation.
engineer/eng-units.js
lines 73 to 106
PartCraft 0.8.1
export function parseUnit(text) {
const src = String(text).trim().replace(/[·⋅*]/g, ' ').replace(/\s*\^\s*/g, '^').replace(/²/g, '^2').replace(/³/g, '^3').replace(/°/g, '');
if (!src) return { f: 1, d: D0.slice() };
let i = 0;
const peek = () => src[i];
const skip = () => { while (src[i] === ' ') i++; };
function atom() {
skip();
if (peek() === '(') { i++; const v = expr(); skip(); if (peek() !== ')') throw new Error('unit: missing )'); i++; return withExp(v); }
const m = /^[A-Za-zµμ%]+/.exec(src.slice(i));
if (!m) { const one = /^1(?![\d.])/.exec(src.slice(i)); if (one) { i++; return { f: 1, d: D0.slice() }; } throw new Error('unit: cannot read "' + src.slice(i) + '"'); }
i += m[0].length;
const s = symbol(m[0]);
if (!s) throw new Error('unit: unknown unit "' + m[0] + '"');
return withExp({ f: s[0], d: s[1].slice() });
}
function withExp(v) {
const m = /^\^?(-?\d+)/.exec(src.slice(i));
if (m && (src[i] === '^' || /[\d-]/.test(src[i]))) { i += m[0].length; return pow(v, +m[1]); }
return v;
}
function product() { // atom atom … (a space is a product); stops at / or )
let v = atom();
for (;;) { skip(); const c = peek(); if (c === undefined || c === '/' || c === ')') return v; v = mul(v, atom()); }
}
function expr() { // product (/ product)* — left to right, as written
let v = product();
for (;;) { skip(); if (peek() !== '/') return v; i++; v = mul(v, product(), -1); }
}
const out = expr();
skip();
if (i < src.length) throw new Error('unit: cannot read "' + src.slice(i) + '"');
return out;
}
Value to SI
64-bit
A value in a given unit as its SI value.
SI = v × f
One multiplication, after the unit's dimension is compared entry by entry with the expected kind.
engineer/eng-units.js
line 114
PartCraft 0.8.1
export function toSI(v, unit, kind) { let u; try { u = parseUnit(unit); } catch (e) { throw new UnitError('unknown', { unit: String(unit), why: e.message }); } check(u, kind); return v * u.f; }
Value from SI
64-bit
An SI value in a given unit.
v = SI ÷ f
One division, after the same dimension check.
engineer/eng-units.js
line 115
PartCraft 0.8.1
export function fromSI(v, unit, kind) { let u; try { u = parseUnit(unit); } catch (e) { throw new UnitError('unknown', { unit: String(unit), why: e.message }); } check(u, kind); return v / u.f; }
Unit to unit
64-bit
A value converted from one unit to another of the same kind.
result = v × f_from ÷ f_to
A multiplication, then a division. The two dimension vectors must be equal entry by entry or the conversion is refused with both kinds named.
engineer/eng-units.js
lines 116 to 120
PartCraft 0.8.1
export function convert(v, from, to) {
let a, b; try { a = parseUnit(from); b = parseUnit(to); } catch (e) { throw new UnitError('unknown', { unit: from + ' → ' + to, why: e.message }); }
if (!same(a.d, b.d)) throw new UnitError('kind', { have: kindName(a.d), want: kindName(b.d) });
return v * a.f / b.f;
}
Temperature points
64-bit
Celsius and Fahrenheit readings to and from kelvin.
K = °C + 273.15; K = (°F − 32) × 5 ÷ 9 + 273.15
273.15 and 32 are the defined offsets. In fToK the multiplication by 5 comes before the division by 9; in kToF the subtraction of 273.15 comes first.
Recursive-descent parse of products, quotients and whole exponents; each unit is a factor to SI and six exponents (m, kg, s, K, A, mol). The file cites the SI Brochure (9th ed., 2019) and NIST Special Publication 811 (2008).
Number types
64-bit numbers; whole-number exponents.
Limits
A compound unit's factor is built by repeated multiplication and powers, so it can differ from the correctly rounded factor in the last bits. convert from a unit to itself computes v × f ÷ f, which is not always exactly v.
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.