1use csv::{self};
2use std::fs;
3
4use crate::{
5 constants::*,
6 errors::ExamReaderError,
7 shuffler::{Choice, Choices, CorrectChoice, ExamSetting, Question},
8};
9
10pub fn from_tex(
11 filename: &str,
12) -> Result<(Option<String>, Vec<Question>, Option<ExamSetting>), ExamReaderError> {
13 let filecontent = fs::read_to_string(filename);
14 match filecontent {
15 Ok(contnet) => match get_questions_from_tex(&contnet) {
16 Ok(cntnt) => Ok((
17 get_preamble_from_text(&contnet),
18 cntnt,
19 get_setting_from_text(&contnet),
20 )),
21 Err(err) => Err(ExamReaderError::TemplateError(err)),
22 },
23 Err(err) => Err(ExamReaderError::IOError(err)),
24 }
25}
26fn get_setting_from_text(content: &String) -> Option<ExamSetting> {
27 if let Some(s) = content.find(TEX_SETTING_START) {
28 if let Some(e) = content.find(TEX_SETTING_END) {
29 let sttng = content[(s + 11)..e].trim().to_string();
30 let sertting_parts: Vec<(String, String)> = sttng
31 .split("\n")
32 .map(|s| s.trim().trim_start_matches("%").trim())
33 .map(|s| {
34 let key_val: Vec<String> = s
35 .split("=")
36 .map(|ss| ss.trim())
37 .map(|v| v.to_string())
38 .map(|v| v.trim().to_string())
39 .collect();
40 let key = if let Some(ks) = key_val.get(0) {
41 let val = if let Some(vs) = key_val.get(1) {
42 (ks.to_owned(), vs.to_owned())
43 } else {
44 (ks.to_owned(), "".to_string())
45 };
46 val
47 } else {
48 ("".to_string(), "".to_string())
49 };
50
51 return (key.0, key.1);
52 })
53 .collect();
54 let exm_setting = sertting_parts.iter().fold(ExamSetting::new(), |a, v| {
55 ExamSetting::append_from_key_value(a, &v.0, (v.1).to_owned())
56 });
57
58 return Some(exm_setting);
59 } else {
60 return None;
61 }
62 }
63
64 None
65}
66fn get_preamble_from_text(content: &String) -> Option<String> {
67 if let Some(s) = content.find(TEX_PREAMBLE_START) {
68 if let Some(e) = content.find(TEX_PREAMBLE_END) {
69 let preamble = content[(s + 12)..e].trim().to_string();
70 return Some(preamble);
71 } else {
72 return None;
73 }
74 }
75 None
76}
77
78fn get_questions_from_tex(content: &String) -> Result<Vec<Question>, String> {
79 let body_start = if let Some(bdy_start) = content.find(TEX_DOC_START) {
80 bdy_start + 16
81 } else {
82 return Err("The document must have \\begin{document} tag".to_owned());
83 };
84 let body_end = if let Some(bdy_end) = content.find(TEX_DOC_END) {
85 bdy_end
86 } else {
87 return Err("The document must have \\end{document} tag".to_owned());
88 };
89 let body = content[body_start..body_end].to_string();
90 let parts: Vec<String> = body
91 .split(TEX_QUESTION_START)
92 .map(|p| String::from(p.trim()))
93 .collect();
94 let mut order: u32 = 1;
95 let qs: Vec<Question> = parts
96 .into_iter()
97 .map(|q| {
98 let body = get_question_text_from_tex(&q);
99 (body, q)
100 })
101 .filter(|(b, _q)| b != "")
102 .map(|(body, q)| {
103 let opts = get_question_options_from_tex(&q);
104 let question = Question {
105 text: body,
106 choices: opts,
107 order,
108 group: 1,
109 };
110 order += 1;
111 question
112 })
113 .collect();
114
115 if qs.len() == 0 {
116 return Err("No questions were found.".to_string());
117 }
118 Ok(qs)
119}
120
121fn get_question_text_from_tex(q: &String) -> String {
122 if let Some(end_of_question_text) = q.find(TEX_QUESTION_END) {
123 let text = q[..end_of_question_text].trim().to_string();
124 text
125 } else {
126 "".to_string()
127 }
128}
129
130fn get_question_options_from_tex(q: &String) -> Option<Choices> {
131 let parts: Vec<Choice> = q
132 .split(TEX_OPTION_START)
133 .map(|f| {
134 if let Some(o_end) = f.find(TEX_OPTION_END) {
135 f[..o_end].trim().to_string()
136 } else {
137 "".to_string()
138 }
139 })
140 .filter(|o| o != "")
141 .map(|o| Choice::new(&o))
142 .collect();
143
144 if parts.len() == 0 {
145 return None;
146 }
147 Some(Choices(parts, CorrectChoice(0), None))
148}
149
150pub fn from_csv(filename: &str) -> Result<Vec<Question>, ExamReaderError> {
151 let filecontent = fs::read_to_string(filename);
152 match filecontent {
153 Ok(content) => {
154 let rdr = csv::ReaderBuilder::new()
155 .has_headers(false)
156 .flexible(true)
157 .from_reader(content.as_bytes());
158
159 match get_questions_from_csv(rdr) {
160 Ok(qs) => Ok(qs),
161 Err(err) => Err(ExamReaderError::TemplateError(err)),
162 }
163 }
164 Err(err) => Err(ExamReaderError::IOError(err)),
165 }
166}
167
168pub fn from_txt(filename: &str) -> Result<Vec<Question>, ExamReaderError> {
169 let filecontent = fs::read_to_string(filename);
170 match filecontent {
171 Ok(content) => {
172 let rdr = csv::ReaderBuilder::new()
173 .delimiter(b'\t')
174 .flexible(true)
175 .has_headers(false)
176 .from_reader(content.as_bytes());
177 match get_questions_from_csv(rdr) {
178 Ok(qs) => Ok(qs),
179 Err(err) => Err(ExamReaderError::TemplateError(err)),
180 }
181 }
182 Err(err) => Err(ExamReaderError::IOError(err)),
183 }
184}
185
186fn get_questions_from_csv(mut rdr: csv::Reader<&[u8]>) -> Result<Vec<Question>, String> {
187 let mut order = 0;
188 let qs: Vec<Question> = rdr
189 .records()
190 .into_iter()
191 .map(|res| match res {
192 Ok(rec) => {
193 let record: Vec<String> = rec.iter().map(|f| f.to_string()).collect();
194 let choices = get_question_options_from_csv(record[2..].to_vec());
195 if let Some(text) = record.get(1) {
196 order = order + 1;
197 let group: u32 = if let Some(group_str) = record.get(0) {
198 group_str.parse().unwrap_or(1)
199 } else {
200 1
201 };
202
203 Question {
204 text: text.to_owned(),
205 order,
206 choices: Some(choices),
207 group,
208 }
209 } else {
210 Question::from("", 0)
211 }
212 }
213 Err(_err) => Question::from("", 0),
214 })
215 .filter(|q| q.text != "")
216 .collect();
217
218 if qs.len() == 0 {
219 return Err("no questions were found".to_string());
220 }
221 Ok(qs)
222}
223
224fn get_question_options_from_csv(options: Vec<String>) -> Choices {
225 let choices: Vec<Choice> = options.into_iter().map(|o| Choice { text: o }).collect();
226 Choices(choices, CorrectChoice(0), None)
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn read_from_txt_bad_file() {
235 let filename = "files/testing/samples.txt";
237 let tex = match from_txt(filename) {
238 Ok(_) => "nothing".to_owned(),
239 Err(err) => err.to_string(),
240 };
241 assert_eq!(
242 tex,
243 "Reading error".to_string(),
244 "testing the file does not exist"
245 )
246 }
247 #[test]
248 fn read_from_txt_no_questions() {
249 let filename = "files/testing/sample-no-questions.txt";
251 let tex = match from_txt(filename) {
252 Ok(_qs) => "".to_string(),
253 Err(err) => err.to_string(),
254 };
255 assert_eq!(
256 tex,
257 "Your input file is badly formatted: `no questions were found`".to_string(),
258 "testing no questions in csv"
259 )
260 }
261
262 #[test]
263 fn read_from_txt_first_is_different() {
264 let filename = "files/testing/sample-first-options-different.txt";
266 let tex = match from_txt(filename) {
267 Ok(qs) => qs,
268 Err(_err) => [].to_vec(),
269 };
270 assert_eq!(
271 tex.len(),
272 20,
273 "testing first question with different options"
274 );
275 let qs1 = match tex.get(0) {
276 Some(q) => match &q.choices {
277 Some(op) => op.0.len(),
278 None => 0,
279 },
280 None => 0,
281 };
282 assert_eq!(qs1, 6, "testing first question with different options");
283
284 let qs2 = match tex.get(1) {
285 Some(q) => match &q.choices {
286 Some(op) => op.0.len(),
287 None => 0,
288 },
289 None => 0,
290 };
291 assert_eq!(qs2, 5, "testing first question with different options");
292
293 let qs3: i32 = match tex.get(2) {
294 Some(q) => match &q.choices {
295 Some(op) => op.0.len() as i32,
296 None => 0,
297 },
298 None => -1,
299 };
300 assert_eq!(qs3, 0, "testing first question with different options")
301 }
302
303 #[test]
304 fn read_from_csv_bad_file() {
305 let filename = "files/testing/samples.csv";
307 let tex = match from_csv(filename) {
308 Ok(_) => "nothing".to_owned(),
309 Err(err) => err.to_string(),
310 };
311 assert_eq!(
312 tex,
313 "Reading error".to_string(),
314 "testing the file does not exist"
315 )
316 }
317 #[test]
318 fn read_from_csv_no_questions() {
319 let filename = "files/testing/sample-no-questions.csv";
321 let tex = match from_csv(filename) {
322 Ok(_qs) => "".to_string(),
323 Err(err) => err.to_string(),
324 };
325 assert_eq!(
326 tex,
327 "Your input file is badly formatted: `no questions were found`".to_string(),
328 "testing no questions in csv"
329 )
330 }
331
332 #[test]
333 fn read_from_csv_first_is_different() {
334 let filename = "files/testing/sample-first-options-different.csv";
336 let tex = match from_csv(filename) {
337 Ok(qs) => qs,
338 Err(_err) => [].to_vec(),
339 };
340 assert_eq!(
341 tex.len(),
342 6,
343 "testing first question with different options"
344 );
345 let qs1 = match tex.get(0) {
346 Some(q) => match &q.choices {
347 Some(op) => op.0.len(),
348 None => 0,
349 },
350 None => 0,
351 };
352 assert_eq!(qs1, 7, "testing first question with different options");
353
354 let qs2 = match tex.get(1) {
355 Some(q) => match &q.choices {
356 Some(op) => op.0.len(),
357 None => 0,
358 },
359 None => 0,
360 };
361 assert_eq!(qs2, 6, "testing first question with different options");
362
363 let qs3: i32 = match tex.get(2) {
364 Some(q) => match &q.choices {
365 Some(op) => op.0.len() as i32,
366 None => 0,
367 },
368 None => -1,
369 };
370 assert_eq!(qs3, 0, "testing first question with different options")
371 }
372
373 #[test]
374 fn read_from_tex_bad_file() {
375 let filename = "files/testing/templatte.tex";
377 let tex = match from_tex(filename) {
378 Ok(_) => "nothing".to_owned(),
379 Err(err) => err.to_string(),
380 };
381 assert_eq!(
382 tex,
383 "Reading error".to_string(),
384 "testing the file does not exist"
385 )
386 }
387 #[test]
388 fn read_from_tex_no_begin_doc() {
389 let filename = "files/testing/template-no-begin-doc.tex";
391 let tex = match from_tex(filename) {
392 Ok(_) => "nothing".to_owned(),
393 Err(err) => err.to_string(),
394 };
395 assert_eq!(
396 tex,
397 "Your input file is badly formatted: `The document must have \\begin{document} tag`"
398 .to_string(),
399 "testing begin document tag"
400 );
401 }
402 #[test]
403 fn read_from_tex_no_end_doc() {
404 let filename = "files/testing/template-no-end-doc.tex";
405 let tex = match from_tex(filename) {
406 Ok(_) => "nothing".to_owned(),
407 Err(err) => err.to_string(),
408 };
409 assert_eq!(
410 tex,
411 "Your input file is badly formatted: `The document must have \\end{document} tag`"
412 .to_string(),
413 "testing end document tag"
414 );
415 }
416
417 #[test]
418 fn read_from_tex_no_questions() {
419 let filename = "files/testing/template-no-questions.tex";
420 let tex = match from_tex(filename) {
421 Ok(_) => "nothing".to_owned(),
422 Err(err) => err.to_string(),
423 };
424 assert_eq!(
425 tex,
426 "Your input file is badly formatted: `No questions were found.`".to_string(),
427 "testing no questions"
428 );
429 }
430 fn read_from_tex() -> Result<(String, usize, Vec<Question>), String> {
431 let filename = "files/testing/template.tex";
432 match from_tex(filename) {
433 Ok((preamble, qs, _)) => match preamble {
434 Some(pre) => Ok((pre, qs.len(), qs)),
435 None => Err("".to_string()),
436 },
437 Err(_err) => Err("".to_string()),
438 }
439 }
440
441 #[test]
442 fn read_from_tex_preamble() {
443 match read_from_tex() {
444 Ok(tex) => {
445 assert_eq!(
446 tex.0,
447 "\\usepackage{amsfonts}".to_string(),
448 "testing preambles"
449 );
450 }
451 Err(_err) => (),
452 }
453 }
454
455 #[test]
456 fn read_from_tex_number_of_qs() {
457 match read_from_tex() {
458 Ok(tex) => {
459 assert_eq!(tex.1, 20);
460 }
461 Err(_err) => (),
462 }
463 }
464
465 #[test]
466 fn read_from_tex_number_of_options_is_zero() {
467 match read_from_tex() {
468 Ok(tex) => {
469 let no_options_1: i32 = match tex.2.get(0) {
470 Some(op) => match &op.choices {
471 Some(opts) => opts.0.len() as i32,
472 None => 0,
473 },
474 None => -2,
475 };
476 assert_eq!(no_options_1, 0);
477 }
478 Err(_err) => (),
479 }
480 }
481
482 #[test]
483 fn read_from_tex_number_of_options_is_five() {
484 match read_from_tex() {
485 Ok(tex) => {
486 let no_options_1: i32 = match tex.2.get(1) {
487 Some(op) => match &op.choices {
488 Some(opts) => opts.0.len() as i32,
489 None => 0,
490 },
491 None => -2,
492 };
493 assert_eq!(no_options_1, 5)
494 }
495 Err(_err) => (),
496 }
497 }
498
499 #[test]
500 fn read_from_tex_setting_full() {
501 let filename = "files/testing/exam_setting.tex";
502 let exammatch = match from_tex(filename) {
503 Ok((_, _, es)) => match es {
504 Some(exam_setting) => exam_setting,
505 None => ExamSetting::new(),
506 },
507 Err(_err) => ExamSetting::new(),
508 };
509 assert_eq!(
510 exammatch,
511 ExamSetting {
512 university: "KFUPM".to_string(),
513 department: "MATH".to_string(),
514 term: "Term 213".to_string(),
515 coursecode: "MATH102".to_string(),
516 examname: "Major Exam 1".to_string(),
517 examdate: "2022-07-22T03:38:27.729Z".to_string(),
518 timeallowed: "Two hours".to_string(),
519 numberofvestions: 4,
520 groups: "4".to_string(),
521 code_name: Some("CODE".to_string()),
522 code_numbering: Some("ARABIC".to_string()),
523 examtype: Some("QUIZ".to_string())
524 },
525 "testing exam setting"
526 );
527 }
528
529 #[test]
530 fn read_from_tex_setting_partial() {
531 let filename = "files/testing/exam_setting_withmissing_ones.tex";
532 let exammatch = match from_tex(filename) {
533 Ok((_, _, es)) => match es {
534 Some(exam_setting) => exam_setting,
535 None => ExamSetting::new(),
536 },
537 Err(_err) => ExamSetting::new(),
538 };
539 assert_eq!(
540 exammatch,
541 ExamSetting {
542 university: "KFUPM".to_string(),
543 department: "MATH".to_string(),
544 term: "Term 213".to_string(),
545 coursecode: "".to_string(),
546 examname: "".to_string(),
547 examdate: "".to_string(),
548 timeallowed: "Two hours".to_string(),
549 numberofvestions: 4,
550 groups: "".to_string(),
551 code_name: None,
552 code_numbering: None,
553 examtype: None
554 },
555 "testing exam partial setting"
556 );
557 }
558
559 #[test]
560 fn read_from_tex_setting_empty() {
561 let filename = "files/testing/template.tex";
562 let exammatch = match from_tex(filename) {
563 Ok((_, _, es)) => match es {
564 Some(exam_setting) => exam_setting,
565 None => ExamSetting::new(),
566 },
567 Err(_err) => ExamSetting::new(),
568 };
569 assert_eq!(
570 exammatch,
571 ExamSetting::new(),
572 "testing exam setting is empty"
573 );
574 }
575}