DEV Community

Cover image for Rust Tutorial 4: Let's build a Simple Calculator! (Part 1)
Khair Alanam
Khair Alanam

Posted on

Rust Tutorial 4: Let's build a Simple Calculator! (Part 1)

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!

No Compile Error Meme


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:

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");

}
Enter fullscreen mode Exit fullscreen mode

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, and DIVIDE. 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:

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");
}
Enter fullscreen mode Exit fullscreen mode

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)

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");
}
Enter fullscreen mode Exit fullscreen mode

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.

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);
}
Enter fullscreen mode Exit fullscreen mode

Now, let's run the code using the cargo run command.

Rust calculator output

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
}
Enter fullscreen mode Exit fullscreen mode

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:

fn add_two(x: i32, y: i32) -> i32 {
    return x + y;
}
Enter fullscreen mode Exit fullscreen mode

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 and y 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 two i32 numbers x and y and we are returning an i32 result. Hence, we define the return type as i32 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:

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;
}
Enter fullscreen mode Exit fullscreen mode

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:

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;
}
Enter fullscreen mode Exit fullscreen mode

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:

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
}
Enter fullscreen mode Exit fullscreen mode

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.

    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;
    }
Enter fullscreen mode Exit fullscreen mode

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:

    let op: f64 = input_parser();

    if f64::is_nan(op) {
        println!("Invalid input!");
        return;
    }

    let op: i32 = op as i32;
Enter fullscreen mode Exit fullscreen mode

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:

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;
}
Enter fullscreen mode Exit fullscreen mode

Now you can run the code using the cargo run command and test for some inputs.

Rust calculator output


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/

Top comments (0)