Resources Numerical accuracy Calculation policy

Unit table: reading and writing values

Holds every unit's size as an exact fraction, reads typed values such as 12'6 1/2", 45°30'15" or 3:1, and writes values as feet and inches, degrees-minutes-seconds or decimals.

How the precision is kept

Each unit is a pair of whole numbers n and d below 2^53 taken from its legal definition, so n ÷ d has one rounding. Typed feet and inches, fractions and degrees-minutes-seconds are combined with one multiplication or division per part. When writing, the value is rounded once to a whole number of the smallest step and then split with whole-number arithmetic.

Calculations under this policy

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

unit-table.js CadCraft 0.8.2 64-bit 8 calculations Reviewed

Tests bin/test-units.mjs

Unit sizes

Exact

Builds each unit's row: its fraction of the base unit and the factor used for quick conversion.

factor = n ÷ d (× π for degrees and gons)

n and d are whole numbers written in the table above, for example 254 ÷ 10000 for the inch and 1200 ÷ 3937 for the US survey foot. factor is the one rounded 64-bit value made from them; for degrees and gons it also carries the rounding of Math.PI.

  • unit-table.js
  • lines 109 to 114
  • CadCraft 0.8.2
export const UNITS = Object.freeze(ROWS.map(([id, kind, mark, n, d, x]) => Object.freeze({
  id, kind, mark, n, d, pi: !!x.pi, factor: (x.pi ? Math.PI : 1) * n / d, on: !!x.on, dec: x.dec ?? 2, ins: x.ins ?? null,
  sys: x.sys || 'any', bare: x.bare || null, src: x.src || '', cs: !!x.cs,
  marks: Object.freeze([mark, ...(x.marks || []), ...(x.sq ? [x.sq + '2', x.sq + '^2', 'sq ' + x.sq, 'sq. ' + x.sq, 'square ' + x.sq] : []),
    ...(x.cu ? [x.cu + '3', x.cu + '^3', 'cu ' + x.cu, 'cu. ' + x.cu, 'cubic ' + x.cu] : [])]),
})));

Numbers and fractions

64-bit

Reads 6, 6.5, 6 1/2, 6-1/2 or 1/2 as a number.

whole + numerator ÷ denominator

One division and one addition. A zero denominator is refused, and so is a mixed number whose whole part has a decimal point or an exponent.

  • unit-table.js
  • lines 173 to 180
  • CadCraft 0.8.2
/** "6", "6.5", "6 1/2", "6-1/2", "1/2" → a number, else null. */
export function readNumber(s) {
  const m = MIXED.exec(String(s).trim());
  if (!m) return null;
  if (m[4] !== undefined) return +m[5] ? m[4] / m[5] : null;
  if (m[2] !== undefined) return +m[3] && !/[.eE]/.test(m[1]) ? +m[1] + m[2] / m[3] : null;
  return +m[1];
}

Feet and inches

64-bit

Reads 12'6", 12'-6 1/2" or 6" as inches.

inches = feet × 12 + inches

Feet are multiplied by the whole number 12 and added to the inches, each read by readNumber. A bare number without a foot or inch mark is not read here.

  • unit-table.js
  • lines 229 to 238
  • CadCraft 0.8.2
