Resources Numerical accuracy Calculation policy

LISP arithmetic, conversions and geometry

The numbers a LISP routine gets from the built-in arithmetic, number and angle formatting, unit conversion and point geometry functions.

How the precision is kept

Reals are 64-bit numbers. Integers are 32-bit, as in the language: a whole-number result is wrapped to 32 bits with a bitwise operation, so an all-integer sum, product, power or FIX beyond ±2³¹ wraps around without a message. Unit factors are the exact defined values. Geometry functions are single closed-form expressions.

Open item. Decide whether a whole-number result outside the 32-bit range should become a real instead of wrapping around.

Calculations under this policy

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

lisp/builtins.js CadCraft 0.8.2 64-bit 12 calculations Reviewed

Tests bin/test-cadcraft-lisp.mjs

Add, subtract, multiply, divide

64-bit

The four arithmetic functions on integers and reals.

all integers: 32-bit result; any real: 64-bit result; integer ÷ integer truncates toward zero

With a real among the arguments the result is a plain 64-bit chain in argument order. With integers only, the sum is formed in 64-bit and wrapped to 32 bits, and the product uses Math.imul, so a result beyond ±2³¹ wraps around silently. Division by zero is an error.

  • lisp/builtins.js
  • lines 142 to 162
  • CadCraft 0.8.2
d('+', (a) => { a.forEach(num); return allInt(a) ? int(a.reduce((s, x) => s + x.v, 0)) : a.reduce((s, x) => s + numVal(x), 0); });
d('-', (a) => {
  a.forEach(num);
  if (!a.length) return int(0);
  if (a.length === 1) return isInt(a[0]) ? int(-a[0].v) : -a[0];
  const r = a.slice(1).reduce((s, x) => s - numVal(x), numVal(a[0]));
  return allInt(a) ? int(r) : r;
});
d('*', (a) => { a.forEach(num); if (!a.length) return int(0); return allInt(a) ? int(a.reduce((s, x) => Math.imul(s, x.v), 1)) : a.reduce((s, x) => s * numVal(x), 1); });
d('/', (a) => {
  a.forEach(num);
  if (!a.length) return int(0);
  let ints = isInt(a[0]), r = numVal(a[0]);
  for (const x of a.slice(1)) {
    const v = numVal(x);
    if (v === 0) throw new LispError('divide by zero');
    ints = ints && isInt(x);
    r = ints ? Math.trunc(r / v) : r / v;
  }
  return ints ? int(r) : r;
});

FIX: a real to an integer

64-bit

Drops the fraction of a number.

int(trunc(x))

Math.trunc drops the fraction, then the integer type applies value | 0. For |x| of 2³¹ or more that bitwise step wraps the value: (fix 3e9) gives −1294967296.

  • lisp/builtins.js
  • lines 167 to 168
  • CadCraft 0.8.2
d('FIX', (a) => { arity(a, 1); return int(Math.trunc(num(a[0]))); });
d('FLOAT', (a) => { arity(a, 1); return num(a[0]); });

Square root, exponential, logarithm and power

64-bit

SQRT, EXP, LOG and EXPT.

Math.sqrt, Math.exp, Math.log, Math.pow

A negative square root and a logarithm of zero or less are errors, not NaN. EXPT with an integer base and a non-negative integer exponent returns a 32-bit integer, so (expt 2 31) wraps to −2147483648.

  • lisp/builtins.js
  • lines 181 to 184
  • CadCraft 0.8.2
d('SQRT', (a) => { arity(a, 1); const v = num(a[0]); if (v < 0) throw new LispError('function undefined for argument: ' + show(a[0])); return Math.sqrt(v); });
d('EXP', (a) => { arity(a, 1); return Math.exp(num(a[0])); });
d('LOG', (a) => { arity(a, 1); const v = num(a[0]); if (v <= 0) throw new LispError('function undefined for argument: ' + show(a[0])); return Math.log(v); });
d('EXPT', (a) => { arity(a, 2); const b = num(a[0]), e = num(a[1]); return isInt(a[0]) && isInt(a[1]) && e >= 0 ? int(Math.pow(b, e)) : Math.pow(b, e); });

EQUAL with a fuzz

64-bit

Whether two numbers or lists of numbers are equal within a given amount.

|a − b| ≤ fuzz

The fuzz is an absolute difference chosen by the routine; with none given the numbers must be identical.

  • lisp/builtins.js
  • lines 37 to 49
  • CadCraft 0.8.2
