Units, precision and tolerance
Internal units vs display units, why 0.1 + 0.2 is not 0.3, why float is dangerous far from the origin, and how to compare coordinates with a tolerance.
Internal units and display units
| Conversion | Exact factor |
|---|---|
const double MmPerFoot = 304.8; static double MmToFeet(double mm) => mm / MmPerFoot;static double FeetToMm(double ft) => ft * MmPerFoot; double wallFt = MmToFeet(3000);Console.WriteLine(wallFt);Console.WriteLine(FeetToMm(wallFt));9.84251968503937 3000
Floating point is not exact
double stores about 15–17 significant digits in binary. Many simple decimals, such as 0.1, have no exact binary form, just as 1/3 has no exact decimal form. Each operation rounds to the nearest representable value, and those tiny errors add up.
double sum = 0;for (int i = 0; i < 10; i++) sum += 0.1; Console.WriteLine(sum);Console.WriteLine(sum == 1.0);Console.WriteLine(0.1 + 0.2);0.9999999999999999 False 0.30000000000000004
double angle = Math.PI / 2; // 90 degreesdouble x = 10 * Math.Cos(angle), y = 10 * Math.Sin(angle); Console.WriteLine($"({x}, {y})");Console.WriteLine(x == 0);(6.123233995736766E-16, 10) False
Compare with a tolerance
==. Ask instead whether they are close enough: within a tolerance chosen for the job.
using P3 = (double X, double Y, double Z); const double Tol = 1e-9; static bool Near(double a, double b) => Math.Abs(a - b) <= Tol;static bool SamePoint(P3 p, P3 q) => Near(p.X, q.X) && Near(p.Y, q.Y) && Near(p.Z, q.Z); double x = 10 * Math.Cos(Math.PI / 2);P3 wallEnd = (x, 10, 0);P3 nextWallStart = (0, 10, 0); Console.WriteLine(wallEnd == nextWallStart);Console.WriteLine(SamePoint(wallEnd, nextWallStart));False True
Too small (say 1e-15) and normal rounding errors make equal points look different, so walls fail to join. Too large (say 1 mm in a model measured in mm) and genuinely different points merge, so small details vanish. A good absolute tolerance sits far above rounding noise and far below the smallest real detail. For building-scale models in metres, 1e-9 to 1e-6 is typical; BIM kernels use a fixed tolerance tied to their internal unit. For values that span huge ranges (areas of a bolt and of a site), a relative tolerance, Math.Abs(a - b) <= tol * Math.Max(Math.Abs(a), Math.Abs(b)), works better.
float vs double: precision depends on distance from the origin
float has only about 7 significant digits, so 10 km from the origin, measured in millimetres, it cannot even represent a whole-millimetre step reliably.
float farF = 10_000_000f; // 10 km in millimetres, as floatdouble farD = 10_000_000.0; // the same, as double Console.WriteLine(farF + 0.3f == farF);Console.WriteLine(farD + 0.3 == farD);True False
float changes nothing at all: the detail is lost. Try it below: move the slider from the origin out to site scale and watch the gap between representable values.
Round for display, not for storage
6.12E-16 as 0.00 with :F2, but keep the stored value as it is. Rounding stored coordinates every time adds a new error on each edit and can push points across a tolerance boundary.
ToFeetInches(double mm) that formats a length in feet and whole inches, rounded to the nearest inch, like 9' 10". Test it with 3000 mm and 2743 mm. Watch out for a value that rounds up to 12 inches.
Show a hint
mm / 25.4), round that, then split into feet (/ 12) and inches (% 12). Rounding the total first means you can never get 12 inches.
Show the solution
static string ToFeetInches(double mm){ int totalInches = (int)Math.Round(mm / 25.4); int feet = totalInches / 12; int inches = totalInches % 12; return $"{feet}' {inches}\"";} Console.WriteLine(ToFeetInches(3000));Console.WriteLine(ToFeetInches(2743));9' 10" 9' 0"
Remember
Store lengths in one internal unit and convert only at the edges; Revit uses decimal feet internally. Doubles round: 0.1 + 0.2is0.30000000000000004, and computed coordinates are rarely exactly equal.Never compare computed coordinates with ==; use a tolerance far above rounding noise and far below real details.Precision falls with distance from the origin; floatloses millimetres at site scale, so usedoubleand keep geometry near the origin.Round when you display, not when you store.