Tier 1 — Beginner

BMI Calculator

Calculate Body Mass Index with metric and imperial support — pure functions, unit conversion, category lookup.

🐍 Python 🟨 JavaScript

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

bmi.mjs
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

  1. Add Waist-to-Height ratio as an additional health metric
  2. Add a history file — log each calculation with a timestamp
  3. Build a browser version with sliders for height and weight
  4. 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

Try It Yourself