use super::league_stats::StatDistribution; use super::weights::StatWeight; /// Convert a raw stat value to a standardized score (-3 to +3). /// /// Uses standard deviation thresholds from the league mean. /// A value of 0 always returns +3 (best possible). pub fn standardize_value(value: f64, distribution: &StatDistribution, high_is_better: bool) -> i32 { if value == 0.0 { return 3; } let avg = distribution.avg; let stdev = distribution.stdev; let base_score = if value > avg + 2.0 * stdev { -3 } else if value > avg + stdev { -2 } else if value > avg + 0.33 * stdev { -1 } else if value > avg - 0.33 * stdev { 0 } else if value > avg - stdev { 1 } else if value > avg - 2.0 * stdev { 2 } else { 3 }; if high_is_better { -base_score } else { base_score } } /// Calculate weighted score for a single stat. pub fn calculate_weighted_score( value: f64, distribution: &StatDistribution, stat_weight: &StatWeight, ) -> f64 { let std_score = standardize_value(value, distribution, stat_weight.high_is_better); std_score as f64 * stat_weight.weight as f64 } #[cfg(test)] mod tests { use super::*; fn dist(avg: f64, stdev: f64) -> StatDistribution { StatDistribution { avg, stdev } } // --- standardize_value tests --- #[test] fn zero_value_always_returns_3() { let d = dist(5.0, 1.0); assert_eq!(standardize_value(0.0, &d, true), 3); assert_eq!(standardize_value(0.0, &d, false), 3); } #[test] fn value_far_above_mean_is_minus3_base() { // value > avg + 2*stdev => base_score = -3 let d = dist(5.0, 1.0); // high_is_better=true: -(-3) = 3 assert_eq!(standardize_value(7.5, &d, true), 3); // high_is_better=false: -3 assert_eq!(standardize_value(7.5, &d, false), -3); } #[test] fn value_far_below_mean_is_plus3_base() { // value <= avg - 2*stdev => base_score = 3 let d = dist(5.0, 1.0); // high_is_better=true: -(3) = -3 assert_eq!(standardize_value(2.5, &d, true), -3); // high_is_better=false: 3 assert_eq!(standardize_value(2.5, &d, false), 3); } #[test] fn value_at_mean_returns_zero_base() { // avg - 0.33*stdev < value < avg + 0.33*stdev => base_score = 0 let d = dist(5.0, 1.0); assert_eq!(standardize_value(5.0, &d, true), 0); assert_eq!(standardize_value(5.0, &d, false), 0); } #[test] fn score_ranges_are_symmetric() { // Walk through all 7 buckets with a clean distribution let d = dist(10.0, 2.0); // base_score mapping (raw, not flipped): // > 14.0 (avg+2*std) => -3 // > 12.0 (avg+std) => -2 // > 10.66 (avg+0.33*std) => -1 // > 9.34 (avg-0.33*std) => 0 // > 8.0 (avg-std) => 1 // > 6.0 (avg-2*std) => 2 // <= 6.0 => 3 // Test with high_is_better=false (no flip) assert_eq!(standardize_value(15.0, &d, false), -3); assert_eq!(standardize_value(13.0, &d, false), -2); assert_eq!(standardize_value(11.0, &d, false), -1); assert_eq!(standardize_value(10.0, &d, false), 0); assert_eq!(standardize_value(9.0, &d, false), 1); assert_eq!(standardize_value(7.0, &d, false), 2); assert_eq!(standardize_value(5.0, &d, false), 3); } // --- calculate_weighted_score tests --- #[test] fn weighted_score_multiplies_std_by_weight() { let d = dist(10.0, 2.0); let w = StatWeight { weight: 5, high_is_better: true }; // value=15.0 => base=-3, flipped=3 => 3*5=15.0 assert!((calculate_weighted_score(15.0, &d, &w) - 15.0).abs() < f64::EPSILON); } #[test] fn weighted_score_zero_value() { let d = dist(10.0, 2.0); let w = StatWeight { weight: 3, high_is_better: false }; // value=0.0 => always 3 => 3*3=9.0 assert!((calculate_weighted_score(0.0, &d, &w) - 9.0).abs() < f64::EPSILON); } }