X-Git-Url: https://harishankar.org/repos/?p=evpf.git;a=blobdiff_plain;f=src%2Fmain.rs;h=51ea94eb5e681337e5b6ecb386c8f81771fcb528;hp=dcf215f5402d1ac679f98b03c4373cc29e150be3;hb=HEAD;hpb=a12f1c210f0d76985c641bc1b8a16175c8512045 diff --git a/src/main.rs b/src/main.rs index dcf215f..51ea94e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,117 +1,280 @@ -use std::io; use regex::Regex; +use colored::*; +use std::env; +use rustyline::Editor; +use std::path::PathBuf; -// describe the operators -enum Operator { - NUM, ADD, SUB, MUL, DIV, +const ILLEGAL_EXP : &'static str = "Illegal Expression"; +const ERR_PARSING_NOT_MATCH : &'static str = "Error parsing expression! \ + Must be a number or operator (+,-,* or /)"; +const ERR_POSTFIX_INCOMPLETE : &'static str = "Postfix expression incomplete!"; +const HELP_TEXT : [&str ; 4] = + ["Type a postfix expression to evaluate.", + "Example: 4 5 + 12 -", + "Supported operators: +, -, *, /", + "Type q, Q to quit" + ]; + +const HOMEDIR_NOT_FOUND : &'static str = "User home directory not found \ + (missing environment?)."; + +const HISTORY_FILE_NOT_FOUND : &'static str = "History file not found."; + +const SAVE_HISTORY_ERROR : &'static str = "Unable to save command history!"; + +const ERROR : &'static str = "Error"; +const ERROR_HELP : &'static str = "Type ? or h or H for help"; + +// Describe an operator - one of add, subtract, multiply or divide +enum Operator { + ADD, + SUB, + MUL, + DIV, } // structure to hold an expression in the stack struct Expression { value : f32, - operator : Operator, } // function to compute a result by popping the stack and // pushing back the result -fn get_result (t : &mut Vec::) { - // pop the stack to get the operator - // if nothing - panic with error - let op = match t.pop () { - Some(x) => x, - None => panic! ("Illegal expression!"), - }; +fn get_result (t : &mut Vec, op : Operator) -> Result { // pop the stack for last operand // if nothing - panic with error - let n1 = match t.pop () { - Some (x) => x, - None => panic! ("Illegal expression!"), - }; + let n1 = t.pop (); + if n1.is_none () { + return Err (ILLEGAL_EXP.to_string()); + } // pop the stack for the first operand // if nothing - panic with error - let n2 = match t.pop () { - Some (x) => x, - None => panic! ("Illegal expression!"), - }; - - let mut res : f32 = 0.0; + let n2 = t.pop (); + if n2.is_none () { + return Err (ILLEGAL_EXP.to_string()); + } + + let num1 = n1.unwrap().value; + let num2 = n2.unwrap().value; // depending on the operation, set the result - match op.operator { - Operator::ADD => res = n1.value + n2.value, - Operator::SUB => res = n2.value - n1.value, - Operator::MUL => res = n1.value * n2.value, - Operator::DIV => res = n2.value / n1.value, - _ => panic! ("Illegal operator in expression!"), + match op { + Operator::ADD => { + t.push (Expression{value: num2 + num1}); + Ok (num2 + num1) + }, + Operator::SUB => { + t.push (Expression{value: num2 - num1}); + Ok (num2 + num1) + }, + Operator::MUL => { + t.push (Expression{value: num2 * num1}); + Ok (num2 * num1) + }, + Operator::DIV => { + t.push (Expression{value: num2 / num1}); + Ok (num2 / num1) + }, } - // push the result back to the stack - t.push (Expression {value : res, operator : Operator::NUM }); } // evaluation function -fn evaluate (expr : &str) { +fn evaluate (expr : &str, match_num : ®ex::Regex) -> Result { let mut ops = Vec::::new (); // tokenize the individual words by splitting at whitespace let words = expr.split_whitespace (); - // regular expression to match a number - let match_num = Regex::new (r"^\d+?.*?\d*?$").unwrap (); + // iterate over the words for word in words { match word { // if the word matches one of +, -, *, / then push it on the stack // and immediately evaluate the expression by popping the operator // and the last two operands and push the result back on to the stack - "+" => {ops.push (Expression{ value: 0.0, operator: Operator::ADD }); - get_result (&mut ops) }, - "-" => {ops.push (Expression{ value: 0.0, operator: Operator::SUB }); - get_result (&mut ops) }, - "*" => {ops.push (Expression{ value: 0.0, operator: Operator::MUL}); - get_result (&mut ops)}, - "/" => {ops.push (Expression{ value: 0.0, operator: Operator::DIV}); - get_result (&mut ops) }, + "+" => { + let m = get_result (&mut ops, Operator::ADD); + if ! m.is_ok () { + return Err (m.unwrap_err().to_string()); + } + }, + "-" => { + let m = get_result (&mut ops, Operator::SUB); + if ! m.is_ok () { + return Err (m.unwrap_err().to_string()); + } + }, + "*" => { + let m = get_result (&mut ops, Operator::MUL); + if ! m.is_ok () { + return Err (m.unwrap_err().to_string()); + } + }, + "/" => { + let m = get_result (&mut ops, Operator::DIV); + if ! m.is_ok () { + return Err (m.unwrap_err().to_string()); + } + }, // if word matches a number, push it on to the stack _ => if match_num.is_match (word) { - let num : f32 = word.parse ().unwrap (); - ops.push (Expression {value: num, operator: Operator::NUM }); - } + let num = word.parse ().unwrap (); + ops.push (Expression { value: num }); + } // if word doesn't match either operator or number then panic. else { - panic! ("Error parsing expression! - Must be a number or operator (+,-,* or /)"); - } + return Err (ERR_PARSING_NOT_MATCH.to_string()); + } } } + + if ops.len () > 1 { // if the stack has more than one value, it means that the postfix // expression is not complete - so display the stack status - if ops.len () > 1 { - println! ("Postfix Expression not complete. Current stack: "); - for exp in ops { - println! ("{}", exp.value); + return Err (ERR_POSTFIX_INCOMPLETE.to_string()); + } else { + // stack has only one item which is the result so display it + let res = ops[0].value; + return Ok (res); + } +} + +// Single command mode - command line arguments mode - evaluate the expression +// given in the command line and quit +fn run_command_line (args : &Vec, match_num : ®ex::Regex) { + let mut expr = String::new (); + let mut i = 0; + // create the expression string to evaluate + for arg in args.iter() { + if i > 0 { + expr.push_str (&arg); + expr.push_str (" "); } + i += 1; } - // stack has only one item which is the result so display it - else { - println! ("Result: {}", ops[0].value); + // evaluate the result + let res = evaluate (&expr, &match_num); + // if Result is OK then print the result in green + if res.is_ok () { + let restxt = format! ("{}", res.unwrap()); + println! ("{}", restxt.green ()); + } else { + // print the error in purple + let errtxt = format! ("{}: {}", ERROR, + res.unwrap_err()); + eprintln! ("{}", errtxt.purple ()); } - println! (); } -fn main() { +// get the history file name as string - home dir + .evpfhistory +fn get_history_file () -> Result { + // get the environment variable HOME + let home_path = env::var ("HOME"); + // if not found, return an error + if home_path.is_err () { + return Err (HOMEDIR_NOT_FOUND.to_string()); + } + // build the path for the history file i.e. homedir + .evpfhistory in + // platform independent way + let mut hist_file = PathBuf::new (); + hist_file.push (home_path.unwrap()); + hist_file.push (".evpfhistory"); + + // if cannot convert to string return error + if hist_file.to_str ().is_none () { + return Err (HOMEDIR_NOT_FOUND.to_string()); + } + + // return the history file path as a string + let hist_file_path = String::from (hist_file.to_str().unwrap()); + return Ok (hist_file_path); +} + +// Interactive mode - display the prompt and evaluate expressions entered into +// the prompt - until user quits +fn run_interactive_mode (match_num : ®ex::Regex) { // get a line from input and evaluate it let mut expr = String::new (); - // loop until a blank line is received + + let mut rl = Editor::<()>::new (); + + // load the history file + let hist_file = get_history_file (); + // if unable to load the history file, display the appropriate error + if hist_file.is_err () { + eprintln! ("{}", &hist_file.unwrap_err()); + } else { + if rl.load_history (&hist_file.unwrap ()).is_err () { + eprintln! ("{}", HISTORY_FILE_NOT_FOUND.purple()); + } + } + // loop until a blank line is received loop { - expr.clear (); - println!("Enter an expression (postfix) (blank to quit): "); - // read a line of text - io::stdin().read_line (&mut expr).expect ("Error reading line!"); - // trim the text - let expr = expr.trim (); - // quit if the expression is blank - if expr == "" { + expr.clear (); + + let line = rl.readline ("evpf> "); + if line.is_err () { + break; + } + let hist = line.unwrap (); + rl.add_history_entry (&hist); + expr.push_str (&hist); + + if expr == "q" || expr == "Q" { + // quit if the expression is q or Qs break; + } else if expr == "?" || expr == "h" || expr == "H" { + // display help text + for text in HELP_TEXT.iter() { + println! ("{}", text.cyan() ); + } + + continue; + } else if expr == "" { + // continue without proceeding + continue; } - evaluate (&expr); + // Evaluate result + let res = evaluate (&expr, &match_num); + + // if Result is OK then print the result in green + if res.is_ok () { + let restxt = format! ("{}", res.unwrap()); + println! ("{}", restxt.green ()); + } else { + // print the error in purple + let errtxt = format! ("{}: {}", ERROR, + res.unwrap_err()); + eprintln! ("{}", errtxt.purple()); + eprintln! ("{}", ERROR_HELP.purple()); + } + } + // save the history + let hist_file = get_history_file (); + if ! hist_file.is_err () { + if rl.save_history (&hist_file.unwrap()).is_err () { + eprintln! ("{}", SAVE_HISTORY_ERROR); + } + } else { + eprintln! ("{}", &hist_file.unwrap_err()); + } +} + +fn main() { + // collect the command line arguments - if any + let args : Vec = env::args().collect (); + // regular expression to match a number + let match_num = Regex::new (r"^\-?\d+?\.*?\d*?$").unwrap (); + + if args.len () > 1 { + // if arguments are provided run in command line mode - i.e. print the + // result and exit + run_command_line (&args, &match_num); + + } else { + // if arguments are not provided run in interactive mode - + // display a prompt and get the expression + // repeat until the user quits + run_interactive_mode (&match_num); + } }