Resources Numerical accuracy Calculation policy

Typed lengths, stations and unit text

Reads lengths and stations as typed, and writes stations, areas, volumes and bearings as text.

How the precision is kept

The length factors are exact: 0.0254 m per inch, 0.3048 m per foot, and 1200 ÷ 3937 m per US survey foot written as that fraction. Typed values are read with Number and combined in 64-bit without rounding. Text output is rounded to a stated number of decimals. One mix exists by stated choice: in a feet project (US survey feet) a length typed in mm, cm or m is converted with the international foot, while other unit marks go through the shared unit table to US survey feet.

Open item. Convert mm, cm and m typed into a feet project with 1200 ÷ 3937 m per foot, the same foot the project and the unit table use.

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.

units.js CivilCraft 0.8.1 64-bit 8 calculations Reviewed

Tests bin/test-civilcraft-lengths.mjs, bin/test-civilcraft.mjs

Meters per unit for units found in files

Exact

The factor for each linear unit name a LandXML file can carry.

US survey foot = 1200 ÷ 3937 m; foot = 0.3048 m

The US survey foot is written as the fraction 1200 ÷ 3937, its definition, and evaluated once in 64-bit; 0.3048 is the exact international foot. No rounded factor is used.

  • units.js
  • lines 39 to 40
  • CivilCraft 0.8.1
/** Metres per unit for linear units found in files (LandXML linearUnit names). */
export const METRES_PER = { USSurveyFoot: 1200 / 3937, foot: 0.3048, ft: 0.3048, meter: 1, m: 1, metre: 1, millimeter: 0.001, centimeter: 0.01, kilometer: 1000 };

One typed length

64-bit

Reads one length as typed (plain number, feet and inches, fraction, mm, cm or m) into project units.

feet + inches ÷ 12; meter project: feet × 12 × 0.0254; feet project, metric mark: meters ÷ 0.0254 ÷ 12

The factors 12 and 0.0254 are exact and the arithmetic is 64-bit with no rounding. In a feet project a metric length is turned into international feet (÷ 0.0254 ÷ 12) although the project's feet are US survey feet, so 1000 m and 1 km typed into a feet project differ by 2 parts per million; the file header states this choice. A fraction with a zero denominator returns no value.

  • units.js
  • lines 69 to 88
  • CivilCraft 0.8.1
