Skip to main content

rust_igraph/algorithms/io/
lgl.rs

1//! LGL (Large Graph Layout) adjacency-list I/O (ALGO-IO-003).
2//!
3//! Reads and writes graphs in the `.lgl` format used by the Large Graph
4//! Layout program. The format encodes an adjacency list where each hub
5//! vertex is introduced by a `# vertexname` header, followed by lines
6//! listing its neighbours (one per line), optionally with a weight.
7//!
8//! ```text
9//! # vertex1
10//! vertex2 weight
11//! vertex3
12//! # vertex2
13//! vertex4 weight
14//! ```
15//!
16//! The resulting graph is always **undirected**. Vertex names are mapped to
17//! internal indices in first-occurrence order.
18//!
19//! Counterpart of `igraph_read_graph_lgl` / `igraph_write_graph_lgl`.
20
21use std::collections::HashMap;
22use std::io::{BufRead, BufReader, Read, Write};
23
24use crate::core::{Graph, IgraphError, IgraphResult};
25
26/// Result of reading an LGL file: the graph plus optional metadata.
27#[derive(Debug, Clone)]
28pub struct LglGraph {
29    /// The parsed graph (always undirected).
30    pub graph: Graph,
31    /// Vertex names in index order.
32    pub names: Vec<String>,
33    /// Edge weights (one per edge, in edge order). `None` if no edge had
34    /// a weight specified. Missing weights default to 0.0.
35    pub weights: Option<Vec<f64>>,
36}
37
38/// Read a graph from LGL (`.lgl`) format.
39///
40/// Lines starting with `# ` introduce a hub vertex. Subsequent lines
41/// (until the next hub or EOF) list neighbours, optionally followed by
42/// a weight. Empty lines are skipped.
43///
44/// # Examples
45///
46/// ```
47/// use rust_igraph::read_lgl;
48///
49/// let lgl = b"# Alice\nBob 1.0\nCarol\n# Bob\nCarol 2.5\n";
50/// let result = read_lgl(&lgl[..]).unwrap();
51/// assert_eq!(result.graph.vcount(), 3);
52/// assert_eq!(result.graph.ecount(), 3);
53/// assert_eq!(result.names, vec!["Alice", "Bob", "Carol"]);
54/// ```
55pub 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
124/// Write a graph in LGL format.
125///
126/// Each vertex is written as a hub (`# name`) followed by its neighbours.
127/// If `names` is provided, uses them as vertex labels; otherwise uses
128/// numeric ids. If `weights` is provided (one per edge), appends the weight.
129///
130/// To avoid duplicate edges in the undirected representation, each edge is
131/// emitted only once: under the endpoint with the smaller index.
132///
133/// # Examples
134///
135/// ```
136/// use rust_igraph::{Graph, write_lgl};
137///
138/// let mut g = Graph::with_vertices(3);
139/// g.add_edge(0, 1).unwrap();
140/// g.add_edge(0, 2).unwrap();
141///
142/// let names = vec!["A".to_string(), "B".to_string(), "C".to_string()];
143/// let mut buf = Vec::new();
144/// write_lgl(&g, Some(&names), None, &mut buf).unwrap();
145/// let s = String::from_utf8(buf).unwrap();
146/// assert!(s.contains("# A"));
147/// assert!(s.contains("B"));
148/// assert!(s.contains("C"));
149/// ```
150pub 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    // Build adjacency lists: for each vertex, collect (neighbour, edge_id).
176    // Only emit each edge once — under the endpoint with the smaller index.
177    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        // For self-loops, emit under the vertex itself
184        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        // Isolated vertices with no edges don't appear in LGL output
426        let mut g = Graph::with_vertices(3);
427        g.add_edge(0, 1).unwrap();
428        // vertex 2 is isolated
429
430        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}