HiveBrain v1.2.0
Get Started
← Back to all entries
patternrustMinor

Rust Console RPN Calculator

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
rustconsolecalculatorrpn

Problem

I've been trying to teach myself Rust, and I decided that a simple console calculator would be a good project to learn from.

``
use std::io;
extern crate regex;
#[macro_use] extern crate lazy_static;

use regex::Regex;

fn main() {
loop {
println!("Enter input:");
let mut input = String::new();
io::stdin().read_line(&mut input)
.expect("Failed to read line");
let tokens = tokenize(input);
let stack = shunt(tokens);
let res = calculate(stack);
println!("{}", res);
}
}

#[derive(Debug)]
#[derive(PartialEq)]
enum Token {
Number (i64),
Plus,
Sub,
Mul,
Div,
LeftParen,
RightParen,
}

/// Tokenizes the input string into a Vec of Tokens.
fn tokenize(mut input: String) -> Vec {
lazy_static! {
static ref NUMBER_RE: Regex = Regex::new(r"^[0-9]+").unwrap();
}
let mut res = vec![];
while !(input.trim_left().is_empty()) {
input = input.trim_left().to_string();
input = if let Some((_, end)) = NUMBER_RE.find(&input) {
let (num, rest) = input.split_at_mut(end);
res.push(Token::Number(num.parse::().unwrap()));
rest.to_string()
} else {
res.push(match input.chars().nth(0) {
Some('+') => Token::Plus,
Some('-') => Token::Sub,
Some('*') => Token::Mul,
Some('/') => Token::Div,
Some('(') => Token::LeftParen,
Some(')') => Token::RightParen,
_ => panic!("Unknown character!")
});
input.trim_left_matches(|c| c == '+' ||
c == '-' ||
c == '*' ||
c == '/' ||
c == '(' ||
c == ')').to_string()
}
}
res
}

/// Transforms the tokens created by
tokenize` int

Solution

-
There's no space before the parenthesis on an enum variant

-
derives are combined into one line.

-
Make precedence an inherent method on Token.

-
Inside of precedence, match on the dereference of the value. This avoids the spread of &.

-
There's no need for lazy_static; just create a structure and put the regex in it, the reuse it in the loop.

-
There's no need for the large amount of string allocation. Take in a &str instead of a String and simply slice it up.

-
Instead of trimming the input by the operator characters, skip by the number of bytes the first character was.

-
There's no need to specify the type of parse.

-
The code trims the left multiple times; just do it once.

-
There's no need to specify type of queue as it's inferrable.

-
There's no need to use the @ pattern binding; can just use token.

Larger ideas:

-
Create a newtype around Vec to indicate that data is in RPN order. Types avoid the need for documentation.

-
Create multiple types of enums; one with parens and one without. Then there's one less place to have an unreachable. If there's subsets, you could embed the subset in the superset.

-
The error handling is pretty rough for the end user. Mismatched parenthesis kill the program instead of explaining the error and letting the user continue. There's no (obvious) way to exit the program other than by killing it or closing stdin (which produces another error message).

``
use std::io;
extern crate regex; // 0.1.80
#[macro_use]
extern crate lazy_static;

use regex::Regex;

fn main() {
let tokenizer = Tokenizer::new();

loop {
println!("Enter input:");
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.expect("Failed to read line");
let tokens = tokenizer.tokenize(&input);
let stack = shunt(tokens);
let res = calculate(stack);
println!("{}", res);
}
}

#[derive(Debug, PartialEq)]
enum Token {
Number(i64),
Plus,
Sub,
Mul,
Div,
LeftParen,
RightParen,
}

impl Token {
/// Returns the precedence of op
fn precedence(&self) -> usize {
match *self {
Token::Plus | Token::Sub => 1,
Token::Mul | Token::Div => 2,
_ => 0,
}
}
}

struct Tokenizer {
number: Regex,
}

impl Tokenizer {
fn new() -> Tokenizer {
Tokenizer {
number: Regex::new(r"^[0-9]+").expect("Unable to create the regex"),
}
}

/// Tokenizes the input string into a Vec of Tokens.
fn tokenize(&self, mut input: &str) -> Vec {
let mut res = vec![];

loop {
input = input.trim_left();
if input.is_empty() { break }

let (token, rest) = match self.number.find(input) {
Some((_, end)) => {
let (num, rest) = input.split_at(end);
(Token::Number(num.parse().unwrap()), rest)
},
_ => {
match input.chars().next() {
Some(chr) => {
(match chr {
'+' => Token::Plus,
'-' => Token::Sub,
'*' => Token::Mul,
'/' => Token::Div,
'(' => Token::LeftParen,
')' => Token::RightParen,
_ => panic!("Unknown character!"),
}, &input[chr.len_utf8()..])
}
None => panic!("Ran out of input"),
}
}
};

res.push(token);
input = rest;
}

res
}
}

/// Transforms the tokens created by
tokenize into RPN using the
/// Shunting-yard algorithm
fn shunt(tokens: Vec) -> Vec {
let mut queue = vec![];
let mut stack: Vec = vec![];
for token in tokens {
match token {
Token::Number(_) => queue.push(token),
Token::Plus | Token::Sub | Token::Mul | Token::Div => {
while let Some(o) = stack.pop() {
if token.precedence() stack.push(token),
Token::RightParen => {
let mut found_paren = false;
while let Some(op) = stack.pop() {
match op {
Token::LeftParen => {
found_paren = true;
break;
},
_ => queue.push(op),
}
}
assert!(found_paren)
},
}
}
while let Some(op) = stack.pop() {
queue.push(op);
}
queue
}

/// Takes a Vec of Tokens converted to RPN by
shunt` and calculates the result
fn calculate

