Eligible when read up until Input.
3.1 Calculator

In this section, we are going to learn about creating our own console-based calculator. This is how it is going to work:

  • The user chooses any number to start off with.
  • There are five options displayed unto the user: addition, subtraction, multiplication, division and CE (clear entry). The user may choose one by typing the whole word into the console underneath the menu being displayed.
  • The user inputs a number that he wishes to use for the operation.
  • The mathical operation is used dependent on what the user has chosen, and the process is repeated from the second paragraph.

Project

Before we can do anything, we are going to need a project to work in. We will go ahead and generate one using Cargo:

$ cargo new calculator --bin

We are now going to open our main.rs file in our favorite programmer’s text editor or IDE. We should also do a test run of the application, from inside the project folder, to make sure the project generated correctly:

$ cargo run

Granted that everything worked perfectly, we have output:

Hello, world!

First number

The first thing we need to do is retrieve the first number whatever the user may choose.

Input

Let us start off programming by retrieving user input from the console. We are going to make a function specificly for reading an integer from the console.

use std::io;

fn main()
{
	println!("First number: ");
	
	let mut number = read_i32();
}

fn read_i32() -> i32
{
	// Retrieve user input
	let mut input = String::new();

	io::stdin().read_line(&mut input)
			.expect("Error reading line.");

	// Next piece of code goes here
}

Whenever we create a new function, make sure to put it outside of the main function. We can now enter something into the console without much else happening.

Parsing

Having done that, we are going to parse the string into a number: an i32 to be exact. We will place this piece of code where the comment for ‘Next piece of code goes here’ lies:

let mut number: i32 = input.trim().parse()
		.expect("Not a number.");

There is however a tiny problem with this. What if the user inputted something that is not a number? That would mean the application would crash saying ‘Not a number’.

If the user is stupid enough to input something other than a number, we wish to give them a second chance. Go ahead and edit the code we just typed:

// Use input as starting number
let mut number: i32 = match input.trim().parse()
{
	Ok(number) => number, 
	Err(_) => {
		println!("Not a valid integer.");
		
		continue;
	}, 
};

Because of the continue, we are already assuming this is all inside a loop. Let us go ahead and put it into one, and finish off our function by returning the i32 we retrieved from the user:

fn read_i32() -> i32
{
	loop
	{
		// Retrieve user input
		let mut input = String::new();

		io::stdin().read_line(&mut input)
				.expect("Error reading line.");

		let number: i32 = match input.trim().parse()
		{
			Ok(number) => number, 
			Err(_) => {
				println!("Not a valid integer.");
				
				continue;
			}, 
		};

		return number;
	}
}

Breakdown

  • We create a function returning an i32.
  • Our code enters a loop.
  • We retrieve user input.
  • Although not yet discussed, we use match to destructure a pattern contained by the Result the method parse yields. Either it is of type Ok or of Err for error: that is, the user inputted something that was not of type i32.
  • Upon error, we go over the loop again using continue.
  • Upon error-free, we carry on to line 22 of the function, where we return the i32 retrieved from the user.

Running

Let us try the application out to make sure it does what it is supposed to:

First number:
Cow
Not a valid integer.
Horse
Not a valid integer.
32

We typed in two things which were not of the i32 type, and so are given an error. On the third try, we successfully enter an actual integer, this is stored inside the let binding inside the main function.

Interface

The next step is to define an interface for us where we display the menu containing all the alternatives the user may use. Whilst you could make a new function containing the menu, we are simply going to put it inside the main method to save ourselves some time.

fn main()
{
	println!("First number: ");

	let mut number = read_i32();

	println!("");

	// Menu
	println!("Operations: ");
	println!("+\tAdds to current number.");
	println!("-\tSubtracts from current number.");
	println!("*\tMultiplies current number.");
	println!("/\tDivides current number.");
	println!("CE\tClears entry");
	
	// Next piece of code goes here
}

The \t nicely writes out a tab for us, so that the different descriptions get placed on the same horizontal position once printed out into the console. The utterly random println! prior to the menu creates a blank space in between the menu and the what concerns the first input.

Menu input

Now that we have a basic interface set up, it is about time we start working on the application’s logic.

We are going to scan for user input once again. For this we are going to create a function which scans the console for a string of text, rather than an i32:

fn read_str() -> String
{
	let mut input = String::new();

	io::stdin().read_line(&mut input)
			.expect("Error reading line.");

	input
}

Now we will go ahead and use this function to retrieve a string inside the main function:

// Process input
let input = read_str();
let input = input.trim();

// Next piece of code goes here

We also trim the string to make sure there is no whitespace left on the string from the reading process. Once that is done, we are going to use match to check for the various different options of the menu:

match input
{
	"+" => calc("+", &mut number),
	"-" => calc("-", &mut number),
	"*" => calc("*", &mut number),
	"/" => calc("/", &mut number),
	_ => {
		if input.eq_ignore_ascii_case("ce")
		{
			calc("ce", &mut number);
		}
		else {
			println!("There is no such!");
		}
	}
}

println!("Number is now {}.", number);

In order for this to work we also need to make use of a feature from the Rust standard library:

use std::ascii::AsciiExt;

Breakdown

  • We match the input and have a look whether or not anything contained within the menu is the same as our input.
  • The four operators are checked: addition, subtraction, multiplication and division.
  • Upon the default case occuring, we check and see if input corresponds to something else. If it does, we wish to check and see if it is equal to ‘ce’, but with a slight difference: we wish for it to select it no matter should the case vary, this is why we need to use std::ascii::AsciiExt.
  • In order to check for equality no matter the letter case, we make use of the eq_ignore_ascii_case method.

Calculate

