beginner⏱35 minutesLesson 10 of 10
Beginner Project — CLI Temperature Converter
Build a complete command-line temperature converter with input validation, error handling, and unit conversions
Beginner Project: CLI Temperature Converter
Build a command-line temperature converter that handles Fahrenheit ↔ Celsius conversions with robust input validation.
Project Setup
bash
cargo new temp_converter
cd temp_converterStep 1: Understanding the Formulas
| Conversion | Formula |
|---|---|
| °F → °C | (f - 32) * 5/9 |
| °C → °F | (c * 9/5) + 32 |
rust
fn fahrenheit_to_celsius(f: f64) -> f64 {
(f - 32.0) * 5.0 / 9.0
}
fn celsius_to_fahrenheit(c: f64) -> f64 {
(c * 9.0 / 5.0) + 32.0
}
#[test]
fn test_conversions() {
assert!((fahrenheit_to_celsius(32.0) - 0.0).abs() < f64::EPSILON);
assert!((celsius_to_fahrenheit(0.0) - 32.0).abs() < f64::EPSILON);
assert!((fahrenheit_to_celsius(212.0) - 100.0).abs() < f64::EPSILON);
assert!((celsius_to_fahrenheit(100.0) - 212.0).abs() < f64::EPSILON);
}Note
We use (a - b).abs() < f64::EPSILON for float comparison because == on floats is unreliable due to precision.
Step 2: Parsing User Input
rust
use std::io::{self, Write};
fn read_line() -> String {
let mut input = String::new();
io::stdin().read_line(&mut input).expect("Failed to read line");
input.trim().to_string()
}
fn parse_temperature(input: &str) -> Result<f64, String> {
input.trim().parse::<f64>()
.map_err(|_| format!("'{input}' is not a valid number"))
}
fn parse_unit(input: &str) -> Result<&str, String> {
match input.trim().to_lowercase().as_str() {
"c" | "celsius" => Ok("C"),
"f" | "fahrenheit" => Ok("F"),
_ => Err(format!("'{input}' is not a valid unit. Use 'C' or 'F'")),
}
}Step 3: Building the Application
rust
fn run_converter() -> Result<(), String> {
println!("=== Temperature Converter ===");
print!("Enter temperature: ");
io::stdout().flush().map_err(|e| e.to_string())?;
let temp_input = read_line();
let temperature = parse_temperature(&temp_input)?;
print!("Convert to (C/F): ");
io::stdout().flush().map_err(|e| e.to_string())?;
let unit_input = read_line();
let target_unit = parse_unit(&unit_input)?;
match target_unit {
"C" => {
let result = fahrenheit_to_celsius(temperature);
println!("{temperature}°F = {result:.1}°C");
}
"F" => {
let result = celsius_to_fahrenheit(temperature);
println!("{temperature}°C = {result:.1}°F");
}
_ => unreachable!(), // parse_unit already validated
}
Ok(())
}
fn main() {
if let Err(e) = run_converter() {
eprintln!("Error: {e}");
std::process::exit(1);
}
}Success
Using Result as the return type lets us use the ? operator for clean error propagation. The main function handles the outer Err case.
Step 4: Adding Input Validation
rust
fn validate_temperature(value: f64, unit: &str) -> Result<(), String> {
match unit {
"C" if value < -273.15 => {
Err(format!("{value}°C is below absolute zero (-273.15°C)"))
}
"F" if value < -459.67 => {
Err(format!("{value}°F is below absolute zero (-459.67°F)"))
}
_ => Ok(()),
}
}
fn run_validated_converter() -> Result<(), String> {
println!("=== Temperature Converter ===");
print!("Enter temperature: ");
io::stdout().flush().map_err(|e| e.to_string())?;
let temperature = parse_temperature(&read_line())?;
print!("Enter unit (C/F): ");
io::stdout().flush().map_err(|e| e.to_string())?;
let unit = parse_unit(&read_line())?;
validate_temperature(temperature, unit)?;
match unit {
"C" => {
let result = celsius_to_fahrenheit(temperature);
println!("{temperature:.1}°C = {result:.1}°F");
}
"F" => {
let result = fahrenheit_to_celsius(temperature);
println!("{temperature:.1}°F = {result:.1}°C");
}
_ => unreachable!(),
}
Ok(())
}Step 5: Full Application
Here's the complete program:
rust
use std::io::{self, Write};
fn fahrenheit_to_celsius(f: f64) -> f64 {
(f - 32.0) * 5.0 / 9.0
}
fn celsius_to_fahrenheit(c: f64) -> f64 {
(c * 9.0 / 5.0) + 32.0
}
fn read_line() -> String {
let mut input = String::new();
io::stdin().read_line(&mut input).expect("Failed to read line");
input.trim().to_string()
}
fn parse_temperature(input: &str) -> Result<f64, String> {
input.parse::<f64>()
.map_err(|_| format!("'{input}' is not a valid number"))
}
fn parse_unit(input: &str) -> Result<&str, String> {
match input.trim().to_lowercase().as_str() {
"c" | "celsius" => Ok("C"),
"f" | "fahrenheit" => Ok("F"),
_ => Err(format!("'{input}' is not a valid unit. Use 'C' or 'F'")),
}
}
fn validate_temperature(value: f64, unit: &str) -> Result<(), String> {
match unit {
"C" if value < -273.15 => {
Err(format!("{value}°C is below absolute zero"))
}
"F" if value < -459.67 => {
Err(format!("{value}°F is below absolute zero"))
}
_ => Ok(()),
}
}
fn run_converter() -> Result<(), String> {
println!("=== Temperature Converter ===");
print!("Enter temperature: ");
io::stdout().flush().map_err(|e| e.to_string())?;
let temperature = parse_temperature(&read_line())?;
print!("Enter unit (C/F): ");
io::stdout().flush().map_err(|e| e.to_string())?;
let unit = parse_unit(&read_line())?;
validate_temperature(temperature, unit)?;
match (unit, temperature) {
("C", c) => {
let f = celsius_to_fahrenheit(c);
println!("{c:.1}°C = {f:.1}°F");
}
("F", f) => {
let c = fahrenheit_to_celsius(f);
println!("{f:.1}°F = {c:.1}°C");
}
_ => unreachable!(),
}
Ok(())
}
fn main() {
if let Err(e) = run_converter() {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fahrenheit_to_celsius() {
assert!((fahrenheit_to_celsius(32.0) - 0.0).abs() < f64::EPSILON);
assert!((fahrenheit_to_celsius(212.0) - 100.0).abs() < f64::EPSILON);
assert!((fahrenheit_to_celsius(-40.0) - (-40.0)).abs() < f64::EPSILON);
}
#[test]
fn test_celsius_to_fahrenheit() {
assert!((celsius_to_fahrenheit(0.0) - 32.0).abs() < f64::EPSILON);
assert!((celsius_to_fahrenheit(100.0) - 212.0).abs() < f64::EPSILON);
assert!((celsius_to_fahrenheit(-40.0) - (-40.0)).abs() < f64::EPSILON);
}
#[test]
fn test_parse_temperature() {
assert_eq!(parse_temperature("25.5"), Ok(25.5));
assert!(parse_temperature("abc").is_err());
assert!(parse_temperature("").is_err());
}
#[test]
fn test_parse_unit() {
assert_eq!(parse_unit("C"), Ok("C"));
assert_eq!(parse_unit("fahrenheit"), Ok("F"));
assert!(parse_unit("K").is_err());
}
#[test]
fn test_validate_temperature() {
assert!(validate_temperature(25.0, "C").is_ok());
assert!(validate_temperature(-300.0, "C").is_err());
assert!(validate_temperature(-500.0, "F").is_err());
assert!(validate_temperature(100.0, "F").is_ok());
}
}Step 6: Running and Testing
bash
# Run in release mode
cargo run --release
# Run tests
cargo test
# Example output:
# === Temperature Converter ===
# Enter temperature: 100
# Enter unit (C/F): F
# 100.0°C = 212.0°F
# Error case:
# === Temperature Converter ===
# Enter temperature: abc
# Enter unit (C/F): F
# Error: 'abc' is not a valid numberWarning
Always check for f64::NAN and f64::INFINITY in real applications. Users can enter these values and cause unexpected behavior.
Extensions
Try these enhancements:
- Loop until quit — Keep converting until user types "q"
- Kelvin support — Add Kelvin unit
- Prettier output — Use colored output with
coloredcrate - Batch conversion — Read multiple values from a file (
--file input.txt) - Precision flag —
--precision 4controls decimal places - CLI arguments — Use
clapcrate:temp_converter 100 C to F
rust
// Extension: loop until quit
fn main() {
loop {
println!("\n=== Temperature Converter (q to quit) ===");
print!("Enter temperature: ");
io::stdout().flush().unwrap();
let input = read_line();
if input.to_lowercase() == "q" {
break;
}
match parse_temperature(&input) {
Ok(temp) => {
print!("Enter unit (C/F): ");
io::stdout().flush().unwrap();
let unit_input = read_line();
match parse_unit(&unit_input) {
Ok(unit) => {
if let Err(e) = validate_temperature(temp, unit) {
eprintln!("Error: {e}");
continue;
}
match unit {
"C" => println!("{temp:.1}°C = {:.1}°F", celsius_to_fahrenheit(temp)),
"F" => println!("{temp:.1}°F = {:.1}°C", fahrenheit_to_celsius(temp)),
_ => unreachable!(),
}
}
Err(e) => eprintln!("Error: {e}"),
}
}
Err(e) => eprintln!("Error: {e}"),
}
}
}Practice Questions
- What does
io::stdout().flush()do and why is it needed? - Why do we use
f64instead off32for temperature? - What does the
?operator do in the converter code? - How does
parse::<f64>()handle invalid input? - Why can't we use
==to compare float results? - What's the purpose of the
validate_temperaturefunction? - How would you add Kelvin conversion to the program?
- Why is the
run_converterfunction separated frommain? - What does
std::process::exit(1)do? - How would you write a test for the case-sensitive unit parsing?
Progress100%