morphius 1.0.0

A tool to randomize test generation to eliminate cheating.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
//! # morphius
//!
//! `morphius` allows users to randomize the order and content of documents
//! which can be used by teachers for generating tests with questions in a 
//! different order or with different numbers in each question for each student. If answers
//! are provided, morphius will generate an answer key for each final test to
//! make grading easier.
//! 
//! # Format
//! `morphius` processes content in a specific format described below:
//! 
//! ##### Questions
//! A question in `morphius` is indicated in the following format:
//! 
//! `"|<q>Question Content</q>|"` The content inside the `"|<q>"` and the `"</q>|"` is
//! the question content. The questions 
//! will be rearranged if desired to create a different order of questions on different 
//! tests while the rest of the content in the template remains in the same place. This means
//! that if question numbers are included in the template, those should be placed outside
//! of the question itself in order to retain the correct numbering when questions are 
//! rearranged.
//! 
//! ##### Expressions
//! Expressions are used to add randomness to questions:
//! 
//! `|<e>a+b</e>|` is an example expression. Expressions must be placed inside of a question
//! in order to be processed. Expressions are a mathematical expression that can include variables and 
//! evaluates to a number when generating a test. Variables represent a random number that will be selected
//! separately for each test generated. A variable in an expression is any identifier that starts with 
//! a letter followed by a sequence of characters that can contain letters, numbers or underscores.
//! Variables do not have to be declared and will default to be an integer between 0 and 99 when generated.
//! Math is allowed in these expressions in order to create specific relationships between numbers in the generated
//! question. Math is supported using the `mexprp` crate. The scope of variables is the question, so you can have
//! the same variable names in different questions and they will likely have different values (unless they randomly
//! end up being the same). If you want more fine tuned control of the range of possible values, you can declare
//! the variable.
//! 
//! ##### Variable Declarations
//! A variable can be declared anywhere in the question in the following format:
//! 
//! `|<v>var_name: type = [min,max]</v>|` where var_name is the name of your variable, type is either int or real, and min and max
//! are integers representing the lower and upper bounds respectively of the value of your variable. An example declaration would be
//! `|<v>a: int = [0,99]</v>|`. This is the declaration assumed for any variable without a declaration, so including this exact
//! declaration in your code would be unecessary.
//! 
//! ##### Answers
//! 
//! Answers are used to generate an answer key for each test. Answers should be included for every question when using
//! `process_with_answers`. They should be in the format `|<a>Answer</a>|` and should appear right after the question. Variables
//! in answers have the same scope as their corresponding question so you can use expressions in your answers to calculate the
//! answer in terms of the randomly generated variables in your question.
//! 
//! 
//! # Examples
//! Here is a simple example, you can find more example templates in the `examples` folder of the GitHub repository.
//! ```
//! let template = "
//! |<q>This is a single question test. You must calculate the sum of two random numbers.
//! What is |<e>a</e>| + |<e>b</e>|?</q>|
//! |<a>The answer to this question is: |<e>a+b</e>|</a>|";
//! 
//! let doc = morphius::process_with_answers(template);
//! let tests = morphius::generate(&doc, 5, Some(1));
//! 
//! //Prints out the first test
//! println!("{}", tests[0].content);
//! 
//! //Prints out the answer key for the first test
//! println!("{}", tests[0].answers);
//! 
//! ```
//! 
//! 
use lazy_static::lazy_static;
use regex::Regex;
use itertools::Itertools;
use rand::Rng;
use std::cmp;
use std::collections::{HashSet, HashMap};

/// A Document is a template to be used to generate filled out tests
pub struct Document {
    ///This is a list of the questions in the Document, in the order provided
    pub questions: Vec<Question>,
    ///This is a list of the other content in the Document that should stay in the same place when the questions move
    pub layout: Vec<String>
}

///A Test is generated from a Document and is ready for use
pub struct Test {
    ///A String representing the contents of the Test
    pub content: String,
    ///A String representing the answers of a Test
    pub answers: String
}

///A Question is an object representing a question
pub struct Question {
    ///This is a list of variables used in the question
    pub vars: HashSet<Var>,
    ///This is a list of expressions that need to be evaluated when generating the question text
    pub expressions: Vec<Expression>,
    ///This is a list of the other content in the question that does not need to be evaluated
    pub layout: Vec<String>,
    ///This is either the Answer to the question, if provided or None
    pub answer: Option<Answer>
}

///An Answer is the answer to a question. It is processed very similarly, it just uses the same scope as its parent question
pub struct Answer {
    ///This is a list of expressions that need to be evaluated using the same variable values as its parent question
    pub expressions: Vec<Expression>,
    ///This is a list of the content in the Answer that doesn't need to be evaluated
    pub layout: Vec<String>
}