// ---- equality ----
export function equal(a, b, fuzz = 0) {
  if (a === b) return true;
  if (isNum(a) && isNum(b)) return Math.abs(numVal(a) - numVal(b)) <= fuzz;
  if (a instanceof Cons && b instanceof Cons) {
    while (a instanceof Cons && b instanceof Cons) { if (!equal(a.car, b.car, fuzz)) return false; a = a.cdr; b = b.cdr; }
    return equal(a, b, fuzz);
  }
  if (a instanceof Ename && b instanceof Ename) return a.id === b.id;
  if (a instanceof PickSet && b instanceof PickSet) return a === b;
  if (a && b && typeof a.same === 'function') return a.same(b);
  return false;
}

RTOS: a number as text

64-bit

Formats a length in decimal or in the drawing's other length formats.

decimal: v.toFixed(prec); 0 when |v| < 0.5 × 10⁻ᵖʳᵉᶜ

The decimal mode rounds once with toFixed; the precision is limited to 0 to 8 decimals by the caller. A routine that reads the text back gets the rounded value. Other modes go through the app's length formatter in units.js.

  • lisp/builtins.js
  • lines 63 to 72
  • CadCraft 0.8.2
/** rtos: mode 1 scientific, 2 decimal, 3 engineering, 4 architectural, 5 fractional; DIMZIN 8 drops trailing zeros. */
export function rtos(v, mode, prec, dimzin = 0) {
  if (mode === 2) {
    let s = (Math.abs(v) < 0.5 * Math.pow(10, -prec) ? 0 : v).toFixed(prec);
    if (dimzin & 8 && s.includes('.')) s = s.replace(/0+$/, '').replace(/\.$/, '');
    if (dimzin & 4 && /^-?0\./.test(s)) s = s.replace(/^(-?)0\./, '$1.');
    return s;
  }
  return formatLength(v, { lunits: mode, prec });
}

ANGTOS: an angle as text

64-bit

Formats an angle in degrees, degrees-minutes-seconds, grads, radians or surveyor's bearings.

degrees = a × 180 ÷ π; grads = a × 200 ÷ π; minutes = (deg − whole) × 60

The angle is first brought into 0 to 2π by two remainders. Splitting into degrees and minutes adds 1e-12 and 1e-9 before taking the whole part, so a value a hair under a whole number is not printed one unit low. The cardinal bearings are recognized within 1e-9 degree.

  • lisp/builtins.js
  • lines 73 to 102
  • CadCraft 0.8.2
/** angtos: 0 degrees, 1 deg/min/sec, 2 grads, 3 radians, 4 surveyor's units. */
export function angtos(a, mode, prec) {
  const TAU = Math.PI * 2;
  a = ((a % TAU) + TAU) % TAU;
  const fx = (v) => v.toFixed(prec);
  if (mode === 0) return fx(a * 180 / Math.PI);
  if (mode === 2) return fx(a * 200 / Math.PI) + 'g';
  if (mode === 3) return fx(a) + 'r';
  const dms = (deg) => {
    let d = Math.floor(deg + 1e-12), m = (deg - d) * 60;
    if (prec <= 0) return Math.round(deg) + 'd';
    let mi = Math.floor(m + 1e-9), s = (m - mi) * 60;
    if (prec <= 2) { mi = Math.round(m); if (mi === 60) { d++; mi = 0; } return d + 'd' + mi + "'"; }
    const sd = Math.max(0, prec - 4);
    let ss = Number(s.toFixed(sd));
    if (ss >= 60) { ss = 0; mi++; } if (mi >= 60) { mi = 0; d++; }
    return d + 'd' + mi + "'" + ss.toFixed(sd) + '"';
  };
  if (mode === 1) return dms(a * 180 / Math.PI);
  // surveyor: N/S <angle> E/W from north/south
  const deg = a * 180 / Math.PI;
  if (Math.abs(deg - 90) < 1e-9) return 'E';
  if (Math.abs(deg - 270) < 1e-9) return 'W';
  if (Math.abs(deg) < 1e-9 || Math.abs(deg - 360) < 1e-9) return 'N';
  if (Math.abs(deg - 180) < 1e-9) return 'S';
  const ns = deg < 180 ? 'N' : 'S';
  const off = deg < 180 ? Math.abs(90 - deg) : Math.abs(270 - deg);
  const ew = deg > 90 && deg < 270 ? 'W' : 'E';
  return ns + dms(off).replace(/^/, '') + ew;
}

ANGTOF: text to an angle

64-bit

Reads an angle written in any of the angle formats and returns radians.

degrees × π ÷ 180; d + m ÷ 60 + s ÷ 3600; grads × π ÷ 200

