Introduction to Rust Programming πŸ¦€

Rust Basics πŸ¦€

Rust Basics Series #8: Write the Milestone Rust Program

In the final chapter of the Rust Basics Series, recall the concepts you learned and write a somewhat complex Rust program.

So long, we have covered a handful of fundamental topics about programming in Rust. Some of these topics are variables, mutability, constants, data types, functions, if-else statements and loops.

In the final chapter of the Rust Basics series, let us now write a program in Rust that uses these topics so their real-world use can be better understood. Let's work on a relatively simple program to order fruits from a fruit mart.

The basic structure of our program

Let us first start by greeting the user and informing them about how to interact with the program.

fn main() {
    println!("Welcome to the fruit mart!");
    println!("Please select a fruit to buy.\n");
    
    println!("\nAvailable fruits to buy: Apple, Banana, Orange, Mango, Grapes");
    println!("Once you are done purchasing, type in 'quit' or 'q'.\n");
}

Getting user input

The above code is very simple. At the moment, you do not know what to do next because you do not know what the user wants to do next.

So let's add code that accepts the user input and stores it somewhere to parse it later, and take the appropriate action based on the user input.

use std::io;

fn main() {
    println!("Welcome to the fruit mart!");
    println!("Plase select a fruit to buy.\n");
    
    println!("Available fruits to buy: Apple, Banana, Orange, Mango, Grapes");
    println!("Once you are done purchasing, type in 'quit' or 'q'.\n");
    
    // get user input
    let mut user_input = String::new();
    io::stdin()
        .read_line(&mut user_input)
        .expect("Unable to read user input.");
}

There are three new elements that I need to tell you about. So let's take a shallow dive into each of these new elements.

1. Understanding the 'use' keyword

On the first line of this program, you might have noticed the use (haha!) of a new keyword called use. The use keyword in Rust is similar to the #include directive in C/C++ and the import keyword in Python. Using the use keyword, we "import" the io (input output) module from the Rust standard library std.

You might be wondering why importing the io module was necessary when you could use the println macro to output something to STDOUT. Rust's standard library has a module called prelude that gets automatically included. The prelude module contains all the commonly used functions that a Rust programmer might need to use, like the println macro. (You can read more about std::prelude module here.)

The io module from the Rust standard library std is necessary to accept user input. Hence, a use statement was added to the 1st line of this program.

2. Understanding the String type in Rust

On line 11, I create a new mutable variable called user_input that, as its name suggests, will be used to store the user input down the road. But on the same line, you might have noticed something new (haha, again!).

Instead of declaring an empty string using double quotes with nothing between them (""), I used the String::new() function to create a new, empty string.

The difference between using "" and String::new() is something that you will learn later in the Rust series. For now, know that, with the use of the String::new() function, you can create a String that is mutable and lives on the heap.

If I had created a string with "", I would get something called a "String slice". The String slice's contents are on the heap too, but the string itself is immutable. So, even if the variable itself is mutable, the actual data stored as a string is immutable and needs to be overwritten instead of modification.

3. Accepting the user input

On line 12, I call the stdin() function that is part of std::io. If I had not included the std::io module in the beginning of this program, this line would be std::io::stdin() instead of io::stdin().

The stdin() function returns an input handle of the terminal. The read_line() function grabs onto that input handle and, as its name suggests, reads a line of input. This function takes in a reference to a mutable string. So, I pass in the user_input variable by preceding it with &mut, making it a mutable reference.

⚠️
The read_line() function has a quirk. This function stops reading the input after the user presses the Enter/Return key. Therefore, this function also records that newline character (\n) and a trailing newline is stored in the mutable string variable that you passed in.

So please, either account for this trailing newline when dealing with it or remove it.

A primer on error handling in Rust

Finally, there is an expect() function at the end of this chain. Let's divert a bit to understand why this function is called.

The read_line() function returns an Enum called Result. I will get into Enums in Rust later on but know that Enums are very powerful in Rust. This Result Enum returns a value that informs the programmer if an error occurred when the user input was being read.

The expect() function takes this Result Enum and checks if the result was okay or not. If no error occurs, nothing happens. But if an error did occur, the message that I passed in ("Unable to read user input.") will be printed to STDERR and the program will exit.

πŸ“‹
All the new concepts that I have briefly touched on will be covered in a new Rust series later.

Now that you hopefully understand these newer concepts, let's add more code to increase the functionality.

Validating user input

I surely accepted the user's input but I have not validated it. In the current context, validation means that the user inputs some "command" that we expect to handle. At the moment, the commands are of two "categories".

The first category of the command that the user can input is the name of fruit that the user wishes to buy. The second command conveys that the user wants to quit the program.

