mirror of
https://github.com/LCTT/TranslateProject.git
synced 2024-12-26 21:30:55 +08:00
523 lines
22 KiB
Markdown
523 lines
22 KiB
Markdown
[#]: subject: "Rust Basics Series #8: Write the Milestone Rust Program"
|
|
[#]: via: "https://itsfoss.com/milestone-rust-program/"
|
|
[#]: author: "Pratham Patel https://itsfoss.com/author/pratham/"
|
|
[#]: collector: "lkxed"
|
|
[#]: translator: "Cubik65536"
|
|
[#]: reviewer: " "
|
|
[#]: publisher: " "
|
|
[#]: url: " "
|
|
|
|
Rust Basics Series #8: Write the Milestone Rust Program
|
|
======
|
|
|
|
So long, we have covered a handful of fundamental topics about programming in Rust. Some of these topics are [variables, mutability, constants][1], [data types][2], [functions][3], [if-else statements][4] and [loops][5].
|
|
|
|
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][6].)
|
|
|
|
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.
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
via: https://itsfoss.com/milestone-rust-program/
|
|
|
|
作者:[Pratham Patel][a]
|
|
选题:[lkxed][b]
|
|
译者:[Cubik65536](https://github.com/Cubik65536)
|
|
校对:[校对者ID](https://github.com/校对者ID)
|
|
|
|
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
|
|
|
[a]: https://itsfoss.com/author/pratham/
|
|
[b]: https://github.com/lkxed/
|
|
[1]: https://itsfoss.com/rust-variables
|
|
[2]: https://itsfoss.com/rust-data-types-01
|
|
[3]: https://itsfoss.com/rust-functions
|
|
[4]: https://itsfoss.com/rust-conditional-statements
|
|
[5]: https://itsfoss.com/rust-loops
|
|
[6]: https://doc.rust-lang.org/std/prelude/index.html?ref=itsfoss.com
|