1 The Problem
We want a small program that converts a temperature from Celsius to Fahrenheit, or the other way around. The user picks which direction, types a number, and the program does the maths and shows the answer. Simple — but it teaches the core shape of every calculator-style tool: ask, calculate, show.
2 How to Think About It
Two formulas, one menu. The whole program is: ask which direction, ask the value, apply the right formula, print the result.
3 The Build — explained part by part
Here is the complete converter, with the actual conversion logic pulled into small, independently testable functions.
use std::io::{self, Write};
fn c_to_f(c: f64) -> f64 {
c * 9.0 / 5.0 + 32.0
}
fn f_to_c(f: f64) -> f64 {
(f - 32.0) * 5.0 / 9.0
}
/// Runs one convert-and-print cycle for a menu choice ("1" = C->F,
/// "2" = F->C) and a value, returning the formatted result line — or an
/// error message for a bad choice, kept separate from I/O so it is testable.
fn convert(choice: &str, value: f64) -> Result<String, String> {
match choice.trim() {
"1" => Ok(format!("{value:.1}\u{b0}C = {:.1}\u{b0}F", c_to_f(value))),
"2" => Ok(format!("{value:.1}\u{b0}F = {:.1}\u{b0}C", f_to_c(value))),
other => Err(format!("Unknown choice '{other}' — pick 1 or 2.")),
}
}
fn main() {
let stdin = io::stdin();
println!("1) Celsius to Fahrenheit");
println!("2) Fahrenheit to Celsius");
print!("Choose 1 or 2: ");
io::stdout().flush().ok();
let mut choice = String::new();
stdin.read_line(&mut choice).ok();
print!("Enter the temperature: ");
io::stdout().flush().ok();
let mut value_line = String::new();
stdin.read_line(&mut value_line).ok();
let value: f64 = match value_line.trim().parse() {
Ok(v) => v,
Err(_) => {
println!("That's not a number.");
return;
}
};
match convert(&choice, value) {
Ok(line) => println!("{line}"),
Err(msg) => println!("{msg}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn converts_freezing_point_c_to_f() {
assert_eq!(convert("1", 0.0).unwrap(), "0.0\u{b0}C = 32.0\u{b0}F");
}
#[test]
fn converts_boiling_point_f_to_c() {
assert_eq!(convert("2", 212.0).unwrap(), "212.0\u{b0}F = 100.0\u{b0}C");
}
#[test]
fn round_trips_a_value() {
let f = c_to_f(37.0);
let back = f_to_c(f);
assert!((back - 37.0).abs() < 0.0001);
}
#[test]
fn rejects_an_unknown_choice() {
assert!(convert("9", 100.0).is_err());
}
}
rustup) is installed.f64 (a 64-bit floating-point number), Rust’s default float type. Keeping them separate from I/O is what makes them testable on line 1 below without any stdin involved.match choice.trim() { "1" => ..., "2" => ..., other => ... } — Rust’s
match is exhaustive: the compiler forces a catch-all arm (other) so every possible string is handled, unlike an if/else if chain you could forget to close.format!("{value:.1}°C = {:.1}°F", ...) — the
:.1 inside the format specifier rounds to one decimal place, and {value} is Rust’s captured-identifier shorthand: it reads the local variable value directly instead of needing {}, value.Result<String, String> —
convert returns either a formatted string or an error message, so the compiler forces the caller to handle both instead of trusting the menu choice blindly.
.trim() on the choice string before matching on it — read_line keeps the trailing newline, so "1\n" never matches the pattern "1"..trim() a line read from stdin before comparing or parsing it.9 / 5 on integers truncates to 1, silently breaking every conversion.9.0 / 5.0, as the code above does.4 Test & Prove Each Part
We test the two formulas directly, and the menu-choice logic, without ever needing to type anything.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn converts_freezing_point_c_to_f() {
assert_eq!(convert("1", 0.0).unwrap(), "0.0\u{b0}C = 32.0\u{b0}F");
}
#[test]
fn converts_boiling_point_f_to_c() {
assert_eq!(convert("2", 212.0).unwrap(), "212.0\u{b0}F = 100.0\u{b0}C");
}
#[test]
fn round_trips_a_value() {
let f = c_to_f(37.0);
let back = f_to_c(f);
assert!((back - 37.0).abs() < 0.0001);
}
#[test]
fn rejects_an_unknown_choice() {
assert!(convert("9", 100.0).is_err());
}
}
Run with cargo test. The round-trip test compares with a small tolerance (< 0.0001) rather than exact equality, because floating-point arithmetic can leave a tiny rounding error — comparing floats for exact equality is a classic bug in any language.
5 The Interface
What it expects
Choice: "1" or "2"
Value: a number like 100What it returns
100.0°C = 212.0°F6 Run It & Automate It
Save the code as src/main.rs inside a Cargo project's src/ folder and run it with cargo run — Cargo compiles and executes in one step while you are experimenting, then cargo build --release gives you an optimized binary once you are done.
cargo runEnter 1 or 2, then the temperature value.
A CI tool like Jenkins runs cargo test automatically whenever the code changes — every line below has a plain explanation.
1) Celsius to Fahrenheit
2) Fahrenheit to Celsius
Choose 1 or 2: 1
Enter the temperature: 100
100.0°C = 212.0°Ff64. Make sure it is a plain number like 100 or 36.6, with no stray letters or units.\r\n rather than just \n. .trim() already strips both, so if you see this, check you did not remove that call.// Jenkinsfile — runs the tests automatically every time the code changes.
pipeline {
agent any // run on any available machine
stages {
stage('Get the code') {
steps { checkout scm } // download the latest code
}
stage('Set up Rust') {
steps {
sh 'rustc --version' // confirm Rust is installed
sh 'cargo build' // compile, downloading any crates
}
}
stage('Run the tests') {
steps {
sh 'cargo clippy -- -D warnings' // catch obvious mistakes before running
sh 'cargo test' // run every test, show each result
}
}
}
post {
success { echo 'All tests passed.' }
failure { echo 'A test failed — look above.' }
}
}
- Add Kelvin. A three-way
matchinstead of two arms. (Teaches: extending an exhaustive match.) - Accept a command-line argument. Read from
std::env::args()instead of a menu. (Teaches: non-interactive CLI design.) - Round-trip validation. Warn if converting forward then back does not return the original value within a tolerance. (Teaches: floating-point comparison.)
match, the format! macro’s precision specifiers and captured-identifier shorthand, and why floats should never be compared for exact equality. Related: Control Flow, Data Types.