rust_igraph/algorithms/properties/
summary.rs1use std::fmt::Write as FmtWrite;
4
5use crate::algorithms::connectivity::components::connected_components;
6use crate::algorithms::connectivity::is_connected::{ConnectednessMode, is_connected};
7use crate::algorithms::properties::basic::density;
8use crate::algorithms::properties::degree::{DegreeMode, max_degree, min_degree};
9use crate::algorithms::properties::multiplicity::{has_loop, has_multiple};
10use crate::core::{Graph, IgraphResult};
11
12#[derive(Debug, Clone)]
14#[allow(clippy::struct_excessive_bools)]
15pub struct GraphSummary {
16 pub directed: bool,
18 pub vcount: u32,
20 pub ecount: usize,
22 pub connected: bool,
24 pub component_count: u32,
26 pub density: Option<f64>,
28 pub min_degree: u32,
30 pub max_degree: u32,
32 pub has_loops: bool,
34 pub has_multiple: bool,
36}
37
38impl std::fmt::Display for GraphSummary {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 let kind = if self.directed {
41 "Directed"
42 } else {
43 "Undirected"
44 };
45 write!(
46 f,
47 "{kind} graph: {v} vertices, {e} edges",
48 v = self.vcount,
49 e = self.ecount
50 )?;
51
52 if self.vcount > 0 {
53 let conn = if self.connected {
54 "connected"
55 } else {
56 "disconnected"
57 };
58 write!(f, ", {conn}")?;
59 if !self.connected {
60 write!(f, " ({} components)", self.component_count)?;
61 }
62 }
63
64 if let Some(d) = self.density {
65 write!(f, ", density={d:.4}")?;
66 }
67
68 if self.vcount > 0 {
69 write!(f, ", degree=[{}, {}]", self.min_degree, self.max_degree)?;
70 }
71
72 let mut flags = Vec::new();
73 if self.has_loops {
74 flags.push("loops");
75 }
76 if self.has_multiple {
77 flags.push("multi");
78 }
79 if !flags.is_empty() {
80 write!(f, " [{}]", flags.join(", "))?;
81 }
82
83 Ok(())
84 }
85}
86
87pub fn graph_summary(graph: &Graph) -> IgraphResult<GraphSummary> {
129 let vcount = graph.vcount();
130 let ecount = graph.ecount();
131 let directed = graph.is_directed();
132
133 let (connected, component_count) = if vcount == 0 {
134 (true, 0)
135 } else {
136 let conn = is_connected(graph, ConnectednessMode::Weak)?;
137 let cc = connected_components(graph)?;
138 (conn, cc.count)
139 };
140
141 let dens = density(graph)?;
142
143 let (min_deg, max_deg) = if vcount == 0 {
144 (0, 0)
145 } else {
146 let mn = min_degree(graph, DegreeMode::All)?;
147 let mx = max_degree(graph, DegreeMode::All)?;
148 (mn, mx)
149 };
150
151 let loops = has_loop(graph)?;
152 let multi = has_multiple(graph)?;
153
154 Ok(GraphSummary {
155 directed,
156 vcount,
157 ecount,
158 connected,
159 component_count,
160 density: dens,
161 min_degree: min_deg,
162 max_degree: max_deg,
163 has_loops: loops,
164 has_multiple: multi,
165 })
166}
167
168pub fn graph_summary_string(graph: &Graph) -> IgraphResult<String> {
190 let s = graph_summary(graph)?;
191 let mut out = String::new();
192
193 let kind = if s.directed { "Directed" } else { "Undirected" };
194 let _ = writeln!(out, "--- Graph Summary ---");
195 let _ = writeln!(out, "Type: {kind}");
196 let _ = writeln!(out, "Vertices: {}", s.vcount);
197 let _ = writeln!(out, "Edges: {}", s.ecount);
198
199 let conn_str = if s.connected { "yes" } else { "no" };
200 let _ = writeln!(out, "Connected: {conn_str}");
201 if !s.connected {
202 let _ = writeln!(out, "Components: {}", s.component_count);
203 }
204
205 if let Some(d) = s.density {
206 let _ = writeln!(out, "Density: {d:.6}");
207 }
208
209 if s.vcount > 0 {
210 let _ = writeln!(out, "Degree range: [{}, {}]", s.min_degree, s.max_degree);
211 }
212
213 let simple = !s.has_loops && !s.has_multiple;
214 let _ = writeln!(out, "Simple: {}", if simple { "yes" } else { "no" });
215 if s.has_loops {
216 let _ = writeln!(out, " Self-loops: yes");
217 }
218 if s.has_multiple {
219 let _ = writeln!(out, " Multi-edges: yes");
220 }
221
222 let _ = write!(out, "--------------------");
223 Ok(out)
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn summary_empty_graph() {
232 let g = Graph::with_vertices(0);
233 let s = graph_summary(&g).unwrap();
234 assert_eq!(s.vcount, 0);
235 assert_eq!(s.ecount, 0);
236 assert!(s.connected);
237 assert_eq!(s.component_count, 0);
238 assert_eq!(s.density, None);
239 assert_eq!(s.min_degree, 0);
240 assert_eq!(s.max_degree, 0);
241 assert!(!s.has_loops);
242 assert!(!s.has_multiple);
243 }
244
245 #[test]
246 fn summary_path_graph() {
247 let mut g = Graph::with_vertices(5);
248 g.add_edge(0, 1).unwrap();
249 g.add_edge(1, 2).unwrap();
250 g.add_edge(2, 3).unwrap();
251 g.add_edge(3, 4).unwrap();
252
253 let s = graph_summary(&g).unwrap();
254 assert!(!s.directed);
255 assert_eq!(s.vcount, 5);
256 assert_eq!(s.ecount, 4);
257 assert!(s.connected);
258 assert_eq!(s.component_count, 1);
259 assert_eq!(s.min_degree, 1);
260 assert_eq!(s.max_degree, 2);
261 assert!(!s.has_loops);
262 assert!(!s.has_multiple);
263 }
264
265 #[test]
266 fn summary_disconnected_graph() {
267 let mut g = Graph::with_vertices(6);
268 g.add_edge(0, 1).unwrap();
269 g.add_edge(1, 2).unwrap();
270 g.add_edge(3, 4).unwrap();
271 let s = graph_summary(&g).unwrap();
274 assert!(!s.connected);
275 assert_eq!(s.component_count, 3);
276 }
277
278 #[test]
279 fn summary_directed_graph() {
280 let mut g = Graph::new(3, true).unwrap();
281 g.add_edge(0, 1).unwrap();
282 g.add_edge(1, 2).unwrap();
283 g.add_edge(2, 0).unwrap();
284
285 let s = graph_summary(&g).unwrap();
286 assert!(s.directed);
287 assert!(s.connected);
288 assert_eq!(s.ecount, 3);
289 }
290
291 #[test]
292 fn summary_with_loops() {
293 let mut g = Graph::with_vertices(3);
294 g.add_edge(0, 1).unwrap();
295 g.add_edge(1, 1).unwrap(); let s = graph_summary(&g).unwrap();
298 assert!(s.has_loops);
299 }
300
301 #[test]
302 fn summary_with_multiedges() {
303 let mut g = Graph::with_vertices(2);
304 g.add_edge(0, 1).unwrap();
305 g.add_edge(0, 1).unwrap(); let s = graph_summary(&g).unwrap();
308 assert!(s.has_multiple);
309 }
310
311 #[test]
312 fn summary_display_format() {
313 let mut g = Graph::with_vertices(4);
314 g.add_edge(0, 1).unwrap();
315 g.add_edge(1, 2).unwrap();
316 g.add_edge(2, 3).unwrap();
317 g.add_edge(3, 0).unwrap();
318
319 let s = graph_summary(&g).unwrap();
320 let display = format!("{s}");
321 assert!(display.contains("Undirected graph"));
322 assert!(display.contains("4 vertices"));
323 assert!(display.contains("4 edges"));
324 assert!(display.contains("connected"));
325 assert!(display.contains("density="));
326 }
327
328 #[test]
329 fn summary_string_format() {
330 let mut g = Graph::with_vertices(3);
331 g.add_edge(0, 1).unwrap();
332 g.add_edge(1, 2).unwrap();
333
334 let out = graph_summary_string(&g).unwrap();
335 assert!(out.contains("Vertices: 3"));
336 assert!(out.contains("Edges: 2"));
337 assert!(out.contains("Connected: yes"));
338 assert!(out.contains("Simple: yes"));
339 }
340
341 #[test]
342 fn summary_singleton() {
343 let g = Graph::with_vertices(1);
344 let s = graph_summary(&g).unwrap();
345 assert_eq!(s.vcount, 1);
346 assert_eq!(s.ecount, 0);
347 assert!(s.connected);
348 assert_eq!(s.component_count, 1);
349 assert_eq!(s.density, None);
350 assert_eq!(s.min_degree, 0);
351 assert_eq!(s.max_degree, 0);
352 }
353}