Project: Command-line Calculator in Rust
Let's dive straight in!!!
In this project, we'll put all of our code in a single file, main.rs
Let's start by importing the necessary libraries.
use std::io::{self, Write};
use std::str::FromStr;
use std::convert::TryFrom;
use std::collections::VecDeque;
use std::iter::FromIterator;
io
module for input/output -->write!
macro to write to standard outputFromStr
andTryFrom
traits for conversionsVecDeque
for a double-ended queue, andFromIterator
trait for conversion from an iterator.
Next, we're going to define two key components of our calculator: the Expression
and Operator
enums.
Why use enums you ask?
Well, enums, short for Enumerations allow us to define a type that could be one of several possible variants.
#[derive(Debug, PartialEq, PartialOrd)]
enum Operator {
Add,
Subtract,
Multiply,
Divide,
}
#[derive(Debug)]
enum Expression {
Number(f64),
Operation(Box<Expression>, Operator, Box<Expression>),
}
Next, we're implementing the FromStr
trait for our Operator
enum, enabling us to effortlessly turn input strings into the operators our calculator needs to execute mathematical operations.
FromStr
trait provides a straightforward way to convert strings into another type. In our case, we're using it to transform string symbols like "+", "-", "*", and "/" into their corresponding Operator
variants.
impl FromStr for Operator {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"+" => Ok(Operator::Add),
"-" => Ok(Operator::Subtract),
"*" => Ok(Operator::Multiply),
"/" => Ok(Operator::Divide),
_ => Err(()),
}
}
}
Now, we're going to implement the TryFrom
trait for our Expression
enum.
TryFrom
trait serves as our safety net, allowing us to attempt conversions that may fail. For our calculator, it means we can try to convert a VecDeque
of string slices into an Expression
, fully prepared for the possibility that this conversion might not always be successful.
VecDeque
stands for "vector deque", which is a growable ring buffer. This data structure is also known as a double-ended queue. It allows for efficient insertion and removal of items from both the front and the back.
impl TryFrom<VecDeque<&str>> for Expression {
type Error = ();
fn try_from(mut expr: VecDeque<&str>) -> Result<Self, Self::Error> {
let lhs:Expression = Expression::Number(expr.pop_front().ok_or(())?.parse::<f64>()
.map_err(|_| ())?
.into());
if expr.is_empty() {
return Ok(lhs);
}
let op = expr.pop_front().ok_or(())?.parse()?;
let rhs = Self::try_from(expr)?;
Ok(Expression::Operation(Box::new(lhs), op, Box::new(rhs)))
}
}
Our next step is to create and implement the eval
method for the Expression
enum.
The method takes an Expression
instance and evaluate it, providing the result of the computation or reporting an error if one occurs.
impl Expression {
fn eval(&self) -> Result<f64, ()> {
match self {
Expression::Number(n) => Ok(*n),
Expression::Operation(lhs, op, rhs) => {
let lhs = lhs.eval()?;
let rhs = rhs.eval()?;
match op {
Operator::Add => Ok(lhs + rhs),
Operator::Subtract => Ok(lhs - rhs),
Operator::Multiply => Ok(lhs * rhs),
Operator::Divide => {
if rhs == 0.0 {
return Err(());
}
Ok(lhs / rhs)
}
}
}
}
}
}
Finally, the main
function, we read the user's input, parse it, evaluate the expression, and print the result.
fn main () {
loop {
let mut buffer = String::new();
print!("Enter an expression (or 'exit' to quit): ");
io::stdout().flush().unwrap();
io::stdin().read_line(&mut buffer).unwrap();
let input = buffer.trim();
if input == "exit" {
break;
}
let parts: VecDeque<&str> = VecDeque::from_iter(input.split_whitespace());
match Expression::try_from(parts) {
Ok(expr) => {
match expr.eval() {
Ok(result) => println!("Result: {:?}", result),
Err(_) => println!("Error: division by zero."),
}
}
Err(_) => println!("Invalid expression")
}
}
}
cargo run
in your terminal to interact with the project
Concepts Learned in the Project:
1. Enums: a way to define a type by enumerating its possible values. In this case, we used it to represent the possible operations (add, subtract, multiply, divide) and expressions (a number or an operation).
2. Traits: defines shared behavior. We used the FromStr
trait to convert from a string to an Operator, and TryFrom
to convert from a VecDeque
to an Expression.
3. Error handling: using the Result
type to handle potential errors in our code.
4. Recursive data structures: expressed using Box
. This allows us to define an Expression
that contains other Expression
s.
5. Using FromStr and TryFrom: traits used for conversion. FromStr
--> convert a string to some other type, and TryFrom
--> conversions that can fail.
Rusty Tools:
TryFrom --> https://doc.rust-lang.org/std/convert/trait.TryFrom.html
Result(error handling) --> https://doc.rust-lang.org/std/result/
VecDeque --> https://doc.rust-lang.org/std/collections/struct.VecDeque.html
FromStr --> https://doc.rust-lang.org/std/str/trait.FromStr.html
Happy Coding and Keep Learning!
Subscribe to my newsletter
Read articles from Nyakio Maina directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Nyakio Maina
Nyakio Maina
Software QA Engineer with knowledge and experience coding in javascript and typescript and learning Rust programming language