The text is converted with parseFloat or the unary plus, each to the nearest 64-bit value, then multiplied by the 64-bit π.

  • lisp/builtins.js
  • lines 103 to 118
  • CadCraft 0.8.2
/** angtof: text in a mode → radians, or null. */
export function angtof(s, mode = 0) {
  const t = String(s).trim();
  let m;
  if (mode === 3 || /r$/i.test(t)) { const v = parseFloat(t); return Number.isFinite(v) ? v : null; }
  if (mode === 2 || /g$/i.test(t)) { const v = parseFloat(t); return Number.isFinite(v) ? v * Math.PI / 200 : null; }
  if ((m = t.match(/^([NS])\s*(.*?)\s*([EW])$/i))) {
    const off = angtof(m[2], 1);
    if (off === null) return null;
    const base = m[1].toUpperCase() === 'N' ? 90 : 270, sgn = (m[1].toUpperCase() === 'N') === (m[3].toUpperCase() === 'E') ? -1 : 1;
    return ((base * Math.PI / 180) + sgn * off + 2 * Math.PI) % (2 * Math.PI);
  }
  if ((m = t.match(/^(-?\d+(?:\.\d*)?)d(?:(\d+(?:\.\d*)?)')?(?:(\d+(?:\.\d*)?)")?$/i))) return (+m[1] + (+(m[2] || 0)) / 60 + (+(m[3] || 0)) / 3600) * Math.PI / 180;
  if (/^[-+]?(\d+\.?\d*|\.\d+)(e[-+]?\d+)?$/i.test(t)) return parseFloat(t) * Math.PI / 180;
  return null;
}

Unit factors for CVUNIT

64-bit

The factor of each unit to its base unit: meter, radian, kilogram or square meter.

inch 0.0254; foot 0.3048; yard 0.9144; mile 1609.344; pound 0.45359237; sq inch 0.00064516; sq foot 0.09290304; acre 4046.8564224

Every length, mass and area factor is the exact defined value, written in full, and stored as the nearest 64-bit number. The angle factors are π ÷ 180 and π ÷ 200 from the 64-bit π.

  • lisp/builtins.js
  • lines 120 to 129
  • CadCraft 0.8.2
// ---- unit conversion (cvunit) ----
const UNIT = {
  inch: ['len', 0.0254], inches: ['len', 0.0254], in: ['len', 0.0254], '"': ['len', 0.0254], foot: ['len', 0.3048], feet: ['len', 0.3048], ft: ['len', 0.3048], "'": ['len', 0.3048],
  yard: ['len', 0.9144], yards: ['len', 0.9144], yd: ['len', 0.9144], mile: ['len', 1609.344], miles: ['len', 1609.344], mi: ['len', 1609.344],
  millimeter: ['len', 0.001], millimeters: ['len', 0.001], mm: ['len', 0.001], centimeter: ['len', 0.01], centimeters: ['len', 0.01], cm: ['len', 0.01],
  meter: ['len', 1], meters: ['len', 1], m: ['len', 1], kilometer: ['len', 1000], kilometers: ['len', 1000], km: ['len', 1000],
  degree: ['ang', Math.PI / 180], degrees: ['ang', Math.PI / 180], radian: ['ang', 1], radians: ['ang', 1], grad: ['ang', Math.PI / 200], grads: ['ang', Math.PI / 200],
  kilogram: ['mass', 1], kg: ['mass', 1], gram: ['mass', 0.001], g: ['mass', 0.001], pound: ['mass', 0.45359237], lb: ['mass', 0.45359237],
  'sq inch': ['area', 0.00064516], 'sq foot': ['area', 0.09290304], 'sq meter': ['area', 1], 'sq mm': ['area', 1e-6], acre: ['area', 4046.8564224], hectare: ['area', 10000],
};

CVUNIT: convert between units

64-bit

Converts a number or a list of numbers from one unit to another of the same kind.

result = v × from ÷ to

The value is multiplied by the source factor and divided by the target factor, two rounded operations through the base unit. Inches to millimeters is therefore v × 0.0254 ÷ 0.001, not v × 25.4, and can differ from it in the last digit.

  • lisp/builtins.js
  • lines 328 to 336
  • CadCraft 0.8.2
d('CVUNIT', (a) => {
  arity(a, 3);
  const from = UNIT[str(a[1]).trim().toLowerCase()], to = UNIT[str(a[2]).trim().toLowerCase()];
  if (!from || !to || from[0] !== to[0]) return null;
  const conv = (v) => v * from[1] / to[1];
  if (isNum(a[0])) return conv(numVal(a[0]));
  if (a[0] instanceof Cons) return fromArray(toArray(a[0]).map((x) => conv(num(x))));
  throw bad('numberp', a[0]);
});

POLAR, DISTANCE and ANGLE

64-bit

A point at an angle and distance from another, the distance between two points, and the angle of the line between them.

q = p + d (cos a, sin a); dist = √(Δx² + Δy² + Δz²); angle = atan2(Δy, Δx) into 0 to 2π

Each is one 64-bit expression; the distance uses Math.hypot. The height difference counts only when both points have a z. A negative angle has 2π added.

  • lisp/builtins.js
  • lines 347 to 349
  • CadCraft 0.8.2
d('POLAR', (a) => { arity(a, 3); const p = pt(a[0]), ang = num(a[1]), dd = num(a[2]); const q = [p[0] + dd * Math.cos(ang), p[1] + dd * Math.sin(ang)]; if (p.length > 2) q.push(p[2]); return ptList(q); });
d('DISTANCE', (a) => { arity(a, 2); const p = pt(a[0]), q = pt(a[1]); const dz = p.length > 2 && q.length > 2 ? p[2] - q[2] : 0; return Math.hypot(p[0] - q[0], p[1] - q[1], dz); });
d('ANGLE', (a) => { arity(a, 2); const p = pt(a[0]), q = pt(a[1]); const v = Math.atan2(q[1] - p[1], q[0] - p[0]); return v < 0 ? v + Math.PI * 2 : v; });

INTERS: where two lines meet

64-bit

The intersection of two lines or segments given by their end points.

den = Δ₁ × Δ₂; u = ((p₃ − p₁) × Δ₂) ÷ den; q = p₁ + u Δ₁

Lines count as parallel when |den| is below 1e-12 × the product of the two lengths, a relative test. For nearly parallel lines above that limit the division by a small den enlarges rounding errors. The on-segment test allows 1e-10 in the parameters. Only x and y are intersected; z is interpolated along the first line.

  • lisp/builtins.js
  • lines 350 to 362
  • CadCraft 0.8.2
d('INTERS', (a) => {
  arity(a, 4, 5);
  const [p1, p2, p3, p4] = a.slice(0, 4).map(pt);
  const onseg = a.length < 5 || truthy(a[4]);
  const dx1 = p2[0] - p1[0], dy1 = p2[1] - p1[1], dx2 = p4[0] - p3[0], dy2 = p4[1] - p3[1];
  const den = dx1 * dy2 - dy1 * dx2;
  if (Math.abs(den) < 1e-12 * (Math.hypot(dx1, dy1) * Math.hypot(dx2, dy2) || 1)) return null;
  const u = ((p3[0] - p1[0]) * dy2 - (p3[1] - p1[1]) * dx2) / den, v = ((p3[0] - p1[0]) * dy1 - (p3[1] - p1[1]) * dx1) / den;
  if (onseg && (u < -1e-10 || u > 1 + 1e-10 || v < -1e-10 || v > 1 + 1e-10)) return null;
  const q = [p1[0] + u * dx1, p1[1] + u * dy1];
  if (p1.length > 2 && p2.length > 2) q.push(p1[2] + u * (p2[2] - p1[2]));
  return ptList(q);
});

ATOI and ATOF: text to a number

64-bit

Reads the leading whole number or real number of a text.

parseInt or parseFloat of the leading number

ATOF converts the matched text to the nearest 64-bit value. ATOI's result is a 32-bit integer, so a longer whole number wraps.

  • lisp/builtins.js
  • lines 301 to 302
  • CadCraft 0.8.2
d('ATOI', (a) => { arity(a, 1); const m = str(a[0]).trim().match(/^[-+]?\d+/); return int(m ? parseInt(m[0], 10) : 0); });
d('ATOF', (a) => { arity(a, 1); const m = str(a[0]).trim().match(/^[-+]?(\d+\.?\d*|\.\d+)(e[-+]?\d+)?/i); return m ? parseFloat(m[0]) : 0; });

In detail

Method
JavaScript 64-bit arithmetic and Math functions; Math.imul for integer products; toFixed for number text.
Number types
64-bit reals; 32-bit integers (value | 0).
Tolerances
INTERS: lines are parallel when the cross product is below 1e-12 × the product of their lengths (relative); the on-segment test allows 1e-10 in the line parameter. EQUAL's fuzz is an absolute difference given by the routine. Angle to text adds 1e-12 degree and 1e-9 minute before taking whole parts.
Limits
Integers wrap at 32 bits. RTOS and ANGTOS give at most 8 decimals.

Approach: 64-bit, direct

Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.

Every policy that takes this approach