Resources Numerical accuracy Calculation policy

Bearings and degrees-minutes-seconds

Reads typed quadrant bearings and degrees-minutes-seconds angles, and writes angles in decimal degrees, degrees-minutes-seconds, grads, radians or surveyor's bearings.

How the precision is kept

Typed parts are read as 64-bit numbers and combined as d + m ÷ 60 + s ÷ 3600. For output the angle is first turned into a whole count of the smallest shown part (degrees, minutes, seconds or decimal seconds) and then split with whole-number arithmetic, so a value never prints 60 minutes or 60 seconds. A fixed 1e-9 is added to that count before it is rounded.

Calculations under this policy

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

angles.js CadCraft 0.8.2 64-bit 6 calculations Reviewed

Tests bin/test-cadcraft.mjs

Degrees, minutes, seconds to decimal degrees

64-bit

The decimal angle for text such as 45°23'30", 45d23'30" or, inside a bearing, 45-23-30.

deg = d + m ÷ 60 + s ÷ 3600

Each part is converted from its text once, then the three terms are added in one expression. Minutes or seconds of 60 or more return null, and a decimal is accepted only on the last part given, so no carry is needed. Division by 60 and by 3600 is not exact in binary; the sum is rounded to the nearest 64-bit number.

  • angles.js
  • lines 24 to 35
  • CadCraft 0.8.2
/** Degrees, minutes, seconds text → decimal degrees, or null (minutes and seconds under 60). */
function dms(text, allowDash) {
  const s = clean(text);
  let m = DMS_SYM.exec(s), d, mi = 0, se = 0;
  if (m) { d = +m[1]; mi = +(m[2] ?? 0); se = +(m[3] ?? 0); if (m[4] !== undefined) se = +m[4]; }
  else if (allowDash && (m = DMS_DASH.exec(s))) { d = +m[1]; mi = +m[2]; se = +(m[3] ?? 0); }
  else if (allowDash && /^(\d+(?:\.\d*)?|\.\d+)$/.test(s)) d = +s;
  else return null;
  if (mi >= 60 || se >= 60) return null;
  if ((mi % 1 !== 0 && se) || (d % 1 !== 0 && (mi || se))) return null;   // only the last part may carry a decimal
  return d + mi / 60 + se / 3600;
}

Bearing or DMS text to an angle

64-bit

The angle in degrees, counter-clockwise from east, for a typed bearing such as N45°23'30"E or a plain DMS angle.

N…E: 90 − off; N…W: 90 + off; S…E: 270 + off; S…W: 270 − off

The offset must be 0 to 90. Each quadrant is one addition or subtraction with a whole number, and 360 is subtracted from a result of 360 or more. A plain signed angle is negated after its parts are summed.

  • angles.js
  • lines 37 to 55
  • CadCraft 0.8.2
/** A bearing or an angle in degrees-minutes-seconds → degrees (math angle), or null when the text is neither. */
export function parseAngle(raw) {
  const s = clean(raw);
  if (!s) return null;
  let m = /^([NS])\s*(.*?)\s*([EWO])$/i.exec(s);
  if (m) {
    const off = dms(m[2], true);
    if (off === null || off > 90) return null;
    const north = m[1].toUpperCase() === 'N', east = m[3].toUpperCase() === 'E';
    const a = north ? (east ? 90 - off : 90 + off) : (east ? 270 + off : 270 - off);
    return a >= 360 ? a - 360 : a;
  }
  if ((m = /^([NSEWO])$/i.exec(s))) return { N: 90, S: 270, E: 0, W: 180, O: 180 }[m[1].toUpperCase()];
  if (/d/i.test(s)) {
    const neg = s[0] === '-', v = dms(neg || s[0] === '+' ? s.slice(1).trim() : s, false);
    return v === null ? null : neg ? -v : v;
  }
  return null;
}

Decimal degrees to degrees, minutes and seconds

64-bit

The degrees, minutes and seconds text for an angle at the chosen precision.

n = round(deg × unit + 10⁻⁹); d = ⌊n ÷ unit⌋; m = ⌊rest ÷ (unit ÷ 60)⌋; s = (rest − m × unit ÷ 60) ÷ 10^secDec

unit is 1, 60 or 3600 × 10^(precision − 4), so n is a whole count of the smallest shown part and the split is whole-number arithmetic. The 1e-9 is added to that count, not to the angle. Precisions 1 and 2 both show minutes; 3 and 4 both show whole seconds.

  • angles.js
  • lines 61 to 71
  • CadCraft 0.8.2