You did probably notice, however, that we have a function being called whenever it locates a correctly inputted string from the menu: calc. This function we are going to use in order to change the value of number. The idea of this function is for us to send in the type of symbol as well as the number and let it change the number for us.

Do not worry about all the &mut and & for now, those will be covered during another chapter. For now, let us focus on creating the calc function.

fn calc(t: &str, number: &mut i32)
{
	if t != "ce"
	{
		println!("Number to {} by?", t);
		let change = read_i32();

		match t
		{
			"+" => *number += change, 
			"-" => *number -= change, 
			"*" => *number *= change, 
			"/" => *number /= change, 
			_ => panic!("This case cannot occur.")
		}
	}
	else
	{
		*number = 0;
	}
}

Breakdown

  • The function takes two arguments: t: &str and &mut i32. That is, a string slice and a mutable reference to an i32.
  • We check to see if t (which stands for type, ‘type’ itself cannot be used since it is a keyword in Rust) is not equal to a string slice of ce (clear entry). If it is and the else block is executed, we set the number to zero, getting the changable version of the variable binding using * before entering the variable we wish to change.
  • If it is not equal to ce on the other hand, we ask the user for another number to use the operation with. We then check and see what operation it was using a match, then perform the operations on the two numbers accordingly.
  • On the default case, we panic! without purpose. It is required to have the case, yet it cannot be reached in any way.

Running

Let us run the application once again and see what happens:

First number:
67

Operations:
+ Adds to current number.
- Subtracts from current number.
* Multiplies current number.
/ Divides current number.
CE Clears entry
+
Number to + by?
12
Number is now 79.

This appears to work like a charm! There are however two points worth considering:

  • Division by zero is not going to work very well.
  • We probably wish to loop the whole program so that it can be used over and over.

Division by zero

In order to fix the division by zero problem, we are going to chang some things on the match inside the calc function:

match t
{
	"+" => *number += change, 
	"-" => *number -= change, 
	"*" => *number *= change, 
	"/" => { // This we change
		if change != 0
		{
			*number /= change
		}
		else {
			println!("Cannot divide number by zero!");
		}
	}, 
	_ => panic!("This case cannot occur.")
}

Rather than letting it divide by zero, we control the flow not to divide should the number for division ever be zero. Instead we choose to print out an error solving the whole thing: and the program carries on normally.

Loop

Finally we are going to make sure our program runs forever, so that people in 2,000 years from now can make use of our program when they calculate their crazy equations. Put the loop around all of the game’s logic and menu as well as blank line to seperate each big chunks of text into smaller ones:

loop
{
	println!("");

	// Menu
	println!("Operations: ");
	println!("+\tAdds to current number.");
	println!("-\tSubtracts from current number.");
	println!("*\tMultiplies current number.");
	println!("/\tDivides current number.");
	println!("CE\tClears entry");

	// Logic
	let input = read_str();
	let input = input.trim();

	match input
	{
		"+" => calc("+", &mut number),
		"-" => calc("-", &mut number),
		"*" => calc("*", &mut number),
		"/" => calc("/", &mut number),
		_ => {
			if input.eq_ignore_ascii_case("ce")
			{
				calc("ce", &mut number);
			}
			else {
				println!("There is no such!");
			}
		}
	}

	println!("Number is now {}.", number);
}

When you run the program, you will get the opportunity to do mathematics forever. Perhaps one would want to end the application? We may then want to expand the menu with the option as well as make it the action fully functional. But that, you will have to do yourself. A few exercises you are going to have to do!

Complete code

For the lazy soul, or to use in order to correct one’s code for errors, feel free to go over the whole code.

use std::io;
use std::ascii::AsciiExt;

fn main()
{
	println!("First number: ");

	let mut number = read_i32();

	loop
	{
		println!("");

		// Menu
		println!("Operations: ");
		println!("+\tAdds to current number.");
		println!("-\tSubtracts from current number.");
		println!("*\tMultiplies current number.");
		println!("/\tDivides current number.");
		println!("CE\tClears entry");
		println!("Exit\tSadly exits the application.")

		// Logic
		let input = read_str();
		let input = input.trim();

		match input
		{
			"+" => calc("+", &mut number),
			"-" => calc("-", &mut number),
			"*" => calc("*", &mut number),
			"/" => calc("/", &mut number),
			_ => {
				if input.eq_ignore_ascii_case("ce")
				{
					calc("ce", &mut number);
				}
				else {
					println!("There is no such!");
				}
			}
		}

		println!("Number is now {}.", number);
	}
}

fn calc(t: &str, number: &mut i32)
{
	if t != "ce"
	{
		println!("Number to {} by?", t);
		let change = read_i32();

		match t
		{
			"+" => *number += change, 
			"-" => *number -= change, 
			"*" => *number *= change, 
			"/" => { // This we change
				if change != 0
				{
					*number /= change
				}
				else {
					println!("Cannot divide number by zero!");
				}
			}, 
			_ => panic!("This case cannot occur.")
		}
	}
	else
	{
		*number = 0;
	}
}

fn read_str() -> String
{
	let mut input = String::new();

	io::stdin().read_line(&mut input)
			.expect("Error reading line.");

	input
}

fn read_i32() -> i32
{
	loop
	{
		let mut input = String::new();

		io::stdin().read_line(&mut input)
				.expect("Error reading line.");

		// Use input as starting number
		let number: i32 = match input.trim().parse()
		{
			Ok(number) => number, 
			Err(_) => {
				println!("Not a valid integer."); continue;
			}, 
		};

		return number;
	}
}

Exercises

  • Add a menu option that says exit.
  • Make the application actually exit upon typing something along the lines of ‘exit’.

Conclusion

For learning purposes, feel free to attempt to write this program without the tutorial: only check it out when you are stuck on something.

You may have been brought here from Input.