pub const MIN_SECTION_SCORE: f64 = 200.0;
pub const MAX_SECTION_SCORE: f64 = 800.0;
pub const MIN_TOTAL_SCORE: f64 = 400.0;
pub const MAX_TOTAL_SCORE: f64 = 1600.0;
pub const RW_ITEMS: u32 = 54;
pub const MATH_ITEMS: u32 = 44;
#[inline]
fn clamp(v: f64, min: f64, max: f64) -> f64 {
v.max(min).min(max)
}
pub fn section_score(correct: i32, max_items: u32) -> f64 {
let max = max_items as f64;
let c = clamp(correct as f64, 0.0, max);
let span = MAX_SECTION_SCORE - MIN_SECTION_SCORE;
MIN_SECTION_SCORE + span * (c / max)
}
pub fn total_score(rw_correct: i32, rw_max: u32, math_correct: i32, math_max: u32) -> f64 {
let rw = section_score(rw_correct, rw_max);
let math = section_score(math_correct, math_max);
clamp(rw + math, MIN_TOTAL_SCORE, MAX_TOTAL_SCORE)
}
pub fn digital_sat_total(rw_correct: i32, math_correct: i32) -> f64 {
total_score(rw_correct, RW_ITEMS, math_correct, MATH_ITEMS)
}
pub fn accuracy(correct: i32, max_items: u32) -> f64 {
let max = max_items as f64;
clamp(correct as f64, 0.0, max) / max
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_correct_is_floor() {
assert!((section_score(0, RW_ITEMS) - 200.0).abs() < 1e-9);
assert!((section_score(0, MATH_ITEMS) - 200.0).abs() < 1e-9);
}
#[test]
fn perfect_is_ceiling() {
assert!((section_score(54, RW_ITEMS) - 800.0).abs() < 1e-9);
assert!((section_score(44, MATH_ITEMS) - 800.0).abs() < 1e-9);
}
#[test]
fn total_range_is_400_to_1600() {
let floor = digital_sat_total(0, 0);
let ceil = digital_sat_total(RW_ITEMS as i32, MATH_ITEMS as i32);
assert!((floor - 400.0).abs() < 1e-9);
assert!((ceil - 1600.0).abs() < 1e-9);
}
#[test]
fn clamps_negative_and_overflow() {
assert!((section_score(-5, RW_ITEMS) - 200.0).abs() < 1e-9);
assert!((section_score(99, RW_ITEMS) - 800.0).abs() < 1e-9);
}
#[test]
fn halfway_is_midpoint() {
let s = section_score(27, RW_ITEMS);
assert!((s - 500.0).abs() < 1e-6);
}
#[test]
fn accuracy_clamped_and_bounded() {
assert!((accuracy(27, 54) - 0.5).abs() < 1e-9);
assert_eq!(accuracy(0, 54), 0.0);
assert_eq!(accuracy(54, 54), 1.0);
assert_eq!(accuracy(99, 54), 1.0);
}
#[test]
fn typical_score_lands_in_plausible_band() {
let total = digital_sat_total(40, 30);
assert!((total - 1_253.53).abs() < 0.05);
assert!((1000.0..=1500.0).contains(&total));
}
}