4
\$\begingroup\$

Introduction

I am recently learning Rust by reading The Rust Programming Language (a.k.a. the book). I have finished the first three chapters so far, and this is my attempt for the first exercise listed at the end of Chapter 3, which has a simple description:

Convert temperatures between Fahrenheit and Celsius.

My program repetitively prompts the user to input for a command (one of \${^{\circ}\text{F}} \to {^{\circ}\text{C}}\$, \${^{\circ}\text{C}} \to {^{\circ}\text{F}}\$, and quit), and asks for temperature values to be converted as floating-point values. If the user input cannot be properly interpreted, the program sends an error message and resumes executing if possible.

I have run rustfmt and clippy on my program. rustfmt taught me something new (removing , after {} in a match expression), and clippy did not report any problems. I have also tested the program with different inputs, and so far it seems to work properly.

Before moving on, I would like to know whether I am in the right direction. Feel free to point out edge cases I neglected, failures to conform to received Rust guidelines, or other things that I did wrong, so I can correct these mistakes as soon as possible and learn productively in the future.

Code

src/main.rs

use std::io;

fn main() {
    println!("This is a program that converts temperatures between Fahrenheit and Celsius.");

    loop {
        println!("\nEnter one of the following commands:");
        println!(" - 0: convert from Fahrenheit to Celsius");
        println!(" - 1: convert from Celsius to Fahrenheit");
        println!(" - 2: quit");

        let mut command = String::new();
        match io::stdin().read_line(&mut command) {
            Ok(_) => {}
            Err(_) => {
                println!("Failed to read command.");
                continue;
            }
        }

        let command: u32 = match command.trim().parse() {
            Ok(num) => num,
            Err(_) => {
                println!("Invalid command.");
                continue;
            }
        };
        match command {
            0 => fahrenheit_to_celsius(),
            1 => celsius_to_fahrenheit(),
            2 => break,
            _ => println!("Invalid command."),
        }
    }
}

fn fahrenheit_to_celsius() {
    println!("Enter the temperature in Fahrenheit.");

    let mut temperature = String::new();
    match io::stdin().read_line(&mut temperature) {
        Ok(_) => {}
        Err(_) => {
            println!("Failed to read temperature.");
            return;
        }
    }

    let temperature: f64 = match temperature.trim().parse() {
        Ok(t) => t,
        Err(_) => {
            println!("Invalid temperature.");
            return;
        }
    };
    let converted = (temperature - 32.0) / 1.8;
    println!("{} Fahrenheit = {} Celsius", temperature, converted);
}

fn celsius_to_fahrenheit() {
    println!("Enter the temperature in Celsius.");

    let mut temperature = String::new();
    match io::stdin().read_line(&mut temperature) {
        Ok(_) => {}
        Err(_) => {
            println!("Failed to read temperature.");
            return;
        }
    }

    let temperature: f64 = match temperature.trim().parse() {
        Ok(t) => t,
        Err(_) => {
            println!("Invalid temperature.");
            return;
        }
    };
    let converted = temperature * 1.8 + 32.0;
    println!("{} Celsius = {} Fahrenheit", temperature, converted);
}

Cargo.toml

[package]
name = "temperature"
version = "0.1.0"
authors = ["L. F."]
edition = "2018"

[dependencies]

Example session

Here's an example cargo run session:

This is a program that converts temperatures between Fahrenheit and Celsius.

Enter one of the following commands:
 - 0: convert from Fahrenheit to Celsius
 - 1: convert from Celsius to Fahrenheit
 - 2: quit
0
Enter the temperature in Fahrenheit.
200
200 Fahrenheit = 93.33333333333333 Celsius

Enter one of the following commands:
 - 0: convert from Fahrenheit to Celsius
 - 1: convert from Celsius to Fahrenheit
 - 2: quit
0
Enter the temperature in Fahrenheit.
123.4
123.4 Fahrenheit = 50.77777777777778 Celsius

Enter one of the following commands:
 - 0: convert from Fahrenheit to Celsius
 - 1: convert from Celsius to Fahrenheit
 - 2: quit
