Separate logic from I/O
This is a general principle for program structure which leads to program that are more easily tested, or refactored.
In this case, the to_celsius
and to_fahrenheit
functions should return a result, rather than printing it. It'll allow you, notably, to test that they result in the correct number by for example doing a round-trip conversion for a few values or matching them against conversions from a book/internet.
Provide feedback to the user/caller
Do not dismiss the error, instead explain to the user what's wrong. You can just print the error to the screen.
Unnest the loops.
There is no reason for the two loops you have to be nested, you could simply have two loops one after another.
(If really you want to use loop, see later)
Split program in 3 phases: input, logic, output.
Split your program in 3 phases:
- One function to parse the input.
- One function to perform the logic.
- One function to print the output.
Simplify your life
As you may have noted, reading from stdin is a bit... lackluster. There's no easy way to have a structured read, such as scanf
in C.
Since chapter 3 is about control-flow, and not reading from stdin, I'd suggest simplifying your life and just using env::args()
.
Similarly, I'd suggest simplifying error reporting and just "panicking" with an explicit error message explaining to the user what they did wrong.
use std::env;
#[derive(Clone, Copy)]
enum Temperature {
Celsius(f64),
Fahrenheit(f64),
}
fn parse_input() -> Temperature {
const HELP: &str = "<program> temperature C|F";
let mut args = env::args();
let degrees: f64 = args
.next()
.expect(HELP)
.parse()
.expect("Temperature must be a floating point number");
let unit = args
.next()
.expect(HELP);
let temperature = match unit {
"C" => Temperature::Celsius(degrees),
"F" => Temperature::Fahrenheit(degrees),
_ => panic!("Unit must be either C or F, not {unit}"),
};
assert!(args.next().is_none(), HELP);
temperature
}
From there, the transformation can be applied, and the result displayed.
fn convert(temperature: Temperature) -> Temperature {
match temperature {
Temperature::Celsius(degrees) =>
Temperature::Fahrenheit(to_fahrenheit(degrees)),
Temperature::Fahrenheit(degrees) =>
Temperature::Celsius(to_celsius(degrees)),
}
}
fn print_output(input: Temperature, output: Temperature) {
fn split(temperature: Temperature) -> (f64, char) {
match input {
Temperature::Celsius(degrees) => (degrees, 'C'),
Temperature::Fahrenheit(degrees) => (degrees, 'F'),
}
}
let (input_degrees, input_unit) = split(input);
let (output_degrees, output_unit) = split(output);
println!("{input_degrees}°{input_unit} = {output_degrees}°{output_unit}");
}
fn main() {
let input = parse_input();
let output = convert(input);
print_output(input, output);
}
I do note that the exercise, as specified, doesn't really use any loop by nature. It does use match
, though, so that's already something. The next exercises will use loops to make up for it.