Added command history and editing using rustyline
[evpf.git] / src / main.rs
1 use regex::Regex;
2 use colored::*;
3 use std::env;
4 use rustyline::Editor;
5
6 const ILLEGAL_EXP : &'static str = "Illegal Expression";
7 const ERR_PARSING_NOT_MATCH : &'static str = "Error parsing expression! \
8 Must be a number or operator (+,-,* or /)";
9 const ERR_POSTFIX_INCOMPLETE : &'static str = "Postfix expression incomplete!";
10 const HELP_TEXT : [&str ; 4] =
11 ["Type a postfix expression to evaluate.",
12 "Example: 4 5 + 12 -",
13 "Supported operators: +, -, *, /",
14 "Type q, Q to quit"
15 ];
16
17 const ERROR : &'static str = "Error";
18 const ERROR_HELP : &'static str = "Type ? or h or H for help";
19
20 // Describe an operator - one of add, subtract, multiply or divide
21 enum Operator {
22 ADD,
23 SUB,
24 MUL,
25 DIV,
26 }
27
28 // structure to hold an expression in the stack
29 struct Expression {
30 value : f32,
31 }
32
33 // function to compute a result by popping the stack and
34 // pushing back the result
35 fn get_result (t : &mut Vec<Expression>, op : Operator) -> Result<f32,String> {
36 // pop the stack for last operand
37 // if nothing - panic with error
38 let n1 = t.pop ();
39 if n1.is_none () {
40 return Err (ILLEGAL_EXP.to_string());
41 }
42 // pop the stack for the first operand
43 // if nothing - panic with error
44 let n2 = t.pop ();
45 if n2.is_none () {
46 return Err (ILLEGAL_EXP.to_string());
47 }
48
49 let num1 = n1.unwrap().value;
50 let num2 = n2.unwrap().value;
51
52 // depending on the operation, set the result
53 match op {
54 Operator::ADD => {
55 t.push (Expression{value: num2 + num1});
56 Ok (num2 + num1)
57 },
58 Operator::SUB => {
59 t.push (Expression{value: num2 - num1});
60 Ok (num2 + num1)
61 },
62 Operator::MUL => {
63 t.push (Expression{value: num2 * num1});
64 Ok (num2 * num1)
65 },
66 Operator::DIV => {
67 t.push (Expression{value: num2 / num1});
68 Ok (num2 / num1)
69 },
70 }
71 }
72
73 // evaluation function
74 fn evaluate (expr : &str, match_num : &regex::Regex) -> Result<f32,String> {
75 let mut ops = Vec::<Expression>::new ();
76 // tokenize the individual words by splitting at whitespace
77 let words = expr.split_whitespace ();
78
79 // iterate over the words
80 for word in words {
81 match word {
82 // if the word matches one of +, -, *, / then push it on the stack
83 // and immediately evaluate the expression by popping the operator
84 // and the last two operands and push the result back on to the stack
85 "+" => {
86 let m = get_result (&mut ops, Operator::ADD);
87 if ! m.is_ok () {
88 return Err (m.unwrap_err().to_string());
89 }
90 },
91 "-" => {
92 let m = get_result (&mut ops, Operator::SUB);
93 if ! m.is_ok () {
94 return Err (m.unwrap_err().to_string());
95 }
96 },
97 "*" => {
98 let m = get_result (&mut ops, Operator::MUL);
99 if ! m.is_ok () {
100 return Err (m.unwrap_err().to_string());
101 }
102 },
103 "/" => {
104 let m = get_result (&mut ops, Operator::DIV);
105 if ! m.is_ok () {
106 return Err (m.unwrap_err().to_string());
107 }
108 },
109 // if word matches a number, push it on to the stack
110 _ => if match_num.is_match (word) {
111 let num = word.parse ().unwrap ();
112 ops.push (Expression { value: num });
113 }
114 // if word doesn't match either operator or number then panic.
115 else {
116 return Err (ERR_PARSING_NOT_MATCH.to_string());
117 }
118 }
119 }
120
121 if ops.len () > 1 {
122 // if the stack has more than one value, it means that the postfix
123 // expression is not complete - so display the stack status
124 return Err (ERR_POSTFIX_INCOMPLETE.to_string());
125 } else {
126 // stack has only one item which is the result so display it
127 let res = ops[0].value;
128 return Ok (res);
129 }
130 }
131
132 // Single command mode - command line arguments mode - evaluate the expression
133 // given in the command line and quit
134 fn run_command_line (args : &Vec<String>, match_num : &regex::Regex) {
135 let mut expr = String::new ();
136 let mut i = 0;
137 // create the expression string to evaluate
138 for arg in args.iter() {
139 if i > 0 {
140 expr.push_str (&arg);
141 expr.push_str (" ");
142 }
143 i += 1;
144 }
145 // evaluate the result
146 let res = evaluate (&expr, &match_num);
147 // if Result is OK then print the result in green
148 if res.is_ok () {
149 let restxt = format! ("{}", res.unwrap());
150 println! ("{}", restxt.green ());
151 } else {
152 // print the error in purple
153 let errtxt = format! ("{}: {}", ERROR,
154 res.unwrap_err());
155 eprintln! ("{}", errtxt.purple ());
156 }
157 }
158
159 // Interactive mode - display the prompt and evaluate expressions entered into
160 // the prompt - until user quits
161 fn run_interactive_mode (match_num : &regex::Regex) {
162 // get a line from input and evaluate it
163 let mut expr = String::new ();
164
165 let mut rl = Editor::<()>::new ();
166 // loop until a blank line is received
167 loop {
168 expr.clear ();
169
170 let line = rl.readline ("evpf> ");
171 if line.is_err () {
172 break;
173 }
174 let hist = line.unwrap ();
175 rl.add_history_entry (&hist);
176 expr.push_str (&hist);
177
178 if expr == "q" || expr == "Q" {
179 // quit if the expression is q or Qs
180 break;
181 } else if expr == "?" || expr == "h" || expr == "H" {
182 // display help text
183 for text in HELP_TEXT.iter() {
184 println! ("{}", text.cyan() );
185 }
186
187 continue;
188 } else if expr == "" {
189 // continue without proceeding
190 continue;
191 }
192
193 // Evaluate result
194 let res = evaluate (&expr, &match_num);
195
196 // if Result is OK then print the result in green
197 if res.is_ok () {
198 let restxt = format! ("{}", res.unwrap());
199 println! ("{}", restxt.green ());
200 } else {
201 // print the error in purple
202 let errtxt = format! ("{}: {}", ERROR,
203 res.unwrap_err());
204 eprintln! ("{}", errtxt.purple());
205 eprintln! ("{}", ERROR_HELP.purple());
206 }
207 }
208 }
209
210 fn main() {
211 // collect the command line arguments - if any
212 let args : Vec<String> = env::args().collect ();
213 // regular expression to match a number
214 let match_num = Regex::new (r"^\d+?\.*?\d*?$").unwrap ();
215
216 if args.len () > 1 {
217 // if arguments are provided run in command line mode - i.e. print the
218 // result and exit
219 run_command_line (&args, &match_num);
220
221 } else {
222 // if arguments are not provided run in interactive mode -
223 // display a prompt and get the expression
224 // repeat until the user quits
225 run_interactive_mode (&match_num);
226
227 }
228 }