use super::models::CategoryScore;
use std::collections::HashMap;
pub const SCORE_DECIMALS: i32 = 6;
#[must_use]
pub fn round_score(value: f64) -> f64 {
if !value.is_finite() {
return value;
}
let factor = 10_f64.powi(SCORE_DECIMALS);
(value * factor).round() / factor
}
#[must_use]
pub fn sorted_categories(
categories: &HashMap<String, CategoryScore>,
) -> Vec<(&String, &CategoryScore)> {
let mut ordered: Vec<(&String, &CategoryScore)> = categories.iter().collect();
ordered.sort_by(|a, b| a.0.cmp(b.0));
ordered
}
fn sorted_sum<F>(categories: &HashMap<String, CategoryScore>, mut value_of: F) -> f64
where
F: FnMut(&CategoryScore) -> Option<f64>,
{
let mut total = 0.0_f64;
for (_, cat) in sorted_categories(categories) {
if let Some(v) = value_of(cat) {
total += v;
}
}
round_score(total)
}
#[must_use]
pub fn total_earned(categories: &HashMap<String, CategoryScore>) -> f64 {
sorted_sum(categories, |cat| Some(cat.earned))
}
#[must_use]
pub fn applicable_earned(categories: &HashMap<String, CategoryScore>) -> f64 {
sorted_sum(categories, |cat| cat.applicable.then_some(cat.earned))
}
#[must_use]
pub fn applicable_possible(categories: &HashMap<String, CategoryScore>) -> f64 {
sorted_sum(categories, |cat| cat.applicable.then_some(cat.max))
}
#[must_use]
pub fn normalized_percentage(categories: &HashMap<String, CategoryScore>) -> f64 {
let applicable: Vec<&CategoryScore> = sorted_categories(categories)
.into_iter()
.map(|(_, cat)| cat)
.filter(|cat| cat.applicable)
.collect();
if applicable.is_empty() {
return 0.0;
}
let mut sum_pcts = 0.0_f64;
for cat in &applicable {
sum_pcts += if cat.max > 0.0 {
(cat.earned / cat.max) * 100.0
} else {
100.0
};
}
round_score(sum_pcts / applicable.len() as f64)
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
fn wobbly_categories() -> HashMap<String, CategoryScore> {
let raw: [(&str, f64, f64); 11] = [
("Build Performance", 4.0, 15.0),
("Code Quality", 7.0, 26.0),
("Dependency Health", 5.0, 12.0),
("Documentation", 11.0, 15.0),
("Formal Verification", 1.0, 16.0),
("GPU/SIMD Quality", 3.0, 10.0),
("Known Defects", 13.0, 20.0),
("Performance & Benchmarking", 7.0, 10.0),
("Reproducibility", 2.0, 15.0),
("Rust Tooling & CI/CD", 91.0, 130.0),
("Testing Excellence", 3.0, 20.0),
];
raw.iter()
.map(|(name, earned, max)| ((*name).to_string(), CategoryScore::new(*earned, *max)))
.collect()
}
#[test]
fn test_round_score_trims_ulp_wobble() {
assert_eq!(
round_score(28.001_373_626_373_628),
round_score(28.001_373_626_373_624)
);
}
#[test]
fn test_round_score_passes_non_finite_through() {
assert!(round_score(f64::NAN).is_nan());
assert_eq!(round_score(f64::INFINITY), f64::INFINITY);
}
#[test]
fn test_sorted_categories_is_alphabetical() {
let cats = wobbly_categories();
let names: Vec<&str> = sorted_categories(&cats)
.into_iter()
.map(|(n, _)| n.as_str())
.collect();
let mut expected = names.clone();
expected.sort_unstable();
assert_eq!(names, expected);
}
#[test]
fn test_normalized_percentage_is_bit_stable_across_25_maps() {
let first = normalized_percentage(&wobbly_categories());
for i in 0..25 {
let again = normalized_percentage(&wobbly_categories());
assert_eq!(
first.to_bits(),
again.to_bits(),
"percentage wobbled on iteration {i}: {first:?} vs {again:?}"
);
}
}
#[test]
fn test_totals_are_bit_stable_across_25_maps() {
let earned = total_earned(&wobbly_categories());
let app_earned = applicable_earned(&wobbly_categories());
let app_possible = applicable_possible(&wobbly_categories());
for i in 0..25 {
let cats = wobbly_categories();
assert_eq!(total_earned(&cats).to_bits(), earned.to_bits(), "iter {i}");
assert_eq!(
applicable_earned(&cats).to_bits(),
app_earned.to_bits(),
"iter {i}"
);
assert_eq!(
applicable_possible(&cats).to_bits(),
app_possible.to_bits(),
"iter {i}"
);
}
}
#[test]
fn test_non_applicable_categories_excluded_from_applicable_totals() {
let mut cats = HashMap::new();
cats.insert("A".to_string(), CategoryScore::new(5.0, 10.0));
cats.insert("B".to_string(), CategoryScore::not_applicable(20.0));
assert_eq!(applicable_earned(&cats), 5.0);
assert_eq!(applicable_possible(&cats), 10.0);
assert_eq!(total_earned(&cats), 5.0);
assert_eq!(normalized_percentage(&cats), 50.0);
}
#[test]
fn test_normalized_percentage_empty_is_zero() {
let cats: HashMap<String, CategoryScore> = HashMap::new();
assert_eq!(normalized_percentage(&cats), 0.0);
}
#[test]
fn test_zero_max_category_counts_as_complete() {
let mut cats = HashMap::new();
cats.insert("Zero".to_string(), CategoryScore::new(0.0, 0.0));
assert_eq!(normalized_percentage(&cats), 100.0);
}
}