Code Snippets

use std::io;
extern crate regex; // 0.1.80
#[macro_use]
extern crate lazy_static;

use regex::Regex;

fn main() {
    let tokenizer = Tokenizer::new();

    loop {
        println!("Enter input:");
        let mut input = String::new();
        io::stdin()
            .read_line(&mut input)
            .expect("Failed to read line");
        let tokens = tokenizer.tokenize(&input);
        let stack = shunt(tokens);
        let res = calculate(stack);
        println!("{}", res);
    }
}

#[derive(Debug, PartialEq)]
enum Token {
    Number(i64),
    Plus,
    Sub,
    Mul,
    Div,
    LeftParen,
    RightParen,
}

impl Token {
    /// Returns the precedence of op
    fn precedence(&self) -> usize {
        match *self {
            Token::Plus | Token::Sub => 1,
            Token::Mul | Token::Div => 2,
            _ => 0,
        }
    }
}

struct Tokenizer {
    number: Regex,
}

impl Tokenizer {
    fn new() -> Tokenizer {
        Tokenizer {
            number: Regex::new(r"^[0-9]+").expect("Unable to create the regex"),
        }
    }

    /// Tokenizes the input string into a Vec of Tokens.
    fn tokenize(&self, mut input: &str) -> Vec<Token> {
        let mut res = vec![];

        loop {
            input = input.trim_left();
            if input.is_empty() { break }

            let (token, rest) = match self.number.find(input) {
                Some((_, end)) => {
                    let (num, rest) = input.split_at(end);
                    (Token::Number(num.parse().unwrap()), rest)
                },
                _ => {
                    match input.chars().next() {
                        Some(chr) => {
                            (match chr {
                                '+' => Token::Plus,
                                '-' => Token::Sub,
                                '*' => Token::Mul,
                                '/' => Token::Div,
                                '(' => Token::LeftParen,
                                ')' => Token::RightParen,
                                _ => panic!("Unknown character!"),
                            }, &input[chr.len_utf8()..])
                        }
                        None => panic!("Ran out of input"),
                    }
                }
            };

            res.push(token);
            input = rest;
        }

        res
    }
}

/// Transforms the tokens created by `tokenize` into RPN using the
/// [Shunting-yard algorithm](https://en.wikipedia.org/wiki/Shunting-yard_algorithm)
fn shunt(tokens: Vec<Token>) -> Vec<Token> {
    let mut queue = vec![];
    let mut stack: Vec<Token> = vec![];
    for token in tokens {
        match token {
            Token::Number(_) => queue.push(token),
            Token::Plus | Token::Sub | Token::Mul | Token::Div => {
                while let Some(o) = stack.pop() {
                    if token.precedence() <= o.precedence() {
                        queue.push(o);
                    } else {
                     
let mut chars = input.chars();
match chars.next() {
    Some(chr) => {
        (match chr {
            '+' => Token::Plus,
            '-' => Token::Sub,
            '*' => Token::Mul,
            '/' => Token::Div,
            '(' => Token::LeftParen,
            ')' => Token::RightParen,
            _ => panic!("Unknown character!"),
        }, chars.as_str())

Context

StackExchange Code Review Q#140982, answer score: 2

Revisions (0)

No revisions yet.