whaleprep/tests/grading.rs
dev d3001612b2 PYQ clone: builder, CBT exam screen, grading, analytics dashboard
Axum + SQLite full-stack copy of the ZenithPrep PYQ practice app.
Paper builder with unseen-question exclusion, timed CBT screen with
palette and autosave, +4/-1 grading with numerical tolerance, per-question
analysis, history, dashboard with weak-subtopic tracking and coach notes.
2026-10-11 20:06:26 +05:30

95 lines
No EOL
2.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Grading rules: MCQ comparison, numerical tolerance, and the +4 / −1 scheme.
use std::collections::HashMap;
#[derive(Clone, Debug, PartialEq)]
struct Q {
qtype: &'static str,
answer: String,
}
fn numerical_ok(selected: &str, answer: &str) -> bool {
let s: f64 = match selected.trim().parse() {
Ok(v) => v,
Err(_) => return false,
};
let a: f64 = match answer.trim().parse() {
Ok(v) => v,
Err(_) => return false,
};
let tol = a.abs() * 1e-3 + 1e-6;
(s - a).abs() <= tol
}
fn is_correct(q: &Q, selected: &str) -> bool {
if selected.trim().is_empty() {
return false;
}
if q.qtype == "NUM" {
numerical_ok(selected, &q.answer)
} else {
selected.trim().eq_ignore_ascii_case(q.answer.trim())
}
}
fn marks(q: &Q, selected: &str) -> i32 {
if selected.trim().is_empty() {
0
} else if is_correct(q, selected) {
4
} else {
-1
}
}
#[test]
fn mcq_is_case_insensitive_and_trimmed() {
let q = Q { qtype: "MCQ", answer: "A".into() };
assert!(is_correct(&q, "a"));
assert!(is_correct(&q, " A "));
assert!(!is_correct(&q, "B"));
}
#[test]
fn blank_answer_is_wrong_not_skipped() {
let q = Q { qtype: "MCQ", answer: "A".into() };
assert_eq!(marks(&q, ""), 0, "a blank question scores zero, not minus one");
assert_eq!(marks(&q, " "), 0);
}
#[test]
fn wrong_answer_costs_one_mark() {
let q = Q { qtype: "MCQ", answer: "A".into() };
assert_eq!(marks(&q, "C"), -1);
}
#[test]
fn correct_answer_scores_four() {
let q = Q { qtype: "MCQ", answer: "A".into() };
assert_eq!(marks(&q, "A"), 4);
}
#[test]
fn numerical_answers_allow_small_float_error() {
let q = Q { qtype: "NUM", answer: "20".into() };
assert!(is_correct(&q, "20.0"));
assert!(is_correct(&q, "20.01"), "within 0.1 percent tolerance");
assert!(!is_correct(&q, "20.5"), "outside tolerance");
assert!(!is_correct(&q, "abc"), "non numeric input is wrong");
}
#[test]
fn zero_answer_is_not_mistaken_for_blank() {
let q = Q { qtype: "NUM", answer: "0".into() };
assert!(is_correct(&q, "0"));
assert_eq!(marks(&q, "0"), 4);
}
#[test]
fn mixed_paper_score_matches_hand_calculation() {
let mut answers = HashMap::new();
answers.insert(1i64, ("A".to_string(), 4)); // correct
answers.insert(2i64, ("B".to_string(), -1)); // wrong
answers.insert(3i64, (String::new(), 0)); // blank
let total: i32 = answers.values().map(|(_, m)| m).sum();
assert_eq!(total, 3);
}