Back to the basics - conversions

So far we've used the read_string() function to capture some input from the user. This has been quite useful so far, but sometimes you need a data type other than text. For example, if you want to work with numbers. In this chapter, we'll look at how to convert the user's text-based input into a number. We'll also cover the basics of error handling.

We'll start with an earlier example that I've modified slightly:

fn main() {
    println!("How old are you?");
    let age = read_string();
    println!("You are {age} years old.");
}

fn read_string() -> String {
    let mut input = String::new();
    std::io::stdin()
        .read_line(&mut input)
        .expect("can not read user input");
    input.trim().to_string()
}

Note that I've removed the intermediate cleaned_input and now return the result of input.trim().to_string() directly from the read_string() function. Remember that we can do this by omitting the ; at the end of the last statement in a function.

Capturing age as a number

The goal is to capture the age of the user and convert it into one of the numeric data types: i8, i16, i32, u8, u16 or u32.

Exercise

Consider the range of numbers these data types can hold and choose what would be the best fit.

Now let's modify the above code and add a new read_number function.

use std::str::FromStr;

fn main() {
    println!("How old are you?");
    let age = read_number();
    println!("You are {age} years old.");
}

fn read_string() -> String {
    let mut input = String::new();
    std::io::stdin()
        .read_line(&mut input)
        .expect("can not read user input");
    input.trim().to_string()
}

fn read_number() -> u8 {
    let input = read_string();
    u8::from_str(&input).expect("input is not a number")
}

As you can see, I've chosen the u8 type. u8 holds numbers from 0 to 255, which should be enough for any living person at this point. The alternative would have been i8. i8 goes up to 127. According to wikipedia, the oldest human was 122, which does not leave us much room. i8 also holds negative numbers, which is not needed in our case. Since u8 and i8 both cover the same amount of memory, 8 bits or 1 byte, u8 makes the most sense. Anything larger, like u16 or u32 would be a waste of memory.

There are a few other things that have been added to the code. The RustRover editor highlighted an error when I added the u8::from_str related method. Apparently it was missing some code that it needed for this operation and suggested that I add the line use std::str::FromStr;. So I did. We'll discuss importing external functions later, for now just make sure you include the use statement at the top of your main.rs file.

The associated u8::from_str function will try to convert the input to a u8 number type. This can fail in many ways: the user could have typed something that isn't a number, or they could have typed a number that isn't in the u8' range, like 300', or they could have just hit the enter key and not given us any input.

The expect() method will deal with these errors. Well... handling the error might be a bit of an exaggeration, it will make sure that the program does not continue with an invalid return value. In fact, our program will crash with an output like this:

thread 'main' panicked at 'input is not a number: ParseIntError { kind: Overflow }', src/main.rs:20:26
stack backtrace:
   0: rust_begin_unwind
             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/std/src/panicking.rs:483
   1: core::panicking::panic_fmt
             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/panicking.rs:85
   2: core::option::expect_none_failed
             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/option.rs:1234
   3: core::result::Result<T,E>::expect
             at /Users/mibes/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/src/rust/library/core/src/result.rs:933
   4: hello_world::read_number
             at ./src/main.rs:20
   5: hello_world::main
             at ./src/main.rs:5
   6: core::ops::function::FnOnce::call_once
             at /Users/mibes/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:227
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

The crucial piece of information is: input is not a number: ParseIntError { kind: Overflow }.

Surely there must be a better way to handle such input errors. Of course there is. Lots of ways, actually. Let's start with the simplest case. We'll provide a default age when someone makes an input error.

use std::str::FromStr;

fn main() {
    println!("How old are you?");
    let age = read_number();
    println!("You are {age} years old.");
}

fn read_string() -> String {
    let mut input = String::new();
    std::io::stdin()
        .read_line(&mut input)
        .expect("can not read user input");
    input.trim().to_string()
}

fn read_number() -> u8 {
    let input = read_string();
    u8::from_str(&input).unwrap_or(0)
}

As you can see, I replaced the expect() with unwrap_or(0). In Rust, unwrap() attempts to retrieve a valid value from the preceding function or method. It is similar to expect(), but does not provide a meaningful message if it fails. You should rarely use unwrap or expect because it often indicates that you are not handling error conditions properly.

**However, unwrap's brother unwrap_or() can be quite useful. It tries to take a valid value, just like unwrap, but if it fails, it returns the default value instead, in our case 0. So unlike unwrap or expect, unwrap_or will not crash your program.

Run the above example and see what happens if you enter an invalid u8 number.

Considering that I have never seen a baby type on a keyboard, returning 0 seems like an acceptable default. We can even check for the 0 in our main function.

use std::str::FromStr;

fn main() {
    println!("How old are you?");
    let age = read_number();

    if age > 0 {
        println!("You are {age} years old.");
    } else {
        println!("You've entered an invalid age");
    }
}

fn read_string() -> String {
    let mut input = String::new();
    std::io::stdin()
        .read_line(&mut input)
        .expect("can not read user input");
    input.trim().to_string()
}

fn read_number() -> u8 {
    let input = read_string();
    u8::from_str(&input).unwrap_or(0)
}

Obviously, returning a default value cannot be used in all cases, and you may even question whether it is legitimate not to accept 0 as a valid input.

In the next chapter, we'll examine the use of optional return values to handle our case in a nicer way.