1. The Problem
Build a unit converter that handles length (km/miles/feet/inches), weight (kg/lbs/g/oz), temperature (C/F/K), and speed (km/h, mph, m/s). Convert any unit to all others in its category.
Why this project? A nested object as a lookup table replaces hundreds of if/else cases. Temperature requires custom functions rather than simple multipliers — a good lesson in when tables don't fit.
2. How to Think About This
For linear units: store a conversion factor to a base unit (e.g. all lengths convert via metres). To convert A→B: convert A to base, then base to B. This means you only need N factors instead of N² pairs. Temperature is non-linear so it gets its own explicit functions.
3. The Build
// Linear unit lookup — factor = "how many base units is 1 of this?"
export const UNITS = {
length: { base: "m",
km: 1000, m: 1, cm: 0.01, mm: 0.001,
mi: 1609.344, yd: 0.9144, ft: 0.3048, "in": 0.0254 },
weight: { base: "kg",
t: 1000, kg: 1, g: 0.001, mg: 0.000001,
lb: 0.453592, oz: 0.0283495 },
speed: { base: "m/s",
"m/s": 1, "km/h": 1/3.6, mph: 0.44704, knot: 0.514444 },
};
// Temperature: non-linear — explicit conversion through Celsius as hub
export function convertTemp(value, from, to) {
const toC = { C: v=>v, F: v=>(v-32)*5/9, K: v=>v-273.15 };
const fromC = { C: v=>v, F: v=>v*9/5+32, K: v=>v+273.15 };
if (!toC[from] || !fromC[to]) throw new Error(`Unknown temp unit: ${from} or ${to}`);
return +fromC[to](toC[from](value)).toFixed(4);
}
export function convertLinear(value, from, to) {
// Find the category this unit belongs to
const category = Object.values(UNITS).find(cat => from in cat && to in cat);
if (!category) throw new Error(`Cannot convert ${from} to ${to} — different or unknown categories`);
const inBase = value * category[from];
return +(inBase / category[to]).toFixed(6);
}
export function convertAll(value, from) {
// Try linear first
for (const [catName, cat] of Object.entries(UNITS)) {
if (from in cat) {
const results = {};
for (const unit of Object.keys(cat)) {
if (unit === "base") continue;
results[unit] = convertLinear(value, from, unit);
}
return { category: catName, results };
}
}
// Try temperature
const tempUnits = ["C","F","K"];
if (tempUnits.includes(from)) {
const results = {};
tempUnits.forEach(to => { results[to] = convertTemp(value, from, to); });
return { category: "temperature", results };
}
throw new Error(`Unknown unit: ${from}`);
}
const [,, valueStr, from] = process.argv;
if (!valueStr || !from) {
console.error("Usage: node convert.mjs <value> <unit>");
console.error("Examples: node convert.mjs 100 km | node convert.mjs 37 C | node convert.mjs 1 kg");
process.exit(1);
}
const value = parseFloat(valueStr);
if (isNaN(value)) { console.error("Value must be a number"); process.exit(1); }
const { category, results } = convertAll(value, from);
console.log(`
${value} ${from} [${category}]`);
Object.entries(results).forEach(([unit, val]) => {
if (unit !== from) console.log(` = ${val} ${unit}`);
});
4. Test & Prove
import { convertLinear, convertTemp, convertAll } from "./convert.mjs";
let p=0,f=0;
function test(d,fn){try{fn();console.log(" ✓",d);p++;}catch(e){console.log(" ✗",d,"—",e.message);f++;}}
const approx = (a,b) => Math.abs(a-b) < 0.001;
test("1 km = 1000 m", () => { if(!approx(convertLinear(1,"km","m"),1000)) throw new Error(); });
test("1 mile ≈ 1.609 km", () => { if(!approx(convertLinear(1,"mi","km"),1.609)) throw new Error(convertLinear(1,"mi","km")); });
test("100 C = 212 F", () => { if(!approx(convertTemp(100,"C","F"),212)) throw new Error(); });
test("0 C = 273.15 K", () => { if(!approx(convertTemp(0,"C","K"),273.15)) throw new Error(); });
test("37 F = 2.778 C", () => { if(!approx(convertTemp(37,"F","C"),2.778)) throw new Error(convertTemp(37,"F","C")); });
test("1 kg = 1000 g", () => { if(!approx(convertLinear(1,"kg","g"),1000)) throw new Error(); });
test("cross-category throws",() => { try{convertLinear(1,"km","kg");throw new Error("no");}catch(e){if(e.message==="no")throw e;} });
test("unknown unit throws", () => { try{convertAll(1,"xyz");throw new Error("no");}catch(e){if(e.message==="no")throw e;} });
console.log(`
${p} passed, ${f} failed`);5. The Interface
node convert.mjs 100 km # 100 km [length] # = 100000 m = 62.137 mi = 328084 ft ... node convert.mjs 37 C # 37 C [temperature] # = 98.6 F = 310.15 K node convert.mjs 1 kg # 1 kg [weight] # = 1000 g = 2.20462 lb ...
6. Run It
node convert.mjs 100 km
node convert.mjs 37 C
node convert.test.mjs🎯 Try this next
- Add area (m², ft², acres, hectares) and volume (L, ml, gallons, fl oz)
- Add a
--toflag:node convert.mjs 100 km --to mi - Build a browser version with dropdowns for from/to unit
- Add currency conversion by fetching a live exchange rate API
What you practised
Lookup table pattern (N factors instead of N² conversions), hub-unit conversion, separating linear from non-linear conversion logic, Object.entries()/Object.keys() for iterating lookup objects.
Reference: Objects · Numbers & Math · Functions