0
Enter the temperature in Fahrenheit.
cat
Invalid temperature.

Enter one of the following commands:
 - 0: convert from Fahrenheit to Celsius
 - 1: convert from Celsius to Fahrenheit
 - 2: quit
1
Enter the temperature in Celsius.
1.98e5
198000 Celsius = 356432 Fahrenheit

Enter one of the following commands:
 - 0: convert from Fahrenheit to Celsius
 - 1: convert from Celsius to Fahrenheit
 - 2: quit
1
Enter the temperature in Celsius.
unicorn
Invalid temperature.

Enter one of the following commands:
 - 0: convert from Fahrenheit to Celsius
 - 1: convert from Celsius to Fahrenheit
 - 2: quit
3
Invalid command.

Enter one of the following commands:
 - 0: convert from Fahrenheit to Celsius
 - 1: convert from Celsius to Fahrenheit
 - 2: quit
dog
Invalid command.

Enter one of the following commands:
 - 0: convert from Fahrenheit to Celsius
 - 1: convert from Celsius to Fahrenheit
 - 2: quit
2
\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

Unnecessary match

When reading from stdin, you check if there's an error with match:

    match io::stdin().read_line(&mut temperature) {
        Ok(_) => {}
        Err(_) => {
            println!("Failed to read temperature.");
            return;
        }
    }

But you don't care about the value of the error, so you can use a simple if:

    if io::stdin().read_line(&mut temperature).is_err() {
        println!("Failed to read temperature.");
        return;
    }

Code duplication

You have a lot of duplicated code between the celsius_to_fahrenheit and fahrenheit_to_celsius functions for reading a String input and converting it to a f64. Let's combine them into one function that takes command as an argument:

fn convert_temperature(command: u32) {
    if command == 0 {
        println!("Enter the temperature in Fahrenheit.");
    }
    else {
        println!("Enter the temperature in Celsius.");
    }

    let mut temperature = String::new();
    if io::stdin().read_line(&mut temperature).is_err() {
        println!("Failed to read temperature.");
        return;
    }

    let temperature: f64 = match temperature.trim().parse() {
        Ok(t) => t,
        Err(_) => {
            println!("Invalid temperature.");
            return;
        }
    };
    
    if command == 0 {
        let converted = (temperature - 32.0) / 1.8;
        println!("{} Fahrenheit = {} Celsius", temperature, converted);
    }
    else {
        let converted = temperature * 1.8 + 32.0;
        println!("{} Celsius = {} Fahrenheit", temperature, converted);
    }
}

Then the match in main would be:

        match command {
            0 | 1 => convert_temperature(command),
            2 => break,
            _ => println!("Invalid command."),
        }

eprintln! for errors

For your error messages, use eprintln! instead of println! to print to stderr instead of stdout.

if let instead of match

This may be personal preference, but I think that an if let is easier to read than a match here:

      let command: u32 = match command.trim().parse() {
            Ok(num) => num,
            Err(_) => {
                eprintln!("Invalid command.");
                continue;
            }
        };

would become

        let command: u32 = if let Ok(num) = command.trim().parse() {
            num
        } else {
            eprintln!("Invalid command.");
            continue;
        };

Same for the parsing of the temperature.

Inline variables in formatted strings

This makes it easier to read:

println!("{} Fahrenheit = {} Celsius", temperature, converted);

can be just:

println!("{temperature} Fahrenheit = {converted} Celsius");

Putting it together so far

use std::io;

fn main() {
    println!("This is a program that converts temperatures between Fahrenheit and Celsius.");

    loop {
        println!("\nEnter one of the following commands:");
        println!(" - 0: convert from Fahrenheit to Celsius");
        println!(" - 1: convert from Celsius to Fahrenheit");
        println!(" - 2: quit");

        let mut command = String::new();
        if io::stdin().read_line(&mut command).is_err() {
            eprintln!("Failed to read command.");
            continue;
        }

        let command: u32 = if let Ok(num) = command.trim().parse() {
            num
        } else {
            eprintln!("Invalid command.");
            continue;
        };

        match command {
            0 | 1 => convert_temperature(command),
            2 => break,
            _ => eprintln!("Invalid command."),
        }
    }
}

