1use std::collections::HashMap;
6
7use crate::algorithm::{DirectedGraph, NodeId, UndirectedGraph};
8
9#[derive(Debug, Clone, serde::Serialize)]
11pub struct GraphStats {
12 pub node_count: usize,
13 pub edge_count: usize,
14 pub density: f64,
15 pub avg_degree: f64,
16 pub max_degree: usize,
17 pub min_degree: usize,
18 pub is_connected: bool,
19 pub component_count: usize,
20 pub has_cycle: bool,
21 pub largest_component_size: usize,
22}
23
24#[derive(Debug, Clone, serde::Serialize)]
26pub struct DegreeDistribution {
27 pub degree: usize,
28 pub count: usize,
29 pub fraction: f64,
30}
31
32pub struct GraphStatsCalculator;
34
35impl GraphStatsCalculator {
36 pub fn directed(graph: &DirectedGraph) -> GraphStats {
38 let node_count = graph.node_count();
39 let edge_count = graph.edge_count();
40 let components = graph.connected_components();
41 let component_count = components.len();
42 let largest_component_size = components.iter().map(|c| c.len()).max().unwrap_or(0);
43 let mut degrees: Vec<usize> = graph
44 .nodes()
45 .map(|n| graph.in_degree(n) + graph.out_degree(n))
46 .collect();
47 degrees.sort_unstable();
48 let max_degree = degrees.last().copied().unwrap_or(0);
49 let min_degree = degrees.first().copied().unwrap_or(0);
50 let avg_degree = if node_count > 0 {
51 degrees.iter().sum::<usize>() as f64 / node_count as f64
52 } else {
53 0.0
54 };
55 let density = if node_count > 1 {
56 edge_count as f64 / (node_count * (node_count - 1)) as f64
57 } else {
58 0.0
59 };
60 GraphStats {
61 node_count,
62 edge_count,
63 density,
64 avg_degree,
65 max_degree,
66 min_degree,
67 is_connected: component_count <= 1,
68 component_count,
69 has_cycle: graph.has_cycle(),
70 largest_component_size,
71 }
72 }
73
74 pub fn undirected(graph: &UndirectedGraph) -> GraphStats {
76 let node_count = graph.node_count();
77 let edge_count = graph.edge_count();
78 let components = graph.connected_components();
79 let component_count = components.len();
80 let largest_component_size = components.iter().map(|c| c.len()).max().unwrap_or(0);
81 let mut degrees: Vec<usize> = (0..node_count as NodeId).map(|n| graph.degree(n)).collect();
82 degrees.sort_unstable();
83 let max_degree = degrees.last().copied().unwrap_or(0);
84 let min_degree = degrees.first().copied().unwrap_or(0);
85 let avg_degree = if node_count > 0 {
86 degrees.iter().sum::<usize>() as f64 / node_count as f64
87 } else {
88 0.0
89 };
90 let density = if node_count > 1 {
91 2.0 * edge_count as f64 / (node_count * (node_count - 1)) as f64
92 } else {
93 0.0
94 };
95 GraphStats {
96 node_count,
97 edge_count,
98 density,
99 avg_degree,
100 max_degree,
101 min_degree,
102 is_connected: component_count <= 1,
103 component_count,
104 has_cycle: false,
105 largest_component_size,
106 }
107 }
108
109 pub fn degree_distribution_directed(graph: &DirectedGraph) -> Vec<DegreeDistribution> {
111 let mut degree_counts: HashMap<usize, usize> = HashMap::new();
112 let node_count = graph.node_count();
113 for node in graph.nodes() {
114 let degree = graph.in_degree(node) + graph.out_degree(node);
115 *degree_counts.entry(degree).or_insert(0) += 1;
116 }
117 let mut dist: Vec<DegreeDistribution> = degree_counts
118 .into_iter()
119 .map(|(degree, count)| DegreeDistribution {
120 degree,
121 count,
122 fraction: if node_count > 0 {
123 count as f64 / node_count as f64
124 } else {
125 0.0
126 },
127 })
128 .collect();
129 dist.sort_by_key(|d| d.degree);
130 dist
131 }
132
133 pub fn degree_distribution_undirected(graph: &UndirectedGraph) -> Vec<DegreeDistribution> {
135 let mut degree_counts: HashMap<usize, usize> = HashMap::new();
136 let node_count = graph.node_count();
137 for node in 0..node_count as NodeId {
138 let degree = graph.degree(node);
139 *degree_counts.entry(degree).or_insert(0) += 1;
140 }
141 let mut dist: Vec<DegreeDistribution> = degree_counts
142 .into_iter()
143 .map(|(degree, count)| DegreeDistribution {
144 degree,
145 count,
146 fraction: if node_count > 0 {
147 count as f64 / node_count as f64
148 } else {
149 0.0
150 },
151 })
152 .collect();
153 dist.sort_by_key(|d| d.degree);
154 dist
155 }
156}
157
158impl GraphStats {
159 pub fn to_json(&self) -> serde_json::Value {
160 serde_json::to_value(self).unwrap_or(serde_json::Value::Null)
161 }
162
163 pub fn to_summary(&self) -> String {
164 format!(
165 "Graph: {} nodes, {} edges, density={:.4}, {} components, cycle={}",
166 self.node_count, self.edge_count, self.density, self.component_count, self.has_cycle
167 )
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn test_graph_stats_empty() {
177 let g = DirectedGraph::new();
178 let stats = GraphStatsCalculator::directed(&g);
179 assert_eq!(stats.node_count, 0);
180 assert_eq!(stats.edge_count, 0);
181 }
182
183 #[test]
184 fn test_graph_stats_single_node() {
185 let mut g = DirectedGraph::new();
186 g.add_node(1);
187 let stats = GraphStatsCalculator::directed(&g);
188 assert_eq!(stats.node_count, 1);
189 assert_eq!(stats.edge_count, 0);
190 assert!(stats.is_connected);
191 }
192
193 #[test]
194 fn test_graph_stats_simple() {
195 let mut g = DirectedGraph::new();
196 g.add_edge_unweighted(1, 2);
197 g.add_edge_unweighted(2, 3);
198 let stats = GraphStatsCalculator::directed(&g);
199 assert_eq!(stats.node_count, 3);
200 assert_eq!(stats.edge_count, 2);
201 assert!(stats.is_connected);
202 assert!(!stats.has_cycle);
203 }
204
205 #[test]
206 fn test_graph_stats_density() {
207 let mut g = DirectedGraph::new();
208 g.add_edge_unweighted(1, 2);
209 g.add_edge_unweighted(2, 1);
210 let stats = GraphStatsCalculator::directed(&g);
211 assert!((stats.density - 1.0).abs() < 0.001);
212 }
213
214 #[test]
215 fn test_graph_stats_disconnected() {
216 let mut g = DirectedGraph::new();
217 g.add_edge_unweighted(1, 2);
218 g.add_edge_unweighted(3, 4);
219 let stats = GraphStatsCalculator::directed(&g);
220 assert!(!stats.is_connected);
221 assert_eq!(stats.component_count, 2);
222 }
223
224 #[test]
225 fn test_graph_stats_with_cycle() {
226 let mut g = DirectedGraph::new();
227 g.add_edge_unweighted(1, 2);
228 g.add_edge_unweighted(2, 3);
229 g.add_edge_unweighted(3, 1);
230 let stats = GraphStatsCalculator::directed(&g);
231 assert!(stats.has_cycle);
232 }
233
234 #[test]
235 fn test_graph_stats_largest_component() {
236 let mut g = DirectedGraph::new();
237 g.add_edge_unweighted(1, 2);
238 g.add_edge_unweighted(2, 3);
239 g.add_edge_unweighted(4, 5);
240 let stats = GraphStatsCalculator::directed(&g);
241 assert_eq!(stats.largest_component_size, 3);
242 }
243
244 #[test]
245 fn test_graph_stats_avg_degree() {
246 let mut g = DirectedGraph::new();
247 g.add_edge_unweighted(1, 2);
248 g.add_edge_unweighted(1, 3);
249 let stats = GraphStatsCalculator::directed(&g);
250 assert!((stats.avg_degree - 4.0 / 3.0).abs() < 0.001);
251 }
252
253 #[test]
254 fn test_graph_stats_max_min_degree() {
255 let mut g = DirectedGraph::new();
256 g.add_edge_unweighted(1, 2);
257 g.add_edge_unweighted(1, 3);
258 g.add_edge_unweighted(1, 4);
259 let stats = GraphStatsCalculator::directed(&g);
260 assert_eq!(stats.max_degree, 3);
261 assert_eq!(stats.min_degree, 1);
262 }
263
264 #[test]
265 fn test_degree_distribution_directed() {
266 let mut g = DirectedGraph::new();
267 g.add_edge_unweighted(1, 2);
268 g.add_edge_unweighted(1, 3);
269 let dist = GraphStatsCalculator::degree_distribution_directed(&g);
270 assert!(!dist.is_empty());
271 let total: usize = dist.iter().map(|d| d.count).sum();
272 assert_eq!(total, 3);
273 }
274
275 #[test]
276 fn test_degree_distribution_undirected() {
277 let mut g = UndirectedGraph::new();
278 g.add_edge_unweighted(1, 2);
279 g.add_edge_unweighted(1, 3);
280 let dist = GraphStatsCalculator::degree_distribution_undirected(&g);
281 assert!(!dist.is_empty());
282 }
283
284 #[test]
285 fn test_graph_stats_to_json() {
286 let g = DirectedGraph::new();
287 let stats = GraphStatsCalculator::directed(&g);
288 let json = stats.to_json();
289 assert!(json.is_object());
290 }
291
292 #[test]
293 fn test_graph_stats_to_summary() {
294 let mut g = DirectedGraph::new();
295 g.add_edge_unweighted(1, 2);
296 let stats = GraphStatsCalculator::directed(&g);
297 let summary = stats.to_summary();
298 assert!(summary.contains("2 nodes"));
299 }
300
301 #[test]
302 fn test_undirected_stats() {
303 let mut g = UndirectedGraph::new();
304 g.add_edge_unweighted(1, 2);
305 g.add_edge_unweighted(2, 3);
306 let stats = GraphStatsCalculator::undirected(&g);
307 assert_eq!(stats.node_count, 3);
308 assert_eq!(stats.edge_count, 2);
309 assert!(stats.is_connected);
310 }
311
312 #[test]
313 fn test_undirected_stats_density() {
314 let mut g = UndirectedGraph::new();
315 g.add_edge_unweighted(1, 2);
316 g.add_edge_unweighted(1, 3);
317 g.add_edge_unweighted(2, 3);
318 let stats = GraphStatsCalculator::undirected(&g);
319 assert!((stats.density - 1.0).abs() < 0.001);
320 }
321}