1. The Problem
Build a BMI calculator that accepts height and weight in either metric (cm/kg) or imperial (ft+in/lbs), computes BMI, and returns the category (Underweight, Normal, Overweight, Obese). Display a simple bar showing where the result falls.
Why this project? Unit conversion, range-based category lookup with a lookup table, pure functions, and input validation — all in a real-world calculation context.
2. How to Think About This
Convert everything to metric (kg, metres) first — the hub-unit pattern. BMI = weight(kg) / height(m)². Category lookup via an array of { max, label } thresholds, sorted ascending — find the first entry whose max exceeds the BMI.
3. The Build
export const CATEGORIES = [
{ max: 18.5, label: "Underweight", advice: "Consider speaking to a nutritionist." },
{ max: 25.0, label: "Normal weight", advice: "Great — maintain your habits." },
{ max: 30.0, label: "Overweight", advice: "A bit above ideal. Light activity helps." },
{ max: Infinity, label: "Obese", advice: "Please consult a healthcare professional." },
];
export function calcBMI(weightKg, heightM) {
if (weightKg <= 0 || heightM <= 0) throw new RangeError("Weight and height must be positive");
if (heightM > 3) throw new RangeError("Height in metres must be < 3 (did you pass cm?)");
return +(weightKg / heightM ** 2).toFixed(1);
}
export function getCategory(bmi) {
return CATEGORIES.find(c => bmi < c.max);
}
export function imperialToMetric(lbs, feet, inches = 0) {
const kg = lbs * 0.453592;
const m = (feet * 12 + inches) * 0.0254;
return { kg: +kg.toFixed(2), m: +m.toFixed(3) };
}
function bmiBar(bmi) {
const pct = Math.min(Math.max((bmi - 10) / 30, 0), 1);
const pos = Math.round(pct * 30);
const bar = "░".repeat(pos) + "▓" + "░".repeat(30 - pos);
return `[${bar}] ←10 18.5 25 30 40→`;
}
const [,, ...args] = process.argv;
const metric = !args.includes("--imperial");
let weightKg, heightM;
if (metric) {
[weightKg, heightM] = [parseFloat(args[0]), parseFloat(args[1]) / 100];
} else {
const { kg, m } = imperialToMetric(parseFloat(args[0]), parseFloat(args[1]), parseFloat(args[2] ?? 0));
weightKg = kg; heightM = m;
}
if (isNaN(weightKg) || isNaN(heightM)) {
console.error("Usage (metric): node bmi.mjs <weight_kg> <height_cm>");
console.error("Usage (imperial): node bmi.mjs <weight_lbs> <feet> [inches] --imperial");
process.exit(1);
}
const bmi = calcBMI(weightKg, heightM);
const cat = getCategory(bmi);
console.log(`
BMI: ${bmi} — ${cat.label}`);
console.log(bmiBar(bmi));
console.log(`
${cat.advice}`);
4. Test & Prove
import { calcBMI, getCategory, imperialToMetric } from "./bmi.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.2;
test("BMI 70kg/1.75m ≈ 22.9", () => { if(!approx(calcBMI(70,1.75),22.9)) throw new Error(calcBMI(70,1.75)); });
test("BMI 50kg/1.70m = Underweight", () => { if(getCategory(calcBMI(50,1.70)).label!=="Underweight") throw new Error(); });
test("BMI 70kg/1.75m = Normal", () => { if(getCategory(22.9).label!=="Normal weight") throw new Error(); });
test("BMI 90kg/1.75m = Overweight", () => { if(getCategory(29.4).label!=="Overweight") throw new Error(); });
test("BMI 100kg/1.60m = Obese", () => { if(getCategory(39.1).label!=="Obese") throw new Error(); });
test("imperial 154lbs 5ft10", () => { const {kg,m}=imperialToMetric(154,5,10); if(!approx(calcBMI(kg,m),22.1)) throw new Error(); });
test("negative weight throws", () => { try{calcBMI(-70,1.75);throw new Error("no");}catch(e){if(e.message==="no")throw e;} });
test("cm not m throws", () => { try{calcBMI(70,175);throw new Error("no");}catch(e){if(e.message==="no")throw e;} });
console.log(`
${p} passed, ${f} failed`);5. The Interface
node bmi.mjs 70 175 # 70kg, 175cm node bmi.mjs 154 5 10 --imperial # 154lbs, 5ft10in BMI: 22.9 — Normal weight [░░░░░░░░░░░░░░░▓░░░░░░░░░░░░░░░] ←10 18.5 25 30 40→ Great — maintain your habits.
6. Run It
node bmi.mjs 70 175
node bmi.test.mjs🎯 Try this next
- Add Waist-to-Height ratio as an additional health metric
- Add a history file — log each calculation with a timestamp
- Build a browser version with sliders for height and weight
- Add child BMI support — BMI-for-age percentile (WHO tables)
What you practised
Hub-unit conversion pattern, Array.find() for range lookups, pure functions for testability, power operator (**), ASCII bar chart visualisation, and the --imperial flag pattern using args.includes().
Reference: Numbers & Math · Arrays · Functions