Next up is my third program written in Rust. All the prompt said was:

Convert temperatures between Fahrenheit and Celsius.

This could be simple functions to go back and forth between Celsius and Fahrenheit. But I wanted to try and challenge myself to do handle some user input and use some of Rust's more advanced features.

Enums

Last week I went over the power match expressions can provide, and this week we're going to go even further with Enums. Enums allow you to create a custom type that "enumerates" all the possible values.

Since we are dealing with temperatures, I thought it would be fitting to capture the two scales we use with an Enum.

enum Temp {
    F(f64),
    C(f64),
}

We use the keyword enum and then define all the possible versions our type can handle. In this case, it is F for Fahrenheit, and C for Celsius. And each one can hold a floating point number for the degrees.

With this, we can now use a match expression to do the actual calculations for conversions:

fn convert_temp(temp: &Temp) -> f64 {
    match temp {
        &Temp::F(degrees) => (degrees - 32.0) / 1.8,
        &Temp::C(degrees) => (degrees * 1.8) + 32.0,
    }
}

This should look like what we've seen before. This function gets passed a reference to a Temp type. The & signifies it's a reference. This function doesn't need "ownership" of the Temp since we are returning a new float based on it, not mutating it

We use pattern-matching to do the correct conversion to the other scale, and return a new float.

Let's print some numbers!

Now to use this conversion! Once again, the match expression is our friend so we can show the temperature in both scales:

fn print_temp(temp: &Temp) {
    match temp {
        &Temp::F(degrees) => println!("{}F = {}C", degrees, convert_temp(temp)),
        &Temp::C(degrees) => println!("{}C = {}F", degrees, convert_temp(temp)),
    }
}

print_temp also takes a reference to a Temp. It uses match to decide which format it needs, and then prints out both the submitted and converted degrees.

Let's test it out with some sample data to make sure it looks correct. Here I've generated a list of temperatures wrapped in the appropriate Temp enum:

fn sample_temps() {
    println!("Sample conversions:");

    let temps = [
        Temp::F(-40.0), // -40
        Temp::F(0.0),   // -18
        Temp::F(32.0),  // 0
        Temp::F(60.0),  // 16
        Temp::F(100.0), // 38
        Temp::F(150.0), // 66
        Temp::F(212.0), // 100
        Temp::C(-40.0), // -40
        Temp::C(0.0),   // 32
        Temp::C(15.0),  // 59
        Temp::C(30.0),  // 86
        Temp::C(60.0),  // 140
        Temp::C(100.0), // 212
        Temp::C(200.0), // 392
    ];

    for temp in temps.iter() {
        print_temp(temp);
    }
}

fn main() {
    println!("Welcome to temperature converter!\n");

    sample_temps();
}

I put in comments what the correct output should be. We then use a for...in loop to iterate over each of these and output the conversion. Finally, we add in some headings and call this in main(). And out comes all the correct conversions!

Welcome to temperature converter!

Sample conversions:
-40F = -40C
0F = -17.77777777777778C
32F = 0C
60F = 15.555555555555555C
100F = 37.77777777777778C
150F = 65.55555555555556C
212F = 100C
-40C = -40F
0C = 32F
15C = 59F
30C = 86F
60C = 140F
100C = 212F
200C = 392F

User Input

This is great, but unless we want users adding new temperatures to that list, it's not a very useful program. So let's allow for user to add in their own input and call it in main after our sample data:

use std::io;

fn get_user_temp() {
    println!("\nType \"quit\" to end the program");

    loop {
        let mut temp_input = String::new();

        println!("\nPlease input a temperature you want to convert (Format: 100F or -40C):");

        io::stdin()
            .read_line(&mut temp_input)
            .expect("Failed to read line");

        let trimmed = temp_input.trim();

        if trimmed == "quit" {
            break;
        }

        let (temp, scale) = trimmed.split_at(trimmed.len() - 1);

        let temp: f64 = match temp.parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        let temp: Temp = match scale {
            "C" => Temp::C(temp),
            "F" => Temp::F(temp),
            _ => continue,
        };

        print_temp(&temp);
    }
}