/** 12'  12'6"  12' 6 1/2"  12'-6-1/2"  12'-6  6"  6 1/2"  1/2" → inches, else null. */
function feetInches(s) {
  if (!/\d/.test(s)) return null;
  const m = /^(?:([\d./ ]+?)\s*')?\s*(?:-?\s*([\d./ -]+?)\s*"?)?$/.exec(s.replace(/\s*-\s*(?=\d+\s*\/)/g, ' '));
  if (!m || (m[1] === undefined && m[2] === undefined)) return null;
  if (m[1] === undefined && !/"$/.test(s)) return null;   // 6 alone is not feet-and-inches
  const ft = m[1] !== undefined ? readNumber(m[1]) : 0, inch = m[2] !== undefined && m[2].trim() !== '' ? readNumber(m[2].replace(/-/g, ' ')) : 0;
  if (ft === null || inch === null) return null;
  return ft * 12 + inch;
}

Degrees, minutes and seconds

64-bit

Reads 45°30'15" or 45d30m15s as degrees.

degrees + minutes ÷ 60 + seconds ÷ 3600

Two divisions by whole numbers and two additions in 64-bit. Minutes or seconds of 60 or more are refused.

  • unit-table.js
  • lines 239 to 246
  • CadCraft 0.8.2
/** 45°30'15"  45° 30'  45d30m15s  45°  → degrees, else null. */
function dms(s) {
  const m = /^(\d+(?:\.\d+)?)\s*(?:°|d)\s*(?:(\d+(?:\.\d+)?)\s*(?:'|m))?\s*(?:(\d+(?:\.\d+)?)\s*(?:"|s))?$/i.exec(s);
  if (!m) return null;
  const mi = m[2] === undefined ? 0 : +m[2], se = m[3] === undefined ? 0 : +m[3];
  if (mi >= 60 || se >= 60) return null;
  return +m[1] + mi / 60 + se / 3600;
}

Slopes

64-bit

Reads 2%, 3H:1V, 1:3 or 6/12 as a grade (rise over run).

percent ÷ 100; H:V → V ÷ H; rise:run → rise ÷ run

One division each. A zero run in the rise:run form is refused outright; a zero H in an H:V form divides by zero and is then refused because the result is not finite.

  • unit-table.js
  • lines 247 to 261
  • CadCraft 0.8.2
/** 2%  3H:1V  1V:3H  3:1 (H:V field) / 1:3 (rise:run)  6/12  6 in 12 → { ok, value (grade), unit }, else null. */
function slope(s, bareUnit) {
  let m = new RegExp('^(' + NUM + ')\\s*%$').exec(s);
  if (m) return done(+m[1] / 100, 'pct');
  m = new RegExp('^(' + NUM + ')\\s*H\\s*:\\s*(' + NUM + ')\\s*V$', 'i').exec(s);
  if (m) return done(+m[2] / +m[1], 'hv');
  m = new RegExp('^(' + NUM + ')\\s*V\\s*:\\s*(' + NUM + ')\\s*H$', 'i').exec(s);
  if (m) return done(+m[1] / +m[2], 'riserun');
  m = new RegExp('^(' + NUM + ')\\s*(:|/|in)\\s*(' + NUM + ')$', 'i').exec(s);
  if (m) {
    if (m[2] === ':' && bareUnit === 'hv') return done(+m[3] / +m[1], 'hv');
    return +m[3] ? done(+m[1] / +m[3], 'riserun') : fail('number');
  }
  return null;
}

Writing feet and inches

64-bit

Writes a length as feet, inches and a fraction of an inch.

n = round(|inches| × den); feet = ⌊n ÷ 12den⌋; inches = ⌊rest ÷ den⌋; fraction = rest ÷ den, reduced

The length is rounded once to a whole number of 1/den inch (den is 16 unless stated). Feet, inches and the fraction are then split with whole-number arithmetic, so a value that rounds up to 12 inches carries into the feet.

  • unit-table.js
  • lines 280 to 287
  • CadCraft 0.8.2
if (id === 'ft-in') {
  const neg = value < 0;
  let n = Math.round(Math.abs(fromBase(value, 'in')) * den);
  const ft = Math.floor(n / (12 * den)); n -= ft * 12 * den;
  const inch = Math.floor(n / den), [fn, fd] = reduce(n - inch * den, den);
  const zero = !ft && !inch && !fn;
  return (neg && !zero ? '-' : '') + ft + "'-" + inch + (fn ? ' ' + fn + '/' + fd : '') + '"';
}

Writing degrees, minutes and seconds

64-bit

Writes an angle as degrees, minutes and seconds.

t = round(|degrees| × 3600 × 10^dec); split by 3600 and 60

The angle is rounded once to a whole number of the smallest shown part of a second, then split with whole-number arithmetic, so seconds cannot print as 60.

  • unit-table.js
  • lines 288 to 296
  • CadCraft 0.8.2
if (id === 'dms') {
  const neg = value < 0;
  const p = Math.pow(10, d);
  let t = Math.round(Math.abs(fromBase(value, 'deg')) * 3600 * p);
  const dg = Math.floor(t / (3600 * p)); t -= dg * 3600 * p;
  const mi = Math.floor(t / (60 * p)); t -= mi * 60 * p;
  const sec = (t / p).toFixed(d);
  return (neg && (dg || mi || +sec) ? '-' : '') + dg + '°' + String(mi).padStart(2, '0') + "'" + sec.padStart(d ? 3 + d : 2, '0') + '"';
}

Writing decimals

64-bit

Writes a number with a fixed count of decimals.

text = v.toFixed(dec)

The number is rounded by toFixed to the unit's decimals for the text only; a result of −0 prints as 0 and trailing zeros can be dropped.

  • unit-table.js
  • lines 263 to 269
  • CadCraft 0.8.2
// ---- format ----
function num(v, dec, trim) {
  const r = Number(v.toFixed(dec));
  let s = (Object.is(r, -0) ? 0 : r).toFixed(dec);
  if (trim && s.includes('.')) s = s.replace(/\.?0+$/, '');
  return s;
}

In detail

Method
Fractions n ÷ d of the base unit (× π for degrees and gons); the file's header names the source of each definition.
Number types
64-bit numbers; whole numbers for n, d and for the split into feet, inches and fraction or degrees, minutes and seconds.
Limits
Decimal output is rounded to the stated number of decimals. A mixed number is refused when its whole part has a decimal point.

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