struct Content {
    vars: HashSet<Var>,
    expressions: Vec<Expression>,
    layout: Vec<String>
}

///An Expression represents a mathematical expression to be evaluated
pub struct Expression {
    ///This is a list of variables/other content that makes up the expression
    pub expression: Vec<ExpComp>
}

#[derive(PartialEq, Eq, Hash)]
///A Var holds information about a variable that is used to generate final values
pub struct Var {
    ///The variable name
    pub name: String,
    ///The type of the variable: either int or real
    pub num_type: String,
    ///The minimum value for this variable
    pub min: String,
    ///The maximum value for this variable
    pub max: String
}

///This is an enum used to differentiate between variable names and other content of an expression
pub enum ExpComp {
    ///This denotes a variable name
    Var(String),
    ///This denotes everything other than variable names
    Other(String)
}

#[derive(PartialEq, Eq, Hash)]
struct Num {
    whole: i64,
    frac: Option<i64>
}





///This function takes an input &str in the desired template format and generates a document. If the document has answers you should use process_with_answers.
///
/// # Arguments
///
/// * `input` - A string slice that holds the template contents for the Document
///
/// # Examples
///
/// ```
/// use morphius;
/// let doc = morphius::process("Document Contents");
/// ```
pub fn process(input: &str) -> Document {
    lazy_static! {
        static ref QUESTION: Regex = Regex::new(r"(?s)\|<q>(.*?)</q>\|").unwrap();
    }
    let questions: Vec<Question> = QUESTION.captures_iter(input).map(|cap| process_question(&cap[1], None)).collect();
    let layout: Vec<String> = QUESTION.split(input).map(String::from).collect();
    Document{ questions, layout }
}

///This function takes an input &str in the desired template format and generates a document. The input document must have answers provided for each question.
///
/// # Arguments
///
/// * `input` - A string slice that holds the template contents for the Document
///
/// # Examples
///
/// ```
/// use morphius;
/// let doc = morphius::process_with_answers("Document Contents with answers");
/// ```
pub fn process_with_answers(input: &str) -> Document {
    lazy_static! {
        static ref QUESTION: Regex = Regex::new(r"(?s)\|<q>(.*?)</q>\|\s*\|<a>(.*?)</a>\|").unwrap();
    }
    let mut questions: Vec<Question> = Vec::new();
    for cap in QUESTION.captures_iter(input) {
        questions.push(process_question(&cap[1], Some(process_answer(&cap[2]))));
    }
    let layout: Vec<String> = QUESTION.split(input).map(String::from).collect();
    Document{ questions, layout }
}

///This function takes an input Document, the number of tests that you want to generate and optionally the number of question per generated test
///
/// # Arguments
///
/// * `doc` - A reference to a Document for the template that you want to generate
/// * `num_results` - The number of tests to generate
/// * `num_quesitions` - The number of questions per test. Enter None to use all questions in the original order. To include all questions and reorder them, enter `Some(x)` where x is the total number of questions
///
/// # Examples
///
/// ```
/// use morphius;
/// let doc = morphius::process("|<q>Example Question 1</q>||<q>Example Question 2</q>|");
/// morphius::generate(&doc, 5, Some(2));
/// ```
pub fn generate(doc: &Document, num_results: usize, num_questions: Option<usize>) -> Vec<Test> {
    match num_questions {
        Some(num_qs) => {
            let mut rng = rand::thread_rng();
            let tot_qs_in_doc = doc.questions.len();
            let num_permutations = cmp::min(num_qs, tot_qs_in_doc);
            let permutations: Vec<Vec<usize>> = (0..tot_qs_in_doc).permutations(num_permutations).collect();

            (0..num_results).map(|_| gen_form(doc, Some(&permutations[rng.gen_range(0..num_permutations)]))).collect()
        }
        None => (0..num_results).map(|_| gen_form(doc, None)).collect()
    }
}

fn gen_form(doc: &Document, order: Option<&Vec<usize>>) -> Test {
    let mut questions: Vec<String> = Vec::new();
    let mut answers: Vec<String> = Vec::new();
    match order {
        Some(ord) => {
            for i in ord.iter() {
                let (content, answer) = gen_question_text(&doc.questions[*i]);
                questions.push(content);
                answers.push(answer);
            }
        },
        None => {
            for q in doc.questions.iter() {
                let (content, answer) = gen_question_text(&q);
                questions.push(content);
                answers.push(answer);
                ()
            }
        }
    };
    Test { content: doc.layout.iter().interleave(&questions).join(""), answers: doc.layout.iter().interleave(&answers).join("") }
}

