rust_igraph/algorithms/io/
lgl.rs1use std::collections::HashMap;
22use std::io::{BufRead, BufReader, Read, Write};
23
24use crate::core::{Graph, IgraphError, IgraphResult};
25
26#[derive(Debug, Clone)]
28pub struct LglGraph {
29 pub graph: Graph,
31 pub names: Vec<String>,
33 pub weights: Option<Vec<f64>>,
36}
37
38pub fn read_lgl<R: Read>(input: R) -> IgraphResult<LglGraph> {
56 let reader = BufReader::new(input);
57 let mut name_map: HashMap<String, u32> = HashMap::new();
58 let mut names: Vec<String> = Vec::new();
59 let mut edges: Vec<(u32, u32)> = Vec::new();
60 let mut weights: Vec<f64> = Vec::new();
61 let mut has_any_weight = false;
62
63 let mut current_hub: Option<u32> = None;
64
65 for (line_idx, line_result) in reader.lines().enumerate() {
66 let line = line_result?;
67 let trimmed = line.trim();
68
69 if trimmed.is_empty() {
70 continue;
71 }
72
73 if let Some(hub_name) = trimmed.strip_prefix('#') {
74 let hub_name = hub_name.trim();
75 if hub_name.is_empty() {
76 return Err(IgraphError::Parse {
77 line: line_idx.wrapping_add(1),
78 message: "hub line '#' has no vertex name".into(),
79 });
80 }
81 let hub_id = get_or_insert_name(hub_name, &mut name_map, &mut names);
82 current_hub = Some(hub_id);
83 } else {
84 let hub_id = current_hub.ok_or_else(|| IgraphError::Parse {
85 line: line_idx.wrapping_add(1),
86 message: "neighbour line before any hub definition".into(),
87 })?;
88
89 let mut parts = trimmed.split_whitespace();
90 let neighbour_name = parts.next().ok_or_else(|| IgraphError::Parse {
91 line: line_idx.wrapping_add(1),
92 message: "empty neighbour line".into(),
93 })?;
94
95 let neighbour_id = get_or_insert_name(neighbour_name, &mut name_map, &mut names);
96 edges.push((hub_id, neighbour_id));
97
98 let weight = if let Some(w_str) = parts.next() {
99 let w = w_str.parse::<f64>().map_err(|e| IgraphError::Parse {
100 line: line_idx.wrapping_add(1),
101 message: format!("invalid weight '{w_str}': {e}"),
102 })?;
103 has_any_weight = true;
104 w
105 } else {
106 0.0
107 };
108 weights.push(weight);
109 }
110 }
111
112 #[allow(clippy::cast_possible_truncation)]
113 let n = names.len() as u32;
114 let mut graph = Graph::with_vertices(n);
115 graph.add_edges(edges)?;
116
117 Ok(LglGraph {
118 graph,
119 names,
120 weights: if has_any_weight { Some(weights) } else { None },
121 })
122}
123
124pub fn write_lgl<W: Write>(
151 graph: &Graph,
152 names: Option<&[String]>,
153 weights: Option<&[f64]>,
154 writer: &mut W,
155) -> IgraphResult<()> {
156 if let Some(n) = names {
157 if n.len() != graph.vcount() as usize {
158 return Err(IgraphError::InvalidArgument(format!(
159 "names length {} does not match vcount {}",
160 n.len(),
161 graph.vcount()
162 )));
163 }
164 }
165 if let Some(w) = weights {
166 if w.len() != graph.ecount() {
167 return Err(IgraphError::InvalidArgument(format!(
168 "weights length {} does not match ecount {}",
169 w.len(),
170 graph.ecount()
171 )));
172 }
173 }
174
175 let vcount = graph.vcount() as usize;
178 let mut adj: Vec<Vec<(u32, usize)>> = vec![Vec::new(); vcount];
179
180 for eid in 0..graph.ecount() {
181 #[allow(clippy::cast_possible_truncation)]
182 let (src, tgt) = graph.edge(eid as u32)?;
183 let hub = src.min(tgt);
185 let neighbour = src.max(tgt);
186 adj[hub as usize].push((neighbour, eid));
187 }
188
189 for v in 0..vcount {
190 if adj[v].is_empty() {
191 continue;
192 }
193
194 let hub_name = match names {
195 Some(n) => n[v].as_str().to_owned(),
196 None => v.to_string(),
197 };
198 writeln!(writer, "# {hub_name}")?;
199
200 for &(neighbour, eid) in &adj[v] {
201 let nbr_name = match names {
202 Some(n) => n[neighbour as usize].as_str().to_owned(),
203 None => neighbour.to_string(),
204 };
205 match weights {
206 Some(w) => writeln!(writer, "{nbr_name} {}", w[eid])?,
207 None => writeln!(writer, "{nbr_name}")?,
208 }
209 }
210 }
211
212 Ok(())
213}
214
215fn get_or_insert_name(name: &str, map: &mut HashMap<String, u32>, names: &mut Vec<String>) -> u32 {
216 if let Some(&id) = map.get(name) {
217 id
218 } else {
219 #[allow(clippy::cast_possible_truncation)]
220 let id = names.len() as u32;
221 map.insert(name.to_owned(), id);
222 names.push(name.to_owned());
223 id
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn test_empty() {
233 let result = read_lgl(&b""[..]).unwrap();
234 assert_eq!(result.graph.vcount(), 0);
235 assert_eq!(result.graph.ecount(), 0);
236 assert!(result.names.is_empty());
237 assert!(result.weights.is_none());
238 }
239
240 #[test]
241 fn test_single_hub_no_neighbours() {
242 let input = b"# Alice\n";
243 let result = read_lgl(&input[..]).unwrap();
244 assert_eq!(result.graph.vcount(), 1);
245 assert_eq!(result.graph.ecount(), 0);
246 assert_eq!(result.names, vec!["Alice"]);
247 }
248
249 #[test]
250 fn test_basic_unweighted() {
251 let input = b"# a\nb\nc\n# b\nc\n";
252 let result = read_lgl(&input[..]).unwrap();
253 assert_eq!(result.graph.vcount(), 3);
254 assert_eq!(result.graph.ecount(), 3);
255 assert_eq!(result.names, vec!["a", "b", "c"]);
256 assert!(result.weights.is_none());
257 }
258
259 #[test]
260 fn test_weighted() {
261 let input = b"# x\ny 1.5\nz 2.0\n";
262 let result = read_lgl(&input[..]).unwrap();
263 assert_eq!(result.graph.ecount(), 2);
264 let w = result.weights.unwrap();
265 assert!((w[0] - 1.5).abs() < 1e-10);
266 assert!((w[1] - 2.0).abs() < 1e-10);
267 }
268
269 #[test]
270 fn test_mixed_weights() {
271 let input = b"# a\nb 3.0\nc\n";
272 let result = read_lgl(&input[..]).unwrap();
273 let w = result.weights.unwrap();
274 assert!((w[0] - 3.0).abs() < 1e-10);
275 assert!((w[1] - 0.0).abs() < 1e-10);
276 }
277
278 #[test]
279 fn test_blank_lines_skipped() {
280 let input = b"\n# a\n\nb\n\n";
281 let result = read_lgl(&input[..]).unwrap();
282 assert_eq!(result.graph.vcount(), 2);
283 assert_eq!(result.graph.ecount(), 1);
284 }
285
286 #[test]
287 fn test_multiple_hubs() {
288 let input = b"# Alice\nBob\nCarol\n# Bob\nCarol\n";
289 let result = read_lgl(&input[..]).unwrap();
290 assert_eq!(result.graph.vcount(), 3);
291 assert_eq!(result.graph.ecount(), 3);
292 }
293
294 #[test]
295 fn test_self_loop() {
296 let input = b"# a\na\n";
297 let result = read_lgl(&input[..]).unwrap();
298 assert_eq!(result.graph.vcount(), 1);
299 assert_eq!(result.graph.ecount(), 1);
300 }
301
302 #[test]
303 fn test_always_undirected() {
304 let input = b"# x\ny\n# y\nx\n";
305 let result = read_lgl(&input[..]).unwrap();
306 assert!(!result.graph.is_directed());
307 }
308
309 #[test]
310 fn test_negative_weight() {
311 let input = b"# a\nb -1.5\n";
312 let result = read_lgl(&input[..]).unwrap();
313 let w = result.weights.unwrap();
314 assert!((w[0] - (-1.5)).abs() < 1e-10);
315 }
316
317 #[test]
318 fn test_scientific_notation_weight() {
319 let input = b"# a\nb 2.5e-3\n";
320 let result = read_lgl(&input[..]).unwrap();
321 let w = result.weights.unwrap();
322 assert!((w[0] - 0.0025).abs() < 1e-10);
323 }
324
325 #[test]
326 fn test_invalid_weight_error() {
327 let input = b"# a\nb notanumber\n";
328 let result = read_lgl(&input[..]);
329 assert!(result.is_err());
330 }
331
332 #[test]
333 fn test_no_hub_before_neighbour_error() {
334 let input = b"b\n";
335 let result = read_lgl(&input[..]);
336 assert!(result.is_err());
337 }
338
339 #[test]
340 fn test_empty_hub_name_error() {
341 let input = b"#\n";
342 let result = read_lgl(&input[..]);
343 assert!(result.is_err());
344 }
345
346 #[test]
347 fn test_write_unweighted() {
348 let mut g = Graph::with_vertices(3);
349 g.add_edge(0, 1).unwrap();
350 g.add_edge(0, 2).unwrap();
351
352 let names = vec!["A".to_string(), "B".to_string(), "C".to_string()];
353 let mut buf = Vec::new();
354 write_lgl(&g, Some(&names), None, &mut buf).unwrap();
355 let s = String::from_utf8(buf).unwrap();
356 assert!(s.contains("# A\n"));
357 assert!(s.contains("B\n"));
358 assert!(s.contains("C\n"));
359 }
360
361 #[test]
362 fn test_write_weighted() {
363 let mut g = Graph::with_vertices(2);
364 g.add_edge(0, 1).unwrap();
365
366 let names = vec!["x".to_string(), "y".to_string()];
367 let weights = vec![2.75];
368 let mut buf = Vec::new();
369 write_lgl(&g, Some(&names), Some(&weights), &mut buf).unwrap();
370 let s = String::from_utf8(buf).unwrap();
371 assert!(s.contains("# x\n"));
372 assert!(s.contains("y 2.75"));
373 }
374
375 #[test]
376 fn test_write_numeric_names() {
377 let mut g = Graph::with_vertices(2);
378 g.add_edge(0, 1).unwrap();
379
380 let mut buf = Vec::new();
381 write_lgl(&g, None, None, &mut buf).unwrap();
382 let s = String::from_utf8(buf).unwrap();
383 assert!(s.contains("# 0\n"));
384 assert!(s.contains("1\n"));
385 }
386
387 #[test]
388 fn test_round_trip() {
389 let input = b"# Alice\nBob 1.0\nCarol 2.0\n# Bob\nCarol 3.0\n";
390 let result = read_lgl(&input[..]).unwrap();
391
392 let mut buf = Vec::new();
393 write_lgl(
394 &result.graph,
395 Some(&result.names),
396 result.weights.as_deref(),
397 &mut buf,
398 )
399 .unwrap();
400
401 let result2 = read_lgl(&buf[..]).unwrap();
402 assert_eq!(result2.graph.vcount(), result.graph.vcount());
403 assert_eq!(result2.graph.ecount(), result.graph.ecount());
404 }
405
406 #[test]
407 fn test_write_names_length_mismatch() {
408 let g = Graph::with_vertices(3);
409 let names = vec!["A".to_string()];
410 let mut buf = Vec::new();
411 assert!(write_lgl(&g, Some(&names), None, &mut buf).is_err());
412 }
413
414 #[test]
415 fn test_write_weights_length_mismatch() {
416 let mut g = Graph::with_vertices(2);
417 g.add_edge(0, 1).unwrap();
418 let weights = vec![1.0, 2.0];
419 let mut buf = Vec::new();
420 assert!(write_lgl(&g, None, Some(&weights), &mut buf).is_err());
421 }
422
423 #[test]
424 fn test_isolated_vertex_not_in_output() {
425 let mut g = Graph::with_vertices(3);
427 g.add_edge(0, 1).unwrap();
428 let names = vec!["A".to_string(), "B".to_string(), "C".to_string()];
431 let mut buf = Vec::new();
432 write_lgl(&g, Some(&names), None, &mut buf).unwrap();
433 let s = String::from_utf8(buf).unwrap();
434 assert!(s.contains("# A\n"));
435 assert!(s.contains("B\n"));
436 assert!(!s.contains('C'));
437 }
438}