๐Ÿฆ€ Rust Series 04: Supermarket Billing System - Part 2

๐Ÿฆ€ Rust Series 04: Supermarket Billing System - Part 2

ยท

10 min read

What to expect?

In Part 1 of the Supermarket Billing System, we used inquire crate to create options. We also read and displayed the product list using serde and structs. In this part, we will finally code the logic for taking and placing orders. We will also print the product list properly. I have covered concepts such as impl , Options , Traits and much more.

Display Products properly

In the last blog, we were displaying the products in this way:

Let's make this clean and remove unnecessary elements.

Go to read_from_file function in product.rs file. Change the code for printing the headers.

let headers = reader.headers()?;
println!("{} {}", 
    headers.get(0).unwrap_or("-"), headers.get(1).unwrap_or("-"));

We were directly printing the headers using {:?} pretty print. Now we are printing each component using get(i) and unwrapping it. Because headers type is &StringRecord, so when we call get(i) method on it, it will return an Option . Let's understand with an example, let's say we call get(3) now if there is nothing at index 3, the call will fail right and as Rust doesn't have null we use Option. So if the value exists it will return Some(value) or None. To handle this Option we call unwrap_or() method to let the compiler know that if it is Some(value) then use that value or else use the value which we have added inside the method i.e. unwrap_or(value). So basically if nothing exists it will print "-".

Easy ๐Ÿฆ€

Let's get rid of the ProductInfo{} thing. Right now we are printing the records using pretty print {:?} .

println!("{:?}", record);

If we remove :? and just write {} it will give an error:

"ProductInfo doesn't implement 'std::fmt::Display` trait"

Traits ? ๐Ÿง

"A trait in Rust is a group of methods that are defined for a particular type. Traits are an abstract definition of shared behavior amongst different types. So, in a way, traits are to Rust what interfaces are to Java or abstract classes are to C++."

Basically, We can use traits to define shared behavior in an abstract way.

Example: Debug, Display, Clone etc.

If you remember we had added Debug trait above the struct ProductInfo . derive(Debug) asks the compiler to auto-generate a suitable implementation of the Debug trait, which provides the result of {:?} in something like format!( "Would the real {:?}.

Now that we have to display the ProductInfo instance properly, we can implement the Display trait. Let's break it down one-by-one.

First, take the whole code related to ProductInfo struct in a new file utils.rs . (This is just for making the codebase clean)

Now you will get alot of errors in product.rs file as you have to import ProductInfo .

First bring utils module in scope by adding this in main.rs .

mod utils;

Then in product.rs you can import ProductInfo using this.

use crate::utils::ProductInfo;

Think of the module system as: The crate root file in this case is src/main.rs
crate -
|- product
|- utils -
|- ProductInfo
|- orders

In utils.rs let's add some implementation for the struct ProductInfo
To create an implementation block we use impl keyword followed by the struct name.

impl ProductInfo {
    pub fn new() -> ProductInfo {
        ProductInfo{
            name: String::from(""),
            rate: 0
        }
    }
}

Basically, this block has all the functions associated with this struct.
For example, if you have a struct called Circle and there is a function that is calculate_area you can add that in the implementation block of Circle as they are associated, this makes the code clean.
Here we have added a function called new which will basically return an instance of ProductInfo. This is helpful to create a default instances. We can call this by ProductInfo::new() and it will return a default instance. If you remember we have used something similar like String::new() which returns an empty string. Now we are doing similar but for our custom struct ProductInfo .

Anywho why did we declare this new() function right now?

Mickey's Surprise Template | It's a Surprise Tool That Will Help Us Later |  Know Your Meme

Back to handling the Display trait, in utils.rs add this.

use std::fmt;

impl fmt::Display for ProductInfo {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} {} per unit", self.name, self.rate)
    }

}

We are again defining an implementation block but for Display trait. To understand this cmd+click or ctrl+click to go to the code of Display trait.

If you see it just "formats the value using the given formatter". It only has only function declaration fmt . So it's kinda like an interface that we can implement on our custom struct .

So inside our Display implementation block, we just called the fmt function and inside that wrote:

write!(f, "{} {} per unit", self.name, self.rate)

write!() : Writes formatted data into a buffer.
This way whenever we print an instance of ProductInfo it will be in this format. Let's try it out?

In read_from_file function inside product.rs , Remove pretty print and print the record using only this {} .

Damn ๐Ÿฆ€

Till now our function looks like this and we have successfully printed the product list in a better way:

fn read_from_file(path: &str) -> Result<(), Box<dyn Error>> {
    let mut reader = csv::Reader::from_path(path)?;

    let headers = reader.headers()?;
    println!("{} {}", headers.get(0).unwrap_or("-"), headers.get(1).unwrap_or("-"));

    for result in reader.deserialize() {
        let record: ProductInfo = result?;
        println!("{}", record);
    }

    Ok(())
}

Place Order

We will allow users to select a product, enter units and then print the bill after they select the print bill option.
To start with copy and paste the whole thing in order.rs

use inquire::Select;

pub fn place_order(){

    println!("You have placed an order");

    let options = vec![
        "Onions",
        "Radishes",
        "Turnips",
        "Ginger",
        "Beets",
        "Potatoes",
        "Carrots",
        "Turmeric",
        "Parsnips",
    ];

    let ans = Select::new("", options).prompt();

    match ans {
        Ok(choice) => println!("{}! We have placed your order", choice),
        Err(_) => println!("There was an error, please try again"),
    }
}

Explanation:

  1. We created a vec options which has all vegetables' names.

  2. We used Select to prompt user to select one vegetable.

  3. We then used pattern matching to handle the Result type and print the order.

