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?
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:
We created a vec
options
which has all vegetables' names.We used
Select
to prompt user to select one vegetable.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.
First we read the file using
let mut reader = csv::Reader::from_path(path)?;
. Now?
symbol let's the compiler knows that use theOk()
value by default but if there is anErr()
then return thatErr()
, as we have used return type for error asBox<dyn Error>
The dyn keyword is used when declaring a trait object (here
Error
, which means any type that hasError
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 theError
trait.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.
After this, we declare an empty
records
vec.We
deserialize
thereader
and push it in therecords
vec.While deserializing we call
unwrap_or()
method and it has argumentProductInfo::new()
. This way if compiler didn't get anOk()
value, it will useProductInfo::new()
. We created this function at the start (i.e. the special tool hehe).We then return the
records
vec.Ok(records)
lets the compiler know the Result type isOk()
.
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.
We called the
generate_options
function.Created a
loop
which will take user's order untill webreak
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);
}
}
We first declared a
u64
mutable variable calledtotal
and gave it a value 0.Then inside the loop, after we prompt the vegetable choice, we ask for the unit.
CustomType::<u64>::new("Units : ")
is just creating a new prompt, and the value which it will accept isu64
.The
with_formatter
method will print this after you enter your choice, so here it will print something as "4 units".The
with_error_message
andwith_help_message
are self-explanatory.Then we are calculating the current total using the units and product rate (This is how
structs
are helpful as you can just callproduct.rate
to get the rate ).This will give a type error as
product.rate
isu8
, So go back to theutils.rs
file and change the type ofrate
tou64
fromu8
.
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:
If we print the bill so we return the total amount.
If we quit.
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,
Find out bugs.
Make the billing system more pretty.
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 ๐ฆ.