fn convert_temperature(command: u32) {
    if command == 0 {
        println!("Enter the temperature in Fahrenheit.");
    } else {
        println!("Enter the temperature in Celsius.");
    }

    let mut temperature = String::new();
    if io::stdin().read_line(&mut temperature).is_err() {
        eprintln!("Failed to read temperature.");
        return;
    }

    let temperature: f64 = if let Ok(num) = temperature.trim().parse() {
        num
    } else {
        eprintln!("Invalid temperature.");
        return;
    };

    if command == 0 {
        let converted = (temperature - 32.0) / 1.8;
        println!("{temperature} Fahrenheit = {converted} Celsius");
    } else {
        let converted = temperature * 1.8 + 32.0;
        println!("{temperature} Celsius = {converted} Fahrenheit");
    }
}

Reusability

Right now, this code is coupled tightly with the input and output system. However, with some changes, it will become more modular and flexible so that other code can reuse its logic. First, let's add an enum for the two temperatures:

enum Temperature {
    Celsius(f64),
    Fahrenheit(f64),
}

And a method for converting between the two variants:

impl Temperature {
    fn convert(&self) -> Self {
        match self {
            Self::Celsius(t) => Self::Fahrenheit(t * 1.8 + 32.0),
            Self::Fahrenheit(t) => Self::Celsius((t - 32.0) / 1.8),
        }
    }
}

Further, let's add an implementation of fmt::Display to allow printing a temperature:

use std::fmt;

impl fmt::Display for Temperature {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{}",
            match self {
                Self::Celsius(t) => format!("{t} Celsius"),
                Self::Fahrenheit(t) => format!("{t} Fahrenheit"),
            }
        )
    }
}

Finally, this can be used in convert_temperature by replacing the last if with:

    let original = if command == 0 {
        Temperature::Fahrenheit(temperature)
    } else {
        Temperature::Celsius(temperature)
    };
    
    let converted = original.convert();
    println!("{original} = {converted}");

Final code:

use std::{fmt, io};

enum Temperature {
    Celsius(f64),
    Fahrenheit(f64),
}

impl Temperature {
    fn convert(&self) -> Self {
        match self {
            Self::Celsius(t) => Self::Fahrenheit(t * 1.8 + 32.0),
            Self::Fahrenheit(t) => Self::Celsius((t - 32.0) / 1.8),
        }
    }
}

impl fmt::Display for Temperature {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{}",
            match self {
                Self::Celsius(t) => format!("{t} Celsius"),
                Self::Fahrenheit(t) => format!("{t} Fahrenheit"),
            }
        )
    }
}

fn main() {
    println!("This is a program that converts temperatures between Fahrenheit and Celsius.");

    loop {
        println!("\nEnter one of the following commands:");
        println!(" - 0: convert from Fahrenheit to Celsius");
        println!(" - 1: convert from Celsius to Fahrenheit");
        println!(" - 2: quit");

        let mut command = String::new();
        if io::stdin().read_line(&mut command).is_err() {
            eprintln!("Failed to read command.");
            continue;
        }

        let command: u32 = if let Ok(num) = command.trim().parse() {
            num
        } else {
            eprintln!("Invalid command.");
            continue;
        };

        match command {
            0 | 1 => convert_temperature(command),
            2 => break,
            _ => eprintln!("Invalid command."),
        }
    }
}

fn convert_temperature(command: u32) {
    if command == 0 {
        println!("Enter the temperature in Fahrenheit.");
    } else {
        println!("Enter the temperature in Celsius.");
    }

    let mut temperature = String::new();
    if io::stdin().read_line(&mut temperature).is_err() {
        eprintln!("Failed to read temperature.");
        return;
    }

    let temperature: f64 = if let Ok(num) = temperature.trim().parse() {
        num
    } else {
        eprintln!("Invalid temperature.");
        return;
    };

    let original = if command == 0 {
        Temperature::Fahrenheit(temperature)
    } else {
        Temperature::Celsius(temperature)
    };

    let converted = original.convert();
    println!("{original} = {converted}");
}
\$\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.