So our task now is to make sure that the input from the user does not diverge from the acceptable commands.

use std::io;

fn main() {
    println!("Welcome to the fruit mart!");
    println!("Plase select a fruit to buy.\n");
    
    println!("Available fruits to buy: Apple, Banana, Orange, Mango, Grapes");
    println!("Once you are done purchasing, type in 'quit' or 'q'.\n");
    
    // get user input
    let mut user_input = String::new();
    io::stdin()
        .read_line(&mut user_input)
        .expect("Unable to read user input.");
        
    // validate user input
    let valid_inputs = ["apple", "banana", "orange", "mango", "grapes", "quit", "q"];
    user_input = user_input.trim().to_lowercase();
    let mut input_error = true;
    for input in valid_inputs {
        if input == user_input {
            input_error = false;
            break;
        }
    }
}

To make validation easier, I created an array of string slices called valid_inputs (on line 17). This array contains the names of all the fruits that are available for purchase, along with the string slices q and quit to let the user convey if they wish to quit.

The user may not know how we expect the input to be. The user may type "Apple" or "apple" or "APPLE" to tell that they intend to purchase Apples. It is our job to handle this correctly.

On line 18, I trim the trailing newline from the user_input string by calling the trim() function on it. And to handle the previous problem, I convert all the characters to lowercase with the to_lowercase() function so that "Apple", "apple" and "APPLE" all end up as "apple".

Now on line 19, I create a mutable boolean variable called input_error with the initial value of true. Later on line 20, I create a for loop that iterates over all the elements (string slices) of the valid_inputs array and stores the iterated pattern inside the input variable.

Inside the loop, I check if the user input is equal to one of the valid strings, and if it is, I set the value of input_error boolean to false and break out of the for loop.

Dealing with invalid input

Now is time to deal with an invalid input. This can be done by moving some of the code inside an infinite loop and continuing said infinite loop if the user gives an invalid input.

use std::io;

fn main() {
    println!("Welcome to the fruit mart!");
    println!("Plase select a fruit to buy.\n");
    
    let valid_inputs = ["apple", "banana", "orange", "mango", "grapes", "quit", "q"];
    
    'mart: loop {
        let mut user_input = String::new();

        println!("\nAvailable fruits to buy: Apple, Banana, Orange, Mango, Grapes");
        println!("Once you are done purchasing, type in 'quit' or 'q'.\n");

        // get user input
        io::stdin()
            .read_line(&mut user_input)
            .expect("Unable to read user input.");
        user_input = user_input.trim().to_lowercase();

        // validate user input
        let mut input_error = true;
        for input in valid_inputs {
            if input == user_input {
                input_error = false;
                break;
            }
        }

        // handle invalid input
        if input_error {
            println!("ERROR: please enter a valid input");
            continue 'mart;
        }
    }
}

Here, I moved some of the code inside the loop and re-structured the code a bit to better deal with this introduction of the loop. Inside the loop, on line 31, I continue the mart loop if the user entered an invalid string.

Reacting to user's input

Now that everything else is handled, time to actually write code about purchasing fruits from the fruit market and quit when the user wishes.

Since you also know which fruit the user chose, let's ask how much they intend to purchase and inform them about the format of entering the quantity.

use std::io;

fn main() {
    println!("Welcome to the fruit mart!");
    println!("Plase select a fruit to buy.\n");
    
    let valid_inputs = ["apple", "banana", "orange", "mango", "grapes", "quit", "q"];
    
    'mart: loop {
        let mut user_input = String::new();
        let mut quantity = String::new();

        println!("\nAvailable fruits to buy: Apple, Banana, Orange, Mango, Grapes");
        println!("Once you are done purchasing, type in 'quit' or 'q'.\n");

        // get user input
        io::stdin()
            .read_line(&mut user_input)
            .expect("Unable to read user input.");
        user_input = user_input.trim().to_lowercase();

        // validate user input
        let mut input_error = true;
        for input in valid_inputs {
            if input == user_input {
                input_error = false;
                break;
            }
        }

        // handle invalid input
        if input_error {
            println!("ERROR: please enter a valid input");
            continue 'mart;
        }
        
        // quit if user wants to
        if user_input == "q" || user_input == "quit" {
            break 'mart;
        }

        // get quantity
        println!(
            "\nYou choose to buy \"{}\". Please enter the quantity in Kilograms.
(Quantity of 1Kg 500g should be entered as '1.5'.)",
            user_input
        );
        io::stdin()
            .read_line(&mut quantity)
            .expect("Unable to read user input.");
    }
}