This is very basic.
What if the product list changes, we will have to update both in the csv and then here in the vec. How about we read the vegetables in vec from the csv itself.
Add this in order.rs

use::std::error::Error;
use crate::utils::ProductInfo;

fn generate_options(path: &str) -> Result<Vec<ProductInfo>, Box<dyn Error>>{
    let mut reader = csv::Reader::from_path(path)?;

    let mut records: Vec<ProductInfo> = vec![];
    for result in reader.deserialize() {
        let record: ProductInfo = result
                                       .unwrap_or(ProductInfo::new());
        records.push(record);
    }

    Ok(records)
}

This function is taking path of the file as argument and returning Result<Vec<ProductInfo>, Box<dyn Error>>. So it will read the file and return us the vector of ProductInfo. If there is an error it will return that.

  1. First we read the file using let mut reader = csv::Reader::from_path(path)?;. Now ? symbol let's the compiler knows that use the Ok() value by default but if there is an Err() then return that Err(), as we have used return type for error as Box<dyn Error>

  2. The dyn keyword is used when declaring a trait object (here Error, which means any type that has Error as their trait comes under this). As inside the function, there can be multiple places where errors can occur and all these errors will have a different type. That's why we allow all errors which implement the Error trait.

  3. As the size of a trait is not known at compile-time; therefore, traits have to be wrapped inside a Box when creating a vector trait object.

  4. After this, we declare an empty records vec.

  5. We deserialize the reader and push it in the records vec.

  6. While deserializing we call unwrap_or() method and it has argument ProductInfo::new() . This way if compiler didn't get an Ok() value, it will use ProductInfo::new() . We created this function at the start (i.e. the special tool hehe).

  7. We then return the records vec. Ok(records) lets the compiler know the Result type is Ok().

Now we declare another function that takes the order:

fn take_order() -> Result<Option<u64>, Box<dyn Error>>{
    let product_options =  generate_options("./data/products.csv")?;

    loop{
        let product = Select::new("Vegetable: ",                              product_options.clone()).prompt()?;        
    }
}

Don't focus on the return type for now.

  1. We called the generate_options function.

  2. Created a loop which will take user's order untill we break this loop.

Along with the vegetable we have to take units too, so for that we will use CustomType from the inquire crate.

use inquire::{Select, CustomType};

fn take_order() -> Result<Option<u64>, Box<dyn Error>>{
    let product_options =  generate_options("./data/products.csv")?;
    let mut total:u64 = 0;

    loop{
        let product = Select::new("Vegetable: ", product_options.clone()).prompt()?;

        let unit = CustomType::<u64>::new("Units: ")
            .with_formatter(&|i| format!("{:} units", i))
            .with_error_message("Please type a valid number")
            .with_help_message("Maximum unit is 10")
            .prompt()?;
        total = total + (product.rate * unit);

    }
}
  1. We first declared a u64 mutable variable called total and gave it a value 0.

  2. Then inside the loop, after we prompt the vegetable choice, we ask for the unit.

  3. CustomType::<u64>::new("Units : ") is just creating a new prompt, and the value which it will accept is u64.

  4. The with_formatter method will print this after you enter your choice, so here it will print something as "4 units".

  5. The with_error_message and with_help_message are self-explanatory.

  6. Then we are calculating the current total using the units and product rate (This is how structs are helpful as you can just call product.rate to get the rate ).

  7. This will give a type error as product.rate is u8, So go back to the utils.rs file and change the type of rate to u64 from u8 .

As we are running this in loop , at some point the user will want to print the bill or maybe quit, to do that we will ask the user the next choice.

Declare this before the loop

let next_options = vec!["+", "bill", "quit"];

Inside the loop after the user enters the unit, call this:

let next = Select::new("", next_options.clone())
    .with_formatter(&|_i| format!(""))
    .prompt()?;

if next.eq(next_options[1]) {
    return Ok(Some(total));        
}
else if next.eq(next_options[2]) {
    return Ok(None);
}

We are using the Select to ask for the next option. So if the user selects "+", nothing will happen and the loop will continue. But if they select "bill" we will return this function with an Okay value of Okay(Some(total)). If they select quit we return Ok(None). Now let's recall the function declaration of take_order

fn take_order() -> Result<Option<u64>, Box<dyn Error>>{
}

We were returning a Result Type, whose Ok was an Option and Err was Box<>.
As there are only three cases where we return from this function:

  1. If we print the bill so we return the total amount.

  2. If we quit.

  3. If an error occurs.

Now both types 1 and 2 are not error based, so they are Ok() values. Thatswhy the type for Ok() is Option because we will either return Some(amount) during print bill or None during quit.

Let's complete this, shall we? Substitute the place_order function with this:

pub fn place_order(){
    let order = take_order();
    match order {
        Ok(amount) => {
            if let Some(a) = amount {
                println!("Your total is: ${}", a);
            };
        },
        Err(e) => {
            eprintln!("{}", e);
        }
    }
}

So we call the take_order() function, run a pattern matching on the return value.
If the Ok() value is Some() then we print the amount, or else we do nothing, which will automatically quit. If you see that because we are returning an error, we don't have to print it at every place, but instead only one time.

Run this ๐Ÿš€

Hot ๐Ÿฆ€
This completes the Supermarket Billing System ๐Ÿš€

You can checkout the code here on GitHub.

What's next?

Play around with the code,

  1. Find out bugs.

  2. Make the billing system more pretty.

  3. Create an actual bill file that the user can open.

Always revise the concepts you have learned either through practice or reading the documentation or watching some videos. Feel free to reach out to me on Twitter, with the awesome projects you build and the bugs you find. Also next up, is something with Discord Bots ๐Ÿฆ€.

ย