mc_exam_randomizer/
examreader.rs

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        //bad file
236        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        //bad file
250        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        //bad file
265        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        //bad file
306        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        //bad file
320        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        //bad file
335        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        //bad file
376        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        // no begin doc, no end doc
390        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}