⚠️ Disclaimer: This project is for educational purposes only. BMI is a simplified indicator and should not be used as a substitute for medical evaluation.
What You’ll Learn
By the end of this lesson, you’ll be able to:
- Use variables and input handling to gather user data.
- Apply conditional logic (
if/elseandmatchwith guards) for real-world classification problems. - Handle invalid inputs and runtime errors using
Resultand custom error types. - Write unit tests to verify logic correctness.
- Structure your Rust program in a clean, maintainable way.
Step 1 – Understanding BMI
The Body Mass Index (BMI) is a simple numerical measure of body weight relative to height:
BMI = weight_kg / (height_m ^ 2)
Common categories:
- Underweight: BMI < 18.5
- Normal weight: 18.5 ≤ BMI < 25.0
- Overweight: 25.0 ≤ BMI < 30.0
- Obesity: BMI ≥ 30.0
This simple classification provides a great foundation for learning conditional branching in Rust.
Step 2 – Quick CLI Implementation
Let’s start with a short version that reads from the terminal and prints the BMI result. This helps you focus on core Rust syntax and flow control.
use std::io::{self, Write};
fn main() {
println!("=== BMI Calculator (Quick Version) ===");
let weight = prompt_float("Enter weight in kg (e.g., 72.4): ");
let height = prompt_float("Enter height in meters (e.g., 1.78): ");
if weight <= 0.0 || height <= 0.0 {
eprintln!("Invalid values: both must be > 0.");
std::process::exit(1);
}
let bmi = weight / (height * height);
let category = classify_bmi(bmi);
println!("\nBMI: {:.1}", bmi);
println!("Category: {}", category);
}
fn prompt_float(msg: &str) -> f32 {
print!("{}", msg);
let _ = io::stdout().flush();
let mut buf = String::new();
io::stdin().read_line(&mut buf).expect("Failed to read input");
let buf = buf.trim().replace(',', ".");
buf.parse::<f32>().unwrap_or_else(|_| {
eprintln!("Invalid input. Please use a number like 70.5");
std::process::exit(1);
})
}
fn classify_bmi(bmi: f32) -> &'static str {
if bmi < 18.5 {
"Underweight"
} else if bmi < 25.0 {
"Normal weight"
} else if bmi < 30.0 {
"Overweight"
} else {
"Obesity"
}
}
Example:
=== BMI Calculator (Quick Version) ===
Enter weight in kg (e.g., 72.4): 72.4
Enter height in meters (e.g., 1.78): 1.78
BMI: 22.8
Category: Normal weight
Step 3 – Clean & Robust Implementation
Now, let’s improve the program structure by separating I/O from logic, introducing Result for safe error handling, and using enum with pattern matching.
use std::error::Error;
use std::fmt;
use std::io::{self, Write};
#[derive(Debug)]
enum BmiError {
NonPositive(&'static str),
Parse(String),
}
impl fmt::Display for BmiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BmiError::NonPositive(field) => write!(f, "{} must be > 0", field),
BmiError::Parse(s) => write!(f, "Could not parse '{}'", s),
}
}
}
impl Error for BmiError {}
#[derive(Debug, Clone, Copy, PartialEq)]
enum BmiCategory { Underweight, Normal, Overweight, Obesity }
impl fmt::Display for BmiCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use BmiCategory::*;
write!(f, "{}", match self {
Underweight => "Underweight",
Normal => "Normal weight",
Overweight => "Overweight",
Obesity => "Obesity",
})
}
}
fn parse_float(prompt: &str, name: &'static str) -> Result<f32, BmiError> {
print!("{}", prompt);
let _ = io::stdout().flush();
let mut s = String::new();
io::stdin().read_line(&mut s).map_err(|_| BmiError::Parse("<stdin>".into()))?;
let s = s.trim().replace(',', ".");
let val: f32 = s.parse().map_err(|_| BmiError::Parse(s.clone()))?;
if val <= 0.0 { return Err(BmiError::NonPositive(name)); }
Ok(val)
}
fn bmi(weight: f32, height: f32) -> f32 {
weight / (height * height)
}
fn category_from_bmi(bmi: f32) -> BmiCategory {
use BmiCategory::*;
match bmi {
x if x < 18.5 => Underweight,
x if x < 25.0 => Normal,
x if x < 30.0 => Overweight,
_ => Obesity,
}
}
fn main() {
println!("=== BMI Calculator (Robust Version) ===");
if let Err(e) = run() {
eprintln!("Error: {}", e);
std::process::exit(1);
}
}
fn run() -> Result<(), BmiError> {
let weight = parse_float("Weight in kg: ", "weight")?;
let height = parse_float("Height in meters: ", "height")?;
let val = bmi(weight, height);
let cat = category_from_bmi(val);
println!("\nBMI: {:.1}", val);
println!("Category: {}", cat);
Ok(())
}
Step 4 – Unit Tests
Writing tests ensures your code works correctly and remains stable.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_category() {
assert_eq!(category_from_bmi(17.9), BmiCategory::Underweight);
assert_eq!(category_from_bmi(18.5), BmiCategory::Normal);
assert_eq!(category_from_bmi(24.9), BmiCategory::Normal);
assert_eq!(category_from_bmi(25.0), BmiCategory::Overweight);
assert_eq!(category_from_bmi(29.9), BmiCategory::Overweight);
assert_eq!(category_from_bmi(30.0), BmiCategory::Obesity);
}
#[test]
fn test_bmi_calculation() {
let b = bmi(72.0, 1.80);
assert!((b - 22.22).abs() < 0.01);
}
}
Run tests:
cargo test
Step 5 – Extensions
- Height auto-conversion (cm → m):
fn height_to_m(v: f32) -> f32 {
if v > 3.0 { v / 100.0 } else { v }
}
- Imperial units: Add a mode for pounds/inches (
BMI = 703 * weight_lb / (height_in^2)). - Batch mode: Read multiple
weight,heightlines from stdin. - Colorized output using ANSI escape codes.
- Advice printing:
fn advice(cat: BmiCategory) -> &'static str {
match cat {
BmiCategory::Underweight => "Consult a nutrition professional.",
BmiCategory::Normal => "Keep up your healthy habits!",
BmiCategory::Overweight => "Exercise and balanced diet can help.",
BmiCategory::Obesity => "Consider discussing options with your doctor.",
}
}
Debug Tip
Use dbg!() for quick runtime inspection:
dbg!(weight, height, bmi);
Learning Checklist
- Handle user input safely.
- Apply
if/elseandmatchexpressions. - Write and run unit tests.
- Separate logic from I/O for cleaner design.
⚠️ Disclaimer (again): The BMI metric is used here purely as a programming exercise for learning conditional logic in Rust. It should not be interpreted as a health diagnostic tool.