function split(deg, prec) {
  // round the total at the shown precision first, so 44.99999 never prints 44°59'60"
  const secDec = Math.max(0, prec - 4);
  const unit = prec <= 0 ? 1 : prec <= 2 ? 60 : 3600 * Math.pow(10, secDec);   // parts of a degree kept
  let n = Math.round(deg * unit + 1e-9);
  const d = Math.floor(n / unit); n -= d * unit;
  if (prec <= 0) return { d, text: d + '°' };
  if (prec <= 2) return { d, text: d + '°' + String(n).padStart(2, '0') + "'" };
  const mi = Math.floor(n / (3600 * Math.pow(10, secDec) / 60)), sec = (n - mi * (unit / 60)) / Math.pow(10, secDec);
  return { d, text: d + '°' + String(mi).padStart(2, '0') + "'" + (secDec ? sec.toFixed(secDec).padStart(3 + secDec, '0') : String(Math.round(sec)).padStart(2, '0')) + '"' };
}

Angle to a surveyor's bearing

64-bit

Bearing text such as N45°23'30"E for an angle in degrees.

a = deg mod 360; offset = |90 − a| (north half) or |270 − a| (south half)

The angle is reduced to 0…360 with two remainders. Within 1e-9 degrees (absolute) of an axis it is written as one letter; otherwise the offset is one subtraction and goes through split. An offset that rounds to exactly 90° is written as due E or W.

  • angles.js
  • lines 73 to 85
  • CadCraft 0.8.2
/** Bearing text for a math angle in degrees: N45°23'30"E (the quadrant letters, 0 < offset < 90; due N, E, S, W alone). */
export function bearingFromAngle(deg, prec = 4) {
  let a = ((deg % TAU) + TAU) % TAU;
  const eps = 1e-9;
  for (const [v, w] of [[90, 'N'], [0, 'E'], [270, 'S'], [180, 'W']]) if (Math.abs(a - v) < eps || (v === 0 && Math.abs(a - 360) < eps)) return w;
  const north = a < 180;
  const off = north ? Math.abs(90 - a) : Math.abs(270 - a);
  const east = a < 90 || a > 270;
  const r = split(off, prec);
  // rounding can land on exactly 90°: that is the next axis
  const text = r.d >= 90 && /^90°(00'(00")?)?$/.test(r.text) ? null : r.text;
  return text === null ? (north ? (east ? 'E' : 'W') : (east ? 'E' : 'W')) : (north ? 'N' : 'S') + text + (east ? 'E' : 'W');
}

Angle in the drawing's angle format

64-bit

An angle written as decimal degrees, degrees-minutes-seconds, grads, radians or a bearing.

grads = a × 10 ÷ 9; radians = a × π ÷ 180

The angle is reduced to 0…360 first. Grads and radians are one multiplication and one division, then toFixed at the chosen precision (0 to 8); decimal degrees use toFixed and drop trailing zeros. The | 0 in the switch is applied to the format code, not to a measured value.

  • angles.js
  • lines 87 to 98
  • CadCraft 0.8.2
/** An angle in degrees as the drawing's angle format writes it (AUNITS: 0 decimal, 1 deg-min-sec, 2 grads, 3 radians, 4 surveyor's). */
export function formatAngle(deg, aunits = 0, prec = 4) {
  const a = ((deg % TAU) + TAU) % TAU;
  const p = Math.max(0, Math.min(8, Math.round(prec)));
  switch (aunits | 0) {
    case 1: return split(a, p).text;
    case 2: return (a * 10 / 9).toFixed(p) + 'g';
    case 3: return (a * Math.PI / 180).toFixed(p) + 'r';
    case 4: return bearingFromAngle(a, p);
    default: { const s = a.toFixed(p); return (p ? s.replace(/\.?0+$/, '') : s) + '°'; }
  }
}

Bearing of a vector

64-bit

The bearing text for a direction given by its x and y components.

angle = atan2(dy, dx) × 180 ÷ π

One atan2 and a conversion to degrees with Math.PI, then bearingFromAngle.

  • angles.js
  • line 100
  • CadCraft 0.8.2
export const bearingText = (dx, dy, prec = 4) => bearingFromAngle(Math.atan2(dy, dx) * 180 / Math.PI, prec);

In detail

Method
Bearing to angle in degrees counter-clockwise from east: N…E = 90 − offset, N…W = 90 + offset, S…W = 270 − offset, S…E = 270 + offset (the convention stated in the file header).
Number types
64-bit numbers; the counts used to split an angle are whole numbers held in 64-bit.
Tolerances
1e-9 added to the count of the smallest shown part before rounding (in units of that part). 1e-9 degrees, absolute: an angle this close to 0, 90, 180 or 270 is written as due E, N, W or S.
Limits
Minutes or seconds of 60 or more are refused, and only the last typed part may carry a decimal. A bearing's offset must be 0 to 90. Output precision is limited to 0 to 8.

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