On line 11, I declare another mutable variable with an empty string and on line 48, I accept input from the user, but this time the quantity of said fruit that the user intends to buy.

Parsing the quantity

I just added code that takes in quantity in a known format, but that data is stored as a string. I need to extract the float out of that. Lucky for us, it can be done with the parse() method.

Just like the read_line() method, the parse() method returns the Result Enum. The reason why the parse() method returns the Result Enum can be easily understood with what we are trying to achieve.

I am accepting a string from users and trying to convert it to a float. A float has two possible values in it. One is the floating point itself and the second is a decimal number.

While a String can have alphabets, a float does not. So, if the user entered something other than the [optional] floating point and the decimal number(s), the parse() function will return an error.

Hence, this error needs to be handled too. We will use the expect() function to deal with this.

use std::io;

fn main() {
    println!("Welcome to the fruit mart!");
    println!("Plase select a fruit to buy.\n");
    
    let valid_inputs = ["apple", "banana", "orange", "mango", "grapes", "quit", "q"];
    
    'mart: loop {
        let mut user_input = String::new();
        let mut quantity = String::new();

        println!("\nAvailable fruits to buy: Apple, Banana, Orange, Mango, Grapes");
        println!("Once you are done purchasing, type in 'quit' or 'q'.\n");

        // get user input
        io::stdin()
            .read_line(&mut user_input)
            .expect("Unable to read user input.");
        user_input = user_input.trim().to_lowercase();

        // validate user input
        let mut input_error = true;
        for input in valid_inputs {
            if input == user_input {
                input_error = false;
                break;
            }
        }

        // handle invalid input
        if input_error {
            println!("ERROR: please enter a valid input");
            continue 'mart;
        }
        
        // quit if user wants to
        if user_input == "q" || user_input == "quit" {
            break 'mart;
        }

        // get quantity
        println!(
            "\nYou choose to buy \"{}\". Please enter the quantity in Kilograms.
(Quantity of 1Kg 500g should be entered as '1.5'.)",
            user_input
        );
        io::stdin()
            .read_line(&mut quantity)
            .expect("Unable to read user input.");

        let quantity: f64 = quantity
            .trim()
            .parse()
            .expect("Please enter a valid quantity.");

    }
}

As you can see, I store the parsed float in the variable quantity by making use of variable shadowing. To inform the parse() function that the intention is to parse the string into f64, I manually annotate the type of the variable quantity as f64.

Now, the parse() function will parse the String and return a f64 or an error, that the expect() function will deal with.

Calculating the price + final touch ups

Now that we know which fruit the user wants to buy and its quantity, it is time to perform those calculations now and let the user know about the results/total.

For the sake of realness, I will have two prices for each fruit. The first price is the retail price, which we pay to fruit vendors when we buy in small quantities. The second price for fruit will be the wholesale price, when someone buys fruits in bulk.

The wholesale price will be determined if the order is greater than the minimum order quantity to be considered as a wholesale purchase. This minimum order quantity varies for every fruit. The prices for each fruit will be in Rupees per Kilogram.

With that logic in mind, down below is the program in its final form.

use std::io;

const APPLE_RETAIL_PER_KG: f64 = 60.0;
const APPLE_WHOLESALE_PER_KG: f64 = 45.0;

const BANANA_RETAIL_PER_KG: f64 = 20.0;
const BANANA_WHOLESALE_PER_KG: f64 = 15.0;

const ORANGE_RETAIL_PER_KG: f64 = 100.0;
const ORANGE_WHOLESALE_PER_KG: f64 = 80.0;

const MANGO_RETAIL_PER_KG: f64 = 60.0;
const MANGO_WHOLESALE_PER_KG: f64 = 55.0;

const GRAPES_RETAIL_PER_KG: f64 = 120.0;
const GRAPES_WHOLESALE_PER_KG: f64 = 100.0;

fn main() {
    println!("Welcome to the fruit mart!");
    println!("Please select a fruit to buy.\n");

    let mut total: f64 = 0.0;
    let valid_inputs = ["apple", "banana", "orange", "mango", "grapes", "quit", "q"];

    'mart: loop {
        let mut user_input = String::new();
        let mut quantity = String::new();

        println!("\nAvailable fruits to buy: Apple, Banana, Orange, Mango, Grapes");
        println!("Once you are done purchasing, type in 'quit' or 'q'.\n");

        // get user input
        io::stdin()
            .read_line(&mut user_input)
            .expect("Unable to read user input.");
        user_input = user_input.trim().to_lowercase();

        // validate user input
        let mut input_error = true;
        for input in valid_inputs {
            if input == user_input {
                input_error = false;
                break;
            }
        }

        // handle invalid input
        if input_error {
            println!("ERROR: please enter a valid input");
            continue 'mart;
        }

        // quit if user wants to
        if user_input == "q" || user_input == "quit" {
            break 'mart;
        }

        // get quantity
        println!(
            "\nYou choose to buy \"{}\". Please enter the quantity in Kilograms.
(Quantity of 1Kg 500g should be entered as '1.5'.)",
            user_input
        );
        io::stdin()
            .read_line(&mut quantity)
            .expect("Unable to read user input.");
        let quantity: f64 = quantity
            .trim()
            .parse()
            .expect("Please enter a valid quantity.");

        total += calc_price(quantity, user_input);
    }

    println!("\n\nYour total is {} Rupees.", total);
}

