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