Rust Tutorial 4: Let's build a Simple Calculator! (Part 1)
Khair Alanam
Posted on October 28, 2023
Reading time: 20 minutes
Welcome back to the Rust Tutorial Series!
In this tutorial, we will be building a simple calculator! On the way, we will learn some more concepts like functions, generics, tuples, arrays, and more!
This tutorial will be a 2-parter since we will be covering a heck load of concepts with just this simple project. The Rust Tutorial 5 will just be the second part of this tutorial.
So let's get started!
Setup
Run the
cargo new rust_calculator
command to create a new Rust project.Open the
rust_calculator
project in your code editor.
Let's start!
Getting the user inputs
For our calculator, we want two numbers as the user input. So, let's set them up.
Go to the main.rs
file and code this:
rust
use std::io;
fn main() {
let mut x: String = String::new();
let mut y: String = String::new();
println!("Enter the first number: ");
io::stdin().read_line(&mut x).expect("Invalid Input");
println!("Enter the second number: ");
io::stdin().read_line(&mut y).expect("Invalid Input");
}
Funnily enough, I had to refer to the previous tutorial to see how to get the user input because I forgot lol. So yeah 😅.
We also need the input to get the type of operation to be done between the given two numbers.
There are many ways to do this. Do remember that for all the cases, we will handle the errors in case of invalid input (Please refer to the previous tutorial for handling errors).
- We can ask the user for any of the four operations (
+
,-
,*
,/
) directly. - We can ask the user to enter the commands like
ADD
,SUBTRACT
,MULTIPLY
, andDIVIDE
. These commands can be used to trigger the corresponding operation in our code.
Or here's my way:
- We show the set of operations the user can select in the terminal and then the user has to input the selection number. Then we can map that selection number to the operation we need.
So let's do that!
Here's the current code after asking for the operator input:
rust
use std::io;
fn main() {
let mut x: String = String::new();
let mut y: String = String::new();
let mut op: String = String::new();
println!("Enter the first number: ");
io::stdin().read_line(&mut x).expect("Invalid Input");
println!("Enter the second number: ");
io::stdin().read_line(&mut y).expect("Invalid Input");
println!("List of operators:");
println!("(1) Add");
println!("(2) Subtract");
println!("(3) Multiply");
println!("(4) Divide");
println!("Select the number associated with the desired operation: ");
io::stdin().read_line(&mut op).expect("Invalid Input");
}
Let's not forget to parse the number inputs for x
, y
, and op
. Along with parsing the input, I will also handle the errors here using the match
statement (Please refer to the previous tutorial to learn more about the match
statement and handling errors)
rust
use std::io;
fn main() {
let mut x: String = String::new();
let mut y: String = String::new();
let mut op: String = String::new();
println!("Enter the first number: ");
io::stdin().read_line(&mut x).expect("Invalid Input");
let x: i32 = match x.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("Invalid input!");
return;
}
};
println!("Enter the second number: ");
io::stdin().read_line(&mut y).expect("Invalid Input");
let y: i32 = match y.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("Invalid input!");
return;
}
};
println!("List of operators:");
println!("(1) Add");
println!("(2) Subtract");
println!("(3) Multiply");
println!("(4) Divide");
println!("Select the number associated with the desired operation: ");
io::stdin().read_line(&mut op).expect("Invalid Input");
}
Notice that you will get warnings about unused variables. We'll ignore that for now.
Now that we have the operator input, let's use a match
statement to see which number the operator corresponds to, do the operation, and store the result in some variable result
(which we have to declare). If the number is not valid, we return some default case.
rust
use std::io;
fn main() {
let mut x: String = String::new();
let mut y: String = String::new();
let result: i32;
let mut op: String = String::new();
println!("Enter the first number: ");
io::stdin().read_line(&mut x).expect("Invalid Input");
let x: i32 = match x.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("Invalid input!");
return;
}
};
println!("Enter the second number: ");
io::stdin().read_line(&mut y).expect("Invalid Input");
let y: i32 = match y.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("Invalid input!");
return;
}
};
println!("List of operators:");
println!("(1) Add");
println!("(2) Subtract");
println!("(3) Multiply");
println!("(4) Divide");
println!("Select the number associated with the desired operation: ");
io::stdin().read_line(&mut op).expect("Invalid Input");
let op: i32 = match op.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("Invalid input!");
return;
}
};
match op {
1 => result = x + y,
2 => result = x - y,
3 => result = x * y,
4 => result = x / y,
_ => {
println!("Invalid selection");
return;
}
}
println!("The result is: {}", result);
}
Now, let's run the code using the cargo run
command.
Our calculator is working!
Making a better calculator
Just like our previous project, we can make this calculator even better with much better readability and other options.
Firstly, if you notice, we are only doing calculations on integers and not on decimal numbers. We can change this by changing the parsing type of our inputs to f64
.
use std::io;
fn main() {
// existing code
let x: f64 = match x.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("Invalid input!");
return;
}
};
// existing code
let y: f64 = match y.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("Invalid input!");
return;
}
};
// existing code
}
All we did was change the parsing type to f64
instead of i32
for the variables x
and y
.
Now you can run the program and input decimals like 23.4 and 12.3.
Next, notice that the code for asking x
and y
inputs from the user is repetitive. Wouldn't it be nice to keep this code as some sort of a function so that we can reuse it?
Functions
Functions are basically reusable blocks of code. They help in maintaining readability.
In Rust, since we have the main
function, we will have to write our functions outside the main
function.
A Rust function looks like this:
rust
fn add_two(x: i32, y: i32) -> i32 {
return x + y;
}
Some points to note:
- Functions are declared using the
fn
keyword. - When including parameters, each parameter has to be declared with its corresponding type like
x
andy
here in the example function. - Notice that arrow
->
after the parameters? That basically means the return type of the function. Here, the result is the addition of twoi32
numbersx
andy
and we are returning ani32
result. Hence, we define the return type asi32
after the arrow->
.
Let's get back to the project!
Let's make a function input_parser
that takes x
of type String
as argument, assigns the user input, and parses the input to f64
.
Here's the code for our function:
rust
fn input_parser() -> f64 {
let mut x: String = String::new();
io::stdin().read_line(&mut x).expect("Invalid Input");
let x: f64 = match x.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("Invalid input!");
return f64::NAN;
}
};
return x;
}
What I basically did was copy the entire code for parsing the variable x
and then return that x
.
Also notice that for errors in parsing the input, we return something called a NAN
(Not A Number). If you know JavaScript, then this should be familiar. We are using f64::NAN
to return NAN
as f64.
Now let's change the main code by using the input_parser
function:
rust
use std::io;
fn main() {
let result: f64;
let mut op: String = String::new();
println!("Enter the first number: ");
let x: f64 = input_parser();
println!("Enter the second number: ");
let y: f64 = input_parser();
println!("List of operators:");
println!("(1) Add");
println!("(2) Subtract");
println!("(3) Multiply");
println!("(4) Divide");
println!("Select the number associated with the desired operation: ");
io::stdin().read_line(&mut op).expect("Invalid Input");
let op: i32 = match op.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("Invalid input!");
return;
}
};
match op {
1 => result = x + y,
2 => result = x - y,
3 => result = x * y,
4 => result = x / y,
_ => {
println!("Invalid selection");
return;
}
}
println!("The result is: {}", result);
}
fn input_parser() -> f64 {
let mut x: String = String::new();
io::stdin().read_line(&mut x).expect("Invalid Input");
let x: f64 = match x.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("Invalid input!");
return f64::NAN;
}
};
return x;
}
Notice how simple our main code is after we refactored the input and the parsing code into a function. Also, notice that we removed the declarations for x
and y
in the main code since input_parser()
does that for us.
We forgot to handle the error in case we get one of our numbers as f64::NAN
. So let's handle that in our main code:
rust
use std::io;
fn main() {
// existing code
if f64::is_nan(x) {
println!("Invalid input!");
return;
}
// existing code
if f64::is_nan(y) {
println!("Invalid input!");
return;
}
// existing code
}
We are using a built-in function called f64::is_nan()
which accepts an f64
parameter to check whether the given parameter is NAN
or not.
Also, notice that op
is just accepting any number in the given selection (1 to 4) and can be considered as f64
. So let's change the types from i32
to f64
. The reason we are doing this is just so that we can use our input_parser()
function for the op
variable to not repeat ourselves.
rust
println!("List of operators:");
println!("(1) Add");
println!("(2) Subtract");
println!("(3) Multiply");
println!("(4) Divide");
println!("Select the number associated with the desired operation: ");
let op: f64 = input_parser();
if f64::is_nan(op) {
println!("Invalid input!");
return;
}
Just like the x
and y
variables, we use input_parser()
for the op
variable too. Also, you can remove the op
variable declaration in the main code since input_parser()
takes care of that.
After doing this, you will notice that there are errors related to mismatching types for f64
and integer in the match
statement for op
. We can fix this by converting the op
variable's type from f64
to i32
like this:
rust
let op: f64 = input_parser();
if f64::is_nan(op) {
println!("Invalid input!");
return;
}
let op: i32 = op as i32;
Here, by the concept of shadowing, we re-declare op
as an i32
variable. Then we assign the value of the previous op
variable. But since the new op
and old op
have different types, we explicitly convert the old op
variable type to i32
, which is what as i32
basically means. This is called type casting and is pretty essential when it comes to explicit conversions of data types.
Now that everything's done, let's see the final code:
rust
use std::io;
fn main() {
let result: f64;
println!("Enter the first number: ");
let x: f64 = input_parser();
if f64::is_nan(x) {
println!("Invalid input!");
return;
}
println!("Enter the second number: ");
let y: f64 = input_parser();
if f64::is_nan(y) {
println!("Invalid input!");
return;
}
println!("List of operators:");
println!("(1) Add");
println!("(2) Subtract");
println!("(3) Multiply");
println!("(4) Divide");
println!("Select the number associated with the desired operation: ");
let op: f64 = input_parser();
if f64::is_nan(op) {
println!("Invalid input!");
return;
}
let op: i32 = op as i32;
match op {
1 => result = x + y,
2 => result = x - y,
3 => result = x * y,
4 => result = x / y,
_ => {
println!("Invalid selection");
return;
}
}
println!("The result is: {}", result);
}
fn input_parser() -> f64 {
let mut x: String = String::new();
io::stdin().read_line(&mut x).expect("Invalid Input");
let x: f64 = match x.trim().parse() {
Ok(num) => num,
Err(_) => {
return f64::NAN;
}
};
return x;
}
Now you can run the code using the cargo run
command and test for some inputs.
Great job! you have made a simple calculator that can do basic math operations!
In the next tutorial which is just the second part of this tutorial, we will be making a better calculator and on the way, learn some more new concepts!
Until then, have a great day!
GitHub Repo: https://github.com/khairalanam/rust-calculator
If you like whatever I write here, follow me on Devto and check out my socials:
LinkedIn: https://www.linkedin.com/in/khair-alanam-b27b69221/
Twitter: https://www.twitter.com/khair_alanam
GitHub: https://github.com/khairalanam
I also have a new portfolio!
Check it out: https://khairalanam.carrd.co/
Posted on October 28, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.