fn gen_question_text(question: &Question) -> (String, String) {
    let mut rng = rand::thread_rng();
    let mut scope:HashMap<&str,Num> = HashMap::new();
    for var in question.vars.iter() {
        if var.num_type == "int" {
            scope.insert(&var.name[..], Num{ whole: rng.gen_range(var.min.parse::<i64>().unwrap()..(var.max.parse::<i64>().unwrap()+1)), frac: None});
        } else {
            let whole = rng.gen_range(var.min.parse::<i64>().unwrap()..var.max.parse::<i64>().unwrap());
            let frac: i64 = rng.gen_range(0..1000);
            scope.insert(&var.name[..], Num{ whole, frac: Some(frac) });
        }
    }


    let content = question.layout.iter().interleave(&question.expressions.iter().map(|exp| gen_expression_text(exp, &scope)).collect::<Vec<String>>()).join("");

    let answer: String = match &question.answer {
        Some(answer) => answer.layout.iter().interleave(&answer.expressions.iter().map(|exp| gen_expression_text(exp, &scope)).collect::<Vec<String>>()).join(""),
        None => String::from("No Answers Provided")
    };

    (content, answer)
}

fn gen_expression_text(expression: &Expression, scope: &HashMap<&str,Num>) -> String {
    let expr = expression.expression.iter().map(|exp_cmp| {
        match exp_cmp {
            ExpComp::Var(var_name) => {
                let num = scope.get(&var_name[..]).unwrap();
                match &num.frac {
                    None => num.whole.to_string(),
                    Some(frac) => (num.whole as f64 + (*frac as f64 / 1000f64)).to_string()
                }
            }
            ExpComp::Other(text) => text.clone()
        }
    })
    .join("");
    match mexprp::eval::<f64>(&expr).unwrap() {
        mexprp::Answer::Single(num) => {
            let rounded = format!("{:.3}", num);
            let normal = num.to_string();
            if normal.chars().count() > rounded.chars().count()  {
                rounded
            } else {
                normal
            }
        }
        mexprp::Answer::Multiple(_) => panic!("Unsupported math")
    }
}

fn process_question(question: &str, answer: Option<Answer>) -> Question {
    lazy_static! {
        static ref VAR: Regex = Regex::new(r"\|<v>([[:alpha:]][[:word:]]*):\s*([[:alpha:]]*)\s*=\s*\[(-?[0-9]+),(-?[0-9]+)\]</v>\|").unwrap();
    }
    let mut content = get_content(&VAR.split(question).join(""));
    for cap in VAR.captures_iter(question) {
        content.vars.remove(&Var{ name: String::from(&cap[1]), num_type: String::from("int"), min: String::from("0"), max: String::from("99") });
        content.vars.insert(Var{ name: String::from(&cap[1]), num_type: String::from(&cap[2]), min: String::from(&cap[3]), max: String::from(&cap[4])});
    }
    Question { vars: content.vars, expressions: content.expressions, layout: content.layout, answer }
}

fn process_answer(answer: &str) -> Answer {
    let content = get_content(answer);
    Answer { expressions: content.expressions, layout: content.layout }
}

fn get_content(text: &str) -> Content {
    lazy_static! {
        static ref EXP: Regex = Regex::new(r"\|<e>(.*?)</e>\|").unwrap();
    }
    let mut vars: HashSet<Var> = HashSet::new();
    let expressions: Vec<Expression> = EXP.captures_iter(text).map(|cap| process_expression(&cap[1], &mut vars)).collect();
    let layout: Vec<String> = EXP.split(text).map(String::from).collect();
    Content{ vars, expressions, layout }
}

fn process_expression(expression: &str, vars: &mut HashSet<Var>) -> Expression {
    lazy_static! {
        static ref VAR: Regex = Regex::new(r"[[:alpha:]][[:word:]]*").unwrap();
    }
    let mut vars_list: Vec<ExpComp> = Vec::new();
    for cap in VAR.captures_iter(expression) {
        vars.insert(Var{ name: String::from(&cap[0]), num_type: String::from("int"), min: String::from("0"), max: String::from("99") });
        vars_list.push(ExpComp::Var(String::from(&cap[0])));
    }
    Expression { expression: VAR.split(expression).map(|text| ExpComp::Other(String::from(text))).interleave(vars_list).collect() }
}

#[cfg(test)]
mod tests {
    use super::*;

