0
\$\begingroup\$

I'm trying to learn Rust and for this reason I'm reading the Rust Book. It contains the following exercise:

Convert temperatures between Fahrenheit and Celsius.

Here's my take. Note I'm only using material from chapters 1-3 of this book (since I've only read so far yet).

use std::io;

fn main() {
    loop {
        println!("Please enter temperature:");
        let mut temperature = String::new();
        io::stdin()
            .read_line(&mut temperature)
            .expect("Failed to read line");
        let temperature: f64 = match temperature.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };
        
        loop {
            println!("[C] or [F]?");
            let mut unit = String::new();
            io::stdin()
                .read_line(&mut unit)
                .expect("Failed to read line");
            match unit.trim() {
                "F" => to_celsius(temperature),
                "C" => to_fahrenheit(temperature),
                _ => continue,
            };
            
            break;
        }
        break;
    }
}

fn to_celsius(fahrenheit: f64) {
    let celsius = (fahrenheit - 32.0) * (5.0/9.0);
    println!("{fahrenheit}°F = {celsius}°C");
}

fn to_fahrenheit(celsius: f64) {
    let fahrenheit = celsius * 9.0 / 5.0 + 32.0;
    println!("{celsius}°C = {fahrenheit}°F");
}

Comments?

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

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:

  1. One function to parse the input.
  2. One function to perform the logic.
  3. 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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.