/** One length as CivilCraft reads it → its value in project units (null when it is not one). */
function oneLength(x, unit) {
  let s = String(x).trim().toLowerCase().replace(/\s+/g, ' ');
  if (isNum(s)) return Number(s);
  const toUnit = (metres) => (unit === 'm' ? metres : metres / M_PER_IN / 12);   // feet project: inches to feet as 12 to 1
  const m = s.match(/^(\d+(?:\.\d*)?|\.\d+) ?(mm|cm|m)$/);
  if (m) return toUnit(Number(m[1]) * (m[2] === 'mm' ? 0.001 : m[2] === 'cm' ? 0.01 : 1));
  // a number with a fraction: 4 7/16, 4-7/16, 7/16 (in the unit it stands in)
  const frac = (q) => { const f = q.trim().match(/^(?:(\d+)[ -])?(\d+)\/(\d+)$/); if (f) return f[3] === '0' ? null : (Number(f[1] || 0) + Number(f[2]) / Number(f[3])); return isNum(q.trim()) ? Number(q) : null; };
  const bare = frac(s);
  if (bare !== null) return bare;
  // feet and inches: 12'6", 12' 6 1/2", 12'-6", 6", 3 ft 6 in, 3ft, 6 in
  s = s.replace(/\b(feet|foot|ft)\b\.?/g, "'").replace(/\b(inches|inch|in)\b\.?/g, '"').replace(/''/g, '"');
  const fi = s.match(/^(?:([\d./ -]+?) ?')?(?: ?-? ?([\d./ -]+?) ?")?$/);
  if (!fi || (!fi[1] && !fi[2])) return tableLength(x, unit);
  const ft = fi[1] ? frac(fi[1]) : 0, inch = fi[2] ? frac(fi[2]) : 0;
  if (ft === null || inch === null) return null;
  const feet = ft + inch / 12;
  return unit === 'm' ? feet * 12 * M_PER_IN : feet;
}

Station text

64-bit

Writes a station as text, such as 12+34.56 in feet or 1+234.567 in meters.

r = round(|sta|, decimals); whole = floor(r ÷ station length + 1e-9); rest = r − whole × station length

The value is rounded to the shown decimals before it is split, so a station just under a whole station does not print as 9+100.00. The 1e-9 guards the floor against a quotient that lands just below a whole number. Text only; the stored station is not changed.

  • units.js
  • lines 42 to 54
  • CivilCraft 0.8.1
/** "12+34.56" (ft) or "1+234.567" (m). Negative stations keep their sign in front: "-0+50.00". */
export function formatStation(sta, unit = 'ft', dec) {
  if (!Number.isFinite(sta)) return '';
  const u = unitOf(unit), d = dec ?? u.staDec;
  const neg = sta < 0;
  const r = Number(Math.abs(sta).toFixed(d));
  const big = Math.floor(r / u.staLen + 1e-9);
  let rest = r - big * u.staLen;
  if (rest < 0) rest = 0;
  const w = String(u.staLen).length - 1;   // digits before the point: 2 for ft, 3 for m
  const restStr = rest.toFixed(d).padStart(w + (d ? d + 1 : 0), '0');
  return (neg ? '-' : '') + big + '+' + restStr;
}

Station as typed

64-bit

Reads a station typed as 12+34.56 or as a plain number.

value = whole × station length + rest

Both parts are read with Number and combined with one multiplication by a whole number and one addition, so nothing is rounded.

  • units.js
  • lines 56 to 66
  • CivilCraft 0.8.1
/** Parses "12+34.56", "1234.56", "-0+50" → number (in the project unit), or null. */
export function parseStation(s, unit = 'ft') {
  const t = String(s ?? '').trim().replace(/\s+/g, '');
  if (!t) return null;
  const m = t.match(/^([+-]?)(\d+)\+(\d+(?:\.\d*)?)$/);
  if (m) {
    const v = Number(m[2]) * unitOf(unit).staLen + Number(m[3]);
    return m[1] === '-' ? -v : v;
  }
  return isNum(t) ? Number(t) : null;
}

Volume text

64-bit

Writes a volume in cubic yards or cubic meters, or in the volume unit chosen in Settings.

feet: V ÷ 27 cu yd; chosen unit: V × (m per unit)³ through the shared unit table

27 cubic feet per cubic yard is exact. For a chosen unit the value is first put in cubic meters with (1200 ÷ 3937)³ for feet projects, then converted by the shared table. The text is rounded to the stated decimals with thousands separators.

  • units.js
  • lines 123 to 131
  • CivilCraft 0.8.1
/**
 * A volume in the unit's report unit (cu yd for feet, cu m for metres), or in the volume unit starred in Settings ›
 * General › Units (0.7 UNITS: acre-feet, litres …); the same for areas (acres, hectares …).
 */
export function volumeText(v, unit = 'ft', d = 1) {
  const s = starred('volume', unit, { ft: 'yd3', m: 'm3' });
  if (s) return grouped(fromBase(v * (M_PER_UNIT[unit] || M_PER_UNIT.ft) ** 3, s.id), s.dec) + ' ' + s.mark;
  const u = unitOf(unit); return grouped(v / u.volPer, d) + ' ' + u.vol;
}

Area text

64-bit

Writes an area in square feet or square meters, or in the area unit chosen in Settings.

chosen unit: A × (m per unit)² through the shared unit table

For a chosen unit the value is first put in square meters with (1200 ÷ 3937)² for feet projects. The text is rounded to the stated decimals, 0 by default.

  • units.js
  • lines 132 to 136
  • CivilCraft 0.8.1
export function areaText(a, unit = 'ft', d = 0) {
  const s = starred('area', unit, { ft: 'ft2', m: 'm2' });
  if (s) return grouped(fromBase(a * (M_PER_UNIT[unit] || M_PER_UNIT.ft) ** 2, s.id), s.dec) + ' ' + s.mark;
  return grouped(a, d) + ' ' + unitOf(unit).area;
}

Bearing text

64-bit

Writes a direction as a quadrant bearing in degrees, minutes and seconds.

azimuth = (90 − a·180 ÷ π) mod 360; quadrant angle to whole seconds

The angle is rounded once, to whole seconds as a total count, and then split into degrees, minutes and seconds, so 59.6 seconds carries into the minutes instead of printing 60. A bearing therefore shows the direction to 1 second of arc.

  • units.js
  • lines 143 to 150
  • CivilCraft 0.8.1
/** Bearing text for a direction angle (radians, math convention: 0 = east, CCW) → N 45°12'30" E. */
export function bearing(a) {
  let az = (90 - a * 180 / Math.PI) % 360;   // azimuth from north, clockwise
  if (az < 0) az += 360;
  let q, ang;
  if (az <= 90) { q = ['N', 'E']; ang = az; } else if (az <= 180) { q = ['S', 'E']; ang = 180 - az; } else if (az <= 270) { q = ['S', 'W']; ang = az - 180; } else { q = ['N', 'W']; ang = 360 - az; }
  return q[0] + ' ' + dms(ang) + ' ' + q[1];
}

Degrees, minutes, seconds

64-bit

Splits an angle in degrees into whole degrees, minutes and seconds.

s = round(|deg| × 3600); d = floor(s ÷ 3600); m = floor(rest ÷ 60)

Rounds to whole seconds first and then works in whole numbers, so the parts always add up to the rounded total.

  • units.js
  • lines 151 to 156
  • CivilCraft 0.8.1
export function dms(deg) {
  let s = Math.round(Math.abs(deg) * 3600);
  const d = Math.floor(s / 3600); s -= d * 3600;
  const m = Math.floor(s / 60); s -= m * 60;
  return d + '°' + String(m).padStart(2, '0') + "'" + String(s).padStart(2, '0') + '"';
}

In detail

Method
Feet and inches as feet + inches ÷ 12; fractions as whole + numerator ÷ denominator; station as whole stations × station length + remainder.
Number types
64-bit numbers; factors 0.0254, 0.3048 and 1200 ÷ 3937 are exact by definition.
Tolerances
formatStation adds 1e-9 before taking the whole-station part; fixed() writes zero for values smaller than half the last shown decimal, so no negative zero is printed.
Limits
A feet project's feet are US survey feet (the file says so), but mm, cm and m typed into a feet project are divided by 0.0254 × 12, the international foot: a difference of 2 parts per million. Bearings are rounded to whole seconds.

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