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