Skip to content
100% in your browser. Nothing you paste is uploaded — all processing runs locally. Read more →

The math of unit conversion (and the temperature trap)

On this page
  1. Scale units — multiplication only
  2. The temperature exception
  3. The Mars Climate Orbiter
  4. The pandemic bicycle problem
  5. What our converter does
  6. When affine math also matters elsewhere
  7. Try it

Unit conversion looks trivial. You have inches, you want centimeters, you multiply by 2.54. The math is one line.

Until you hit Fahrenheit. Then it’s not one line — and the difference between “scale” units and “affine” units has caused enough production incidents that NASA crashed a spacecraft into Mars over it.

This is a short tour of the math, with the trap explained.

Scale units — multiplication only

Most units are linear scale factors: you multiply by a constant to convert.

1 inch    = 2.54 cm
1 km      = 1000 m
1 lb      = 0.453592 kg
1 mile/h  = 0.44704 m/s

The pattern: a single conversion factor relative to a base unit. A length library only needs to store factors-to-meters; everything else is multiplication.

const TO_METERS = {
  km: 1000,
  m: 1,
  cm: 0.01,
  in: 0.0254,
  ft: 0.3048,
  mile: 1609.344,
};

function convert(value, fromUnit, toUnit) {
  const meters = value * TO_METERS[fromUnit];
  return meters / TO_METERS[toUnit];
}

Three lines of math, every length unit covered. Same shape works for weight (base = kg), area (base = m²), volume (base = liter), speed (base = m/s).

The temperature exception

Temperature breaks the pattern because the zero points don’t agree.

These zeros are different physical states. So you can’t convert between scales by multiplication alone — you have to shift the zero, then scale.

°F = °C × 9/5 + 32
°C = (°F - 32) × 5/9
K  = °C + 273.15
°R = (°C + 273.15) × 9/5

The pattern is output = input × scale + offset. This is called an affine transform (vs the linear transform of the other units).

The implication for code: you can’t store temperature as “factor-to-Celsius.” You need to store both a factor and an offset, or implement two functions per unit (toBase, fromBase).

const TEMP = {
  C: { toBase: (v) => v,                    fromBase: (v) => v },
  F: { toBase: (v) => (v - 32) * 5 / 9,     fromBase: (v) => v * 9 / 5 + 32 },
  K: { toBase: (v) => v - 273.15,           fromBase: (v) => v + 273.15 },
};

function tempConvert(value, from, to) {
  const baseC = TEMP[from].toBase(value);
  return TEMP[to].fromBase(baseC);
}

A bit more code, but you can’t shortcut it.

The Mars Climate Orbiter

In 1999, NASA’s Mars Climate Orbiter burned up in the Martian atmosphere because two pieces of software — Lockheed Martin’s ground-side trajectory code and Goddard Space Flight Center’s flight-side code — disagreed on units.

The ground software output force in pound-seconds (imperial). The flight software expected newton-seconds (SI). Multiplier mismatch: about 4.45.

Lost spacecraft: $327 million. Lost mission: 16 months of work. Cost of the missing line of code that should have applied the unit conversion: zero.

The investigation report named the proximate cause as “failure to use metric units” but the deeper cause is more useful: a system can exchange numbers without exchanging units, and when that happens, the receiving software has to assume what the units are. Whenever the assumption is wrong, you get a bug. Sometimes the bug burns up in an atmosphere.

The fix is to attach units to numbers — either via the type system (in a typed language) or via wrapper objects (in JS/Python). When both sides of an interface speak in Quantity { value: 12.5, units: "m/s" } instead of bare 12.5, the conversion happens at the boundary and mismatches surface as type errors.

The pandemic bicycle problem

A smaller-scale example. In 2020, multiple US fitness apps started displaying riders’ speeds wildly above what was actually achieved. Investigations traced it to a unit-conversion bug: the apps were storing speed internally in m/s but displaying mph by multiplying by 2.237 (correct factor) — but for distance traveled, they were dividing by 2.237 (which is wrong, you’d divide by 1609 to get miles from meters, then divide by hours).

The category of bug: confusing “units of distance per units of time” with “compound conversion.” It’s easy when you have a single conversion factor sitting in a constants file to use it in the wrong direction or for the wrong derived unit.

The fix in such code is the same as for the Mars Orbiter: stop treating numbers as bare floats, attach units explicitly. Or at the very least, write tests where the assertions look like “speed.toUnit(MPH) === expectedMPH” rather than “speed * 2.237 === expectedMPH.”

What our converter does

The unit converter implements the math above:

When affine math also matters elsewhere

Temperature is the most-encountered affine-units case, but not the only one:

When in doubt, ask: “is the zero of unit A the same physical quantity as the zero of unit B?” If yes, scale. If no, you need both scale and offset.

Try it

The unit converter handles all of this — including the Fahrenheit/Celsius/Kelvin/Rankine matrix — correctly. The full math is in sites/unit/src/lib/units.ts if you want to verify.