We start by telling the user how to quit the program. We're going to leave this thing running for as long as they want to keep converting.

We then enter into a loop that will keep running until we call break.

We then create a mutable String variable called temp_input to store the user input.

Next, we explain what format their queries should be in. We support a number immediately followed by either F or C. This is to make our lives easier for parsing later.

We use the standard library's IO function to read a line from the terminal, which saves it into temp_input. We then trim() what the user typed in to remove any extra characters and put in our check for if they wanted to quit. If they did, we break out of the loop and the program ends.

We split the string at the last character to see if the temperature is in C or F. We use (temp, scale) to destructure the result of split_at into variables we can use later.

Next, we try and parse the first part of the string into a float. If they typed a number, we store it into temp. Otherwise we call continue, which starts the loop over and gives the user the instructions so they can try again.

We then match on the last character of their input. If it was a "C", then we convert temp into a Temp::C. If it's an "F" we convert to a Temp::F. If they put anything else in, we also continue and let them try again.

If everything is good at this point, we have a well-formed Temp, and we print out the conversion:

Type "quit" to end the program

Please input a temperature you want to convert (Format: 100F or -40C):
> 100.5F
100.5F = 38.05555555555556C

Conclusion

In the end, your main.rs should look something like this:

use std::io;

enum Temp {
    F(f64),
    C(f64),
}

fn convert_temp(temp: &Temp) -> f64 {
    match temp {
        &Temp::F(degrees) => (degrees - 32.0) / 1.8,
        &Temp::C(degrees) => (degrees * 1.8) + 32.0,
    }
}

fn print_temp(temp: &Temp) {
    match temp {
        &Temp::F(degrees) => println!("{}F = {}C", degrees, convert_temp(temp)),
        &Temp::C(degrees) => println!("{}C = {}F", degrees, convert_temp(temp)),
    }
}

fn sample_temps() {
    println!("Sample conversions:");

    let temps = [
        Temp::F(-40.0), // -40
        Temp::F(0.0),   // -18
        Temp::F(32.0),  // 0
        Temp::F(60.0),  // 16
        Temp::F(100.0), // 38
        Temp::F(150.0), // 66
        Temp::F(212.0), // 100
        Temp::C(-40.0), // -40
        Temp::C(0.0),   // 32
        Temp::C(15.0),  // 59
        Temp::C(30.0),  // 86
        Temp::C(60.0),  // 140
        Temp::C(100.0), // 212
        Temp::C(200.0), // 392
    ];

    for temp in temps.iter() {
        print_temp(temp);
    }
}

fn get_user_temp() {
    println!("\nType \"quit\" to end the program");

    loop {
        let mut temp_input = String::new();

        println!("\nPlease input a temperature you want to convert (Format: 100F or -40C):");

        io::stdin()
            .read_line(&mut temp_input)
            .expect("Failed to read line");

        let trimmed = temp_input.trim();

        if trimmed == "quit" {
            break;
        }

        let (temp, scale) = trimmed.split_at(trimmed.len() - 1);

        let temp: f64 = match temp.parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        let temp: Temp = match scale {
            "C" => Temp::C(temp),
            "F" => Temp::F(temp),
            _ => continue,
        };

        print_temp(&temp);
    }
}

fn main() {
    println!("Welcome to temperature converter!\n");

    sample_temps();
    get_user_temp();
}

For me, this was a good exploration into how Rust handles references, strings, enums, and a lot more. I hope this walk-through gave you some examples of how you can use these Rust features in your programs as well! Some of this definitely took me awhile. But thanks to helpful compiler warnings and the docs, I was able to figure it out!

What's nice is that Rust nudges you into creating programs that don't crash, even when dealing with something as fickle as user-inputted text! It was amazing to see how easy it was to create a program that guarded against incorrect input and allowed users to try again.

I'm excited to start building some more complicated programs and see where Rust guides me as I deal with different types of data!