code_ranker_graph/
stats.rs1use crate::attrs::{attr_f64, is_external, num_attr};
7use code_ranker_plugin_api::{attrs::AttrValue, graph::Graph};
8use std::collections::BTreeMap;
9
10pub fn compute_stats(graph: &Graph, stat_keys: &[String]) -> BTreeMap<String, AttrValue> {
16 let mut stats = BTreeMap::new();
17 for key in stat_keys {
18 let vals: Vec<f64> = graph
19 .nodes
20 .iter()
21 .filter(|n| !is_external(n))
22 .filter_map(|n| attr_f64(n, key))
23 .filter(|v| v.is_finite() && *v > 0.0)
24 .collect();
25 if vals.is_empty() {
26 continue;
27 }
28 let avg = vals.iter().sum::<f64>() / vals.len() as f64;
29 if avg > 0.0 {
30 stats.insert(key.clone(), num_attr(avg));
31 }
32 }
33 stats
34}
35
36#[cfg(test)]
37mod tests {
38 use super::*;
39 use code_ranker_plugin_api::node::Node;
40
41 fn file(id: &str, cyclomatic: Option<i64>) -> Node {
42 let mut n = Node {
43 id: id.into(),
44 kind: "file".into(),
45 name: id.into(),
46 parent: None,
47 attrs: Default::default(),
48 };
49 if let Some(c) = cyclomatic {
50 n.attrs.insert("cyclomatic".into(), AttrValue::Int(c));
51 }
52 n
53 }
54
55 #[test]
56 fn average_excludes_zero_and_missing() {
57 let g = Graph {
58 nodes: vec![
59 file("a", Some(2)),
60 file("b", Some(4)),
61 file("z", Some(0)),
62 file("n", None),
63 ],
64 edges: vec![],
65 };
66 let s = compute_stats(&g, &["cyclomatic".to_string()]);
67 assert_eq!(s.get("cyclomatic"), Some(&AttrValue::Int(3)));
68 }
69
70 #[test]
71 fn empty_graph_has_no_stats() {
72 let g = Graph::default();
73 assert!(compute_stats(&g, &["cyclomatic".to_string()]).is_empty());
74 }
75}