fn calc_price(quantity: f64, fruit: String) -> f64 {
    if fruit == "apple" {
        price_apple(quantity)
    } else if fruit == "banana" {
        price_banana(quantity)
    } else if fruit == "orange" {
        price_orange(quantity)
    } else if fruit == "mango" {
        price_mango(quantity)
    } else {
        price_grapes(quantity)
    }
}

fn price_apple(quantity: f64) -> f64 {
    if quantity > 7.0 {
        quantity * APPLE_WHOLESALE_PER_KG
    } else {
        quantity * APPLE_RETAIL_PER_KG
    }
}

fn price_banana(quantity: f64) -> f64 {
    if quantity > 4.0 {
        quantity * BANANA_WHOLESALE_PER_KG
    } else {
        quantity * BANANA_RETAIL_PER_KG
    }
}

fn price_orange(quantity: f64) -> f64 {
    if quantity > 3.5 {
        quantity * ORANGE_WHOLESALE_PER_KG
    } else {
        quantity * ORANGE_RETAIL_PER_KG
    }
}

fn price_mango(quantity: f64) -> f64 {
    if quantity > 5.0 {
        quantity * MANGO_WHOLESALE_PER_KG
    } else {
        quantity * MANGO_RETAIL_PER_KG
    }
}

fn price_grapes(quantity: f64) -> f64 {
    if quantity > 2.0 {
        quantity * GRAPES_WHOLESALE_PER_KG
    } else {
        quantity * GRAPES_RETAIL_PER_KG
    }
}

Compared to the previous iteration, I made some changes...

The fruit prices may fluctuate, but for the lifecycle of our program, these prices will not fluctuate. So I store the retail and wholesale prices of each fruit in constants. I define these constants outside the main() functions (i.e. globally) because I will not calculate the prices for each fruit inside the main() function. These constants are declared as f64 because they will be multiplied with quantity which is f64. Recall, Rust doesn't have implicit type casting ;)

After storing the fruit name and the quantity that the user wants to purchase, the calc_price() function is called to calculate the price of said fruit in the user provided quantity. This function takes in the fruit name and the quantity as its parameters and returns the price as f64.

Looking inside the calc_price() function, it is what many people call a wrapper function. It is called a wrapper function because it calls other functions to do its dirty laundry.

Since each fruit has a different minimum order quantity to be considered as a wholesale purchase, to ensure that the code can be maintained easily in the future, the actual price calculation for each fruit is split in separate functions for each individual fruit.

So, all that the calc_price() function does is to determine which fruit was chosen and call the respective function for chosen fruit. These fruit-specific functions accept only one argument: quantity. And these fruit-specific functions return the price as f64.

Now, price_*() functions do only one thing. They check if the order quantity is greater than the minimum order quantity to be considered as a wholesale purchase for said fruit. If it is such, quantity is multiplied by the fruit's wholesale price per Kilogram. Otherwise, quantity is multiplied by the fruit's retail price per Kilogram.

Since the line with multiplication does not have a semi-colon at the end, the function returns the resulting product.

If you look closely at the function calls of the fruit-specific functions in the calc_price() function, these function calls do not have a semi-colon at the end. Meaning, the value returned by the price_*() functions will be returned by the calc_price() function to its caller.

And there is only one caller for calc_price() function. This is at the end of the mart loop where the returned value from this function is what is used to increment the value of total.

Finally, when the mart loop ends (when the user inputs q or quit), the value stored inside the variable total gets printed to the screen and the user is informed about the price he/she has to pay.

Conclusion

With this post, I have used all the previously explained topics about the Rust programming language to create a simple program that still somewhat demonstrates a real-world problem.

Now, the code that I wrote can definitely be written in a more idiomatic way that best uses Rust's loved features but I haven't covered them yet!

So stay tuned for follow-up Take Rust to The Next Level series and learn more of the Rust programming language!

The Rust Basics series concludes here. I welcome your feedback.