Project:  Command-line Calculator in Rust

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 output

  • FromStr and TryFrom traits for conversions

  • VecDeque for a double-ended queue, and

  • FromIterator 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 Expressions.

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:

Happy Coding and Keep Learning!