Resources Numerical accuracy Calculation policy

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.

engineer/eng-units.js PartCraft 0.8.1 64-bit 9 calculations Reviewed

Tests None found by the review

Defining constants

Exact

The pound, standard gravity, inch and foot, and the pound-force, gallon and British thermal unit built from them.

lbf = 0.45359237 × 9.80665; gal = 231 × 0.0254³; BTU = 1055.05585262 J

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.

  • engineer/eng-units.js
  • lines 24 to 41
  • PartCraft 0.8.1
const U = {
  // length
  m: [1, dim(1)], in: [IN, dim(1)], ft: [FT, dim(1)], yd: [3 * FT, dim(1)], mi: [5280 * FT, dim(1)], mil: [IN / 1000, dim(1)], um: [1e-6, dim(1)],
  // mass, time, temperature difference, current, amount
  g: [1e-3, dim(0, 1)], kg: [1, dim(0, 1)], t: [1000, dim(0, 1)], lb: [LBM, dim(0, 1)], lbm: [LBM, dim(0, 1)], oz: [LBM / 16, dim(0, 1)], slug: [LBF / FT, dim(0, 1)],
  s: [1, dim(0, 0, 1)], min: [60, dim(0, 0, 1)], h: [3600, dim(0, 0, 1)], d: [86400, dim(0, 0, 1)],
  K: [1, dim(0, 0, 0, 1)], R: [5 / 9, dim(0, 0, 0, 1)], A: [1, dim(0, 0, 0, 0, 1)], mol: [1, dim(0, 0, 0, 0, 0, 1)],
  // derived
  Hz: [1, dim(0, 0, -1)], N: [1, dim(1, 1, -2)], lbf: [LBF, dim(1, 1, -2)], kgf: [GN, dim(1, 1, -2)],
  Pa: [1, dim(-1, 1, -2)], bar: [1e5, dim(-1, 1, -2)], psi: [LBF / IN ** 2, dim(-1, 1, -2)], atm: [101325, dim(-1, 1, -2)], mmHg: [13595.1 * GN * 1e-3, dim(-1, 1, -2)], torr: [101325 / 760, dim(-1, 1, -2)],
  J: [1, dim(2, 1, -2)], Wh: [3600, dim(2, 1, -2)], BTU: [BTU, dim(2, 1, -2)], cal: [4.184, dim(2, 1, -2)], eV: [1.602176634e-19, dim(2, 1, -2)],
  W: [1, dim(2, 1, -3)], hp: [550 * FT * LBF, dim(2, 1, -3)],
  L: [1e-3, dim(3)], gal: [GAL, dim(3)], qt: [GAL / 4, dim(3)], floz: [GAL / 128, dim(3)],
  gpm: [GAL / 60, dim(3, 0, -1)], cfm: [FT ** 3 / 60, dim(3, 0, -1)],
  P: [0.1, dim(-1, 1, -1)], cP: [1e-3, dim(-1, 1, -1)], St: [1e-4, dim(2, 0, -1)], cSt: [1e-6, dim(2, 0, -1)],
  rad: [1, D0], deg: [Math.PI / 180, D0], rev: [2 * Math.PI, D0], rpm: [2 * Math.PI / 60, dim(0, 0, -1)],
  '%': [0.01, D0], ppm: [1e-6, D0],
};

SI prefixes

64-bit

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.

  • engineer/eng-units.js
  • line 122
  • PartCraft 0.8.1
export const cToK = (c) => c + 273.15, kToC = (k) => k - 273.15, fToK = (f) => (f - 32) * 5 / 9 + 273.15, kToF = (k) => (k - 273.15) * 9 / 5 + 32;

In detail

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

Every policy that takes this approach