~fkfd/hamrs

e8a613679811d182eceea96c363d2b8b028d6f23 — Frederick Yin 4 months ago 461bf66
Exam mode but there's a lot of other changes as well it's a mess
4 files changed, 137 insertions(+), 19 deletions(-)

M src/main.rs
M src/poolfile.rs
M src/quiz.rs
M src/util.rs
M src/main.rs => src/main.rs +9 -3
@@ 16,6 16,8 @@ struct Args {
    log: String,
    #[arg(short, long)]
    random: bool,
    #[arg(short = 'x', long)]
    exam: bool,
}

fn main() {


@@ 24,16 26,20 @@ fn main() {
    if pool.is_none() {
        panic!("Failed to open and parse {}", args.pool);
    }
    let pool = pool.unwrap();

    let log = read_logfile(&args.log);
    if log.is_none() {
        panic!("Failed to open and parse {}", args.log);
    }
    let mut log = log.unwrap();

    if args.random {
        quiz::begin_random(&pool.unwrap(), &mut log);
    if args.exam {
        quiz::begin_exam(&pool, &mut log);
    } else if args.random {
        quiz::begin_random(&pool, &mut log);
    } else {
        quiz::begin_sequential(&pool.unwrap(), &mut log);
        quiz::begin_sequential(&pool, &mut log);
    }
    write_logfile(&args.log, &log).expect("Failed to write logfile");
}

M src/poolfile.rs => src/poolfile.rs +38 -3
@@ 1,3 1,4 @@
use std::collections::HashMap;
use std::fs;

use json::JsonValue;


@@ 5,6 6,7 @@ use rand::prelude::*;

use crate::util::*;

#[derive(Clone, Debug)]
pub struct Question {
    pub id: String,
    pub question: String,


@@ 70,14 72,47 @@ pub fn correct_answer(question: &Question) -> &str {
pub fn shuffle_options(question: &Question) -> Question {
    // map ABCD to a random permutation
    let mut rng = thread_rng();
    let mut mapping: Vec<usize> = vec![0, 1, 2, 3];
    let mut mapping = [0, 1, 2, 3];
    mapping.shuffle(&mut rng);
    let mut inverse_mapping = [0, 0, 0, 0];
    for (i, n) in mapping.iter().enumerate() {
        inverse_mapping[*n] = i;
    }

    let options = vec!['A', 'B', 'C', 'D'];
    let letters = ['A', 'B', 'C', 'D'];
    Question {
        id: question.id.clone(),
        question: question.question.clone(),
        options: Vec::from_iter(mapping.iter().map(|i| question.options[*i].clone())),
        correct: options[mapping[abcd_to_1234(question.correct)]],
        correct: letters[inverse_mapping[abcd_to_0123(question.correct)]],
    }
}

// create an exam pool containing one question from each category of the complete pool
pub fn exam_pool_from(pool: &Pool) -> Pool {
    // put question indices into categories
    let mut categories: HashMap<&str, Vec<usize>> = HashMap::new();
    for (i, question) in pool.iter().enumerate() {
        let cat = category(question);
        match categories.get_mut(cat) {
            Some(vec) => {
                vec.push(i);
            }
            None => {
                categories.insert(cat, vec![i]);
            }
        }
    }
    // draw one random question from each category
    let mut exam_pool = Pool::with_capacity(categories.len());
    let mut rng = thread_rng();
    for (_, indices) in categories.iter() {
        let i = indices.choose(&mut rng).unwrap();
        exam_pool.push(shuffle_options(&pool[*i]));
    }
    exam_pool
}

fn category(question: &Question) -> &str {
    &question.id[..3]
}

M src/quiz.rs => src/quiz.rs +89 -12
@@ 8,15 8,18 @@ use crate::logfile::*;
use crate::poolfile::*;
use crate::util::*;

#[derive(Debug)]
pub struct Answer {
    pub correct: bool,
    pub choice: char,
    pub quit: bool,
}

pub fn begin_sequential(pool: &Pool, log: &mut Log) -> () {
    let progress = log.progress;
    for (idx, question) in pool[progress..].iter().enumerate() {
        let answer = ask_question(&question);
        let question = shuffle_options(&question);
        let answer = ask_question(&question, false);
        if answer.quit {
            // log progress before quitting
            log.progress = progress + idx;


@@ 29,8 32,8 @@ pub fn begin_sequential(pool: &Pool, log: &mut Log) -> () {
pub fn begin_random(pool: &Pool, log: &mut Log) -> () {
    loop {
        let idx = random::<usize>() % pool.len();
        let question = &pool[idx];
        let answer = ask_question(&question);
        let question = shuffle_options(&pool[idx]);
        let answer = ask_question(&question, false);
        if answer.quit {
            break;
        }


@@ 38,9 41,76 @@ pub fn begin_random(pool: &Pool, log: &mut Log) -> () {
    }
}

fn ask_question(question: &Question) -> Answer {
    let question = shuffle_options(&question);
pub fn begin_exam(pool: &Pool, log: &mut Log) -> () {
    let exam_pool = exam_pool_from(pool);
    // indices of and user's incorrect answers to questions
    let mut missed: Vec<(usize, Answer)> = Vec::new();
    let mut quit = false;

    for (i, question) in exam_pool.iter().enumerate() {
        let answer = ask_question(&question, true);
        if answer.quit {
            quit = true;
            break;
        }
        log_attempt(log, &question.id, answer.correct);
        if !answer.correct {
            missed.push((i, answer));
        }
    }

    if quit {
        return;
    }

    // calculate statistics
    let hits = exam_pool.len() - missed.len();
    println!("{GREEN}Your grade: {}/{}{RESET}", hits, exam_pool.len());
    if missed.is_empty() {
        return;
    }

    // ask user if they want to review incorrect questions
    let mut input = String::new();
    let mut review = true;
    loop {
        input.clear();
        print!("Review incorrect questions? Y/n ");
        let _ = io::stdout().flush();
        if io::stdin().read_line(&mut input).is_err() {
            println!("{RED}Failed to read line, try again.{RESET}\n");
            continue;
        }
        let answer = input.trim().to_ascii_uppercase();
        if answer == "Y" || answer.is_empty() {
            break;
        } else if input == "N" {
            review = false;
            break;
        } else {
            println!("{RED}Invalid input, try again.{RESET}");
            continue;
        }
    }

    if review {
        for (i, answer) in missed {
            let question = &exam_pool[i];
            print_wrap("Q: ", &question.question);
            let user_option = &question.options[abcd_to_0123(answer.choice)];
            let correct_option = &correct_answer(question);
            println!("You chose:{RED}");
            print_wrap(&format!("{}. ", answer.choice), user_option);
            println!("{RESET}Correct answer is:{GREEN}");
            print_wrap(&format!("{}. ", question.correct), correct_option);
            println!("\n{RED}Press Enter to continue{RESET}\n");
            io::stdin().read_line(&mut input).unwrap();
        }
    }
}

// print question and options, read user input, then give feedback unless in exam mode
fn ask_question(question: &Question, exam_mode: bool) -> Answer {
    // print a solid row of ========
    let equal_signs = iter::repeat('=').take(MAX_COLUMNS);
    let divider = String::from_iter(equal_signs);


@@ 60,7 130,7 @@ fn ask_question(question: &Question) -> Answer {
        input.clear();
        print!("{BOLD}You choose: {RESET}");
        let _ = io::stdout().flush();
        while io::stdin().read_line(&mut input).is_err() {
        if io::stdin().read_line(&mut input).is_err() {
            println!("{RED}Failed to read line, try again.{RESET}\n");
            continue;
        }


@@ 77,6 147,7 @@ fn ask_question(question: &Question) -> Answer {
            println!("{YELLOW}73{RESET}");
            return Answer {
                correct: false,
                choice: '\0',
                quit: true,
            };
        }


@@ 95,19 166,25 @@ fn ask_question(question: &Question) -> Answer {

        let answer = answer.chars().next().unwrap();
        if answer == question.correct {
            println!("{GREEN}Correct!{RESET}\n");
            if !exam_mode {
                println!("{GREEN}Correct!{RESET}\n");
            }
            return Answer {
                correct: true,
                choice: answer,
                quit: false,
            };
        } else if is_in_abcd(answer) {
            println!("{RED}Incorrect - the correct answer is:{BLUE}");
            let heading = format!("{}. ", question.correct);
            print_wrap(&heading, correct_answer(&question));
            println!("{RED}Press Enter to continue{RESET}");
            io::stdin().read_line(&mut input).unwrap();
            if !exam_mode {
                println!("{RED}Incorrect - the correct answer is:{BLUE}");
                let heading = format!("{}. ", question.correct);
                print_wrap(&heading, correct_answer(&question));
                println!("{RED}Press Enter to continue{RESET}");
                io::stdin().read_line(&mut input).unwrap();
            }
            return Answer {
                correct: false,
                choice: answer,
                quit: false,
            };
        } else {

M src/util.rs => src/util.rs +1 -1
@@ 11,7 11,7 @@ pub fn is_in_abcd(answer: char) -> bool {
    answer == 'A' || answer == 'B' || answer == 'C' || answer == 'D'
}

pub fn abcd_to_1234(abcd: char) -> usize {
pub fn abcd_to_0123(abcd: char) -> usize {
    match abcd {
        'A' => 0,
        'B' => 1,