    const FORM1: &str = "Beginning|<q>Question 1</q>|Middle|<q>Question 2</q>|End";
    const FORM2: &str = "|<q>1</q>|Middle 1|<q>2</q>|Middle 2|<q>3</q>|";
    const FORM3: &str = "|<q>1</q>||<q>2</q>||<q>3</q>|";
    const FORM4: &str = "|<q>1</q>||<a>3</a>|";
    const FORM5: &str = "|<q>1</q>|\n\n          \t\t|<a>3</a>|";


    #[test]
    fn test_process_1() {
        let doc = process(FORM1);
        assert_eq!(doc.layout[0], "Beginning");
        assert_eq!(doc.layout[1], "Middle");
        assert_eq!(doc.layout[2], "End");
        assert_eq!(gen_question_text(&doc.questions[0]).0, "Question 1");
        assert_eq!(gen_question_text(&doc.questions[1]).0, "Question 2");
    }

    #[test]
    fn test_process_2() {
        let doc = process(FORM2);
        assert_eq!(doc.layout, vec!["","Middle 1", "Middle 2",""]);
        assert_eq!(gen_question_text(&doc.questions[0]).0, "1");
        assert_eq!(gen_question_text(&doc.questions[1]).0, "2");
        assert_eq!(gen_question_text(&doc.questions[2]).0, "3");
    }

    #[test]
    fn test_gen_form_original_order() {
        let doc = process(FORM1);
        assert_eq!(gen_form(&doc, None).content, "BeginningQuestion 1MiddleQuestion 2End");
    }

    #[test]
    fn test_gen_form_different_order() {
        let doc = process(FORM2);
        assert_eq!(gen_form(&doc, Some(&vec![1,2,0])).content, "2Middle 13Middle 21");
        assert_eq!(gen_form(&doc, Some(&vec![2,1,0])).content, "3Middle 12Middle 21");
        assert_eq!(gen_form(&doc, Some(&vec![0,1,2])).content, "1Middle 12Middle 23");
    }

    #[test]
    fn test_generate_no_reorder() {
        let doc = process(FORM3);
        assert_eq!(generate(&doc, 2, None)[0].content, "123");
        assert_eq!(generate(&doc, 2, None)[1].content, "123");
    }

    #[test]
    fn test_generate_reorder() {
        let doc = process(FORM3);
        let results = generate(&doc, 3, Some(3));
        for result in results {
            assert!(result.content.contains("1") && result.content.contains("2") && result.content.contains("3"));
        }
    }

    #[test]
    fn test_generate_skip_questions() {
        let doc = process(FORM3);
        let results = generate(&doc, 3, Some(1));
        for result in results {
            assert!(!result.content.contains("1") || !result.content.contains("2") || !result.content.contains("3"));
        }
    }

    #[test]
    fn test_generate_var() {
        let doc = process("|<q>|<e>a</e>|</q>|");
        let result = generate(&doc, 3, Some(1));
        let num_re = Regex::new(r"^[[:digit:]]+$").unwrap();
        assert!(num_re.is_match(&result[0].content));
    }

    #[test]
    fn test_generate_var_math() {
        let doc = process("|<q>|<e>(a+b)-c</e>|</q>|");
        let result = generate(&doc, 3, Some(1));
        let num_re = Regex::new(r"^-?[[:digit:]]+$").unwrap();
        println!("{}", result[0].content);
        assert!(num_re.is_match(&result[0].content));
    }

    #[test]
    fn test_process_with_anwer() {
        let doc1 = process_with_answers(FORM4);
        match doc1.questions[0].answer {
            Some(_) => assert!(true),
            None => assert!(false)
        }

        let doc2 = process(FORM4);
        match doc2.questions[0].answer {
            Some(_) => assert!(false),
            None => assert!(true)
        }
    }

    #[test]
    fn test_process_with_anwer_newlines_are_ok() {
        let doc = process_with_answers(FORM5);
        match doc.questions[0].answer {
            Some(_) => assert!(true),
            None => assert!(false)
        }
    }

    #[test]
    fn test_answer_generated_correctly() {
        let doc = process_with_answers("|<q>|<e>a</e>|</q>||<a>|<e>a</e>|</a>|");
        for result in generate(&doc, 3, Some(1)) {
            assert_eq!(result.content, result.answers);
        }
    }

    #[test]
    fn test_var_bounds_are_processed() {
        let doc = process("|<q>|<v>x: real = [5,55]</v>||<e>x/x</e>|</q>|");
        for result in generate(&doc, 3, Some(1)) {
            assert!(result.content == "1");
        }
    }

    #[test]
    fn test_numerical_rounding_to_three_decimal_places() {
        let doc = process("|<q>|<e>1/3</e>|</q>|");
        for result in generate(&doc, 3, Some(1)) {
            assert_eq!("0.333", result.content);
        }
    }

}