Skip to main content

rust_igraph/algorithms/io/
leda.rs

1//! LEDA native graph format writer (ALGO-IO-007).
2//!
3//! Writes graphs in the LEDA native graph format. This is a write-only
4//! format in igraph and in our implementation. The format is:
5//!
6//! ```text
7//! LEDA.GRAPH
8//! string
9//! void
10//! -2
11//! # Vertices
12//! 3
13//! |{Alice}|
14//! |{Bob}|
15//! |{Carol}|
16//! # Edges
17//! 2
18//! 1 2 0 |{}|
19//! 2 3 0 |{}|
20//! ```
21//!
22//! The header lines specify vertex/edge attribute types (`void`, `string`,
23//! `double`). The directedness flag is `-1` for directed, `-2` for
24//! undirected. Each edge line has: source target reversal label.
25//!
26//! Counterpart of `igraph_write_graph_leda`.
27
28use std::io::Write;
29
30use crate::core::{Graph, IgraphError, IgraphResult};
31
32/// Write a graph in LEDA native graph format.
33///
34/// Vertex labels are written as string attributes if provided.
35/// Edge weights are written as double attributes if provided.
36/// The `reversal` field for edges is always 0 (no reverse edge pointer).
37///
38/// # Examples
39///
40/// ```
41/// use rust_igraph::{Graph, write_leda};
42///
43/// let mut g = Graph::with_vertices(3);
44/// g.add_edge(0, 1).unwrap();
45/// g.add_edge(1, 2).unwrap();
46///
47/// let labels = vec!["A".to_string(), "B".to_string(), "C".to_string()];
48/// let mut buf = Vec::new();
49/// write_leda(&g, Some(&labels), None, &mut buf).unwrap();
50/// let s = String::from_utf8(buf).unwrap();
51/// assert!(s.contains("LEDA.GRAPH"));
52/// assert!(s.contains("|{A}|"));
53/// ```
54pub fn write_leda<W: Write>(
55    graph: &Graph,
56    vertex_labels: Option<&[String]>,
57    edge_weights: Option<&[f64]>,
58    writer: &mut W,
59) -> IgraphResult<()> {
60    if let Some(l) = vertex_labels {
61        if l.len() != graph.vcount() as usize {
62            return Err(IgraphError::InvalidArgument(format!(
63                "vertex_labels length {} does not match vcount {}",
64                l.len(),
65                graph.vcount()
66            )));
67        }
68        for (i, lbl) in l.iter().enumerate() {
69            if lbl.contains('\n') {
70                return Err(IgraphError::InvalidArgument(format!(
71                    "vertex label at index {i} contains a newline character"
72                )));
73            }
74        }
75    }
76    if let Some(w) = edge_weights {
77        if w.len() != graph.ecount() {
78            return Err(IgraphError::InvalidArgument(format!(
79                "edge_weights length {} does not match ecount {}",
80                w.len(),
81                graph.ecount()
82            )));
83        }
84    }
85
86    // Header
87    writeln!(writer, "LEDA.GRAPH")?;
88
89    // Vertex attribute type
90    if vertex_labels.is_some() {
91        writeln!(writer, "string")?;
92    } else {
93        writeln!(writer, "void")?;
94    }
95
96    // Edge attribute type
97    if edge_weights.is_some() {
98        writeln!(writer, "double")?;
99    } else {
100        writeln!(writer, "void")?;
101    }
102
103    // Directedness: -1 = directed, -2 = undirected
104    if graph.is_directed() {
105        writeln!(writer, "-1")?;
106    } else {
107        writeln!(writer, "-2")?;
108    }
109
110    // Vertices section
111    writeln!(writer, "# Vertices")?;
112    writeln!(writer, "{}", graph.vcount())?;
113
114    for v in 0..graph.vcount() {
115        match vertex_labels {
116            Some(labels) => writeln!(writer, "|{{{}}}|", labels[v as usize])?,
117            None => writeln!(writer, "|{{}}|")?,
118        }
119    }
120
121    // Edges section
122    writeln!(writer, "# Edges")?;
123    writeln!(writer, "{}", graph.ecount())?;
124
125    for eid in 0..graph.ecount() {
126        #[allow(clippy::cast_possible_truncation)]
127        let (from, to) = graph.edge(eid as u32)?;
128
129        match edge_weights {
130            Some(w) => writeln!(writer, "{} {} 0 |{{{}}}|", from + 1, to + 1, w[eid])?,
131            None => writeln!(writer, "{} {} 0 |{{}}|", from + 1, to + 1)?,
132        }
133    }
134
135    Ok(())
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn test_basic_undirected() {
144        let mut g = Graph::with_vertices(3);
145        g.add_edge(0, 1).unwrap();
146        g.add_edge(1, 2).unwrap();
147
148        let mut buf = Vec::new();
149        write_leda(&g, None, None, &mut buf).unwrap();
150        let s = String::from_utf8(buf).unwrap();
151
152        assert!(s.starts_with("LEDA.GRAPH\n"));
153        assert!(s.contains("void\nvoid\n-2\n"));
154        assert!(s.contains("# Vertices\n3\n"));
155        assert!(s.contains("|{}|\n|{}|\n|{}|\n"));
156        assert!(s.contains("# Edges\n2\n"));
157        assert!(s.contains("1 2 0 |{}|\n"));
158        assert!(s.contains("2 3 0 |{}|\n"));
159    }
160
161    #[test]
162    fn test_directed() {
163        let mut g = Graph::new(2, true).unwrap();
164        g.add_edge(0, 1).unwrap();
165
166        let mut buf = Vec::new();
167        write_leda(&g, None, None, &mut buf).unwrap();
168        let s = String::from_utf8(buf).unwrap();
169
170        assert!(s.contains("-1\n"));
171    }
172
173    #[test]
174    fn test_with_labels() {
175        let mut g = Graph::with_vertices(3);
176        g.add_edge(0, 1).unwrap();
177
178        let labels = vec!["Alice".to_string(), "Bob".to_string(), "Carol".to_string()];
179        let mut buf = Vec::new();
180        write_leda(&g, Some(&labels), None, &mut buf).unwrap();
181        let s = String::from_utf8(buf).unwrap();
182
183        assert!(s.contains("string\nvoid\n"));
184        assert!(s.contains("|{Alice}|\n"));
185        assert!(s.contains("|{Bob}|\n"));
186        assert!(s.contains("|{Carol}|\n"));
187    }
188
189    #[test]
190    fn test_with_weights() {
191        let mut g = Graph::with_vertices(2);
192        g.add_edge(0, 1).unwrap();
193
194        let weights = vec![3.5];
195        let mut buf = Vec::new();
196        write_leda(&g, None, Some(&weights), &mut buf).unwrap();
197        let s = String::from_utf8(buf).unwrap();
198
199        assert!(s.contains("void\ndouble\n"));
200        assert!(s.contains("1 2 0 |{3.5}|\n"));
201    }
202
203    #[test]
204    fn test_with_labels_and_weights() {
205        let mut g = Graph::with_vertices(2);
206        g.add_edge(0, 1).unwrap();
207
208        let labels = vec!["X".to_string(), "Y".to_string()];
209        let weights = vec![1.25];
210        let mut buf = Vec::new();
211        write_leda(&g, Some(&labels), Some(&weights), &mut buf).unwrap();
212        let s = String::from_utf8(buf).unwrap();
213
214        assert!(s.contains("string\ndouble\n"));
215        assert!(s.contains("|{X}|\n"));
216        assert!(s.contains("|{Y}|\n"));
217        assert!(s.contains("1 2 0 |{1.25}|\n"));
218    }
219
220    #[test]
221    fn test_empty_graph() {
222        let g = Graph::with_vertices(0);
223
224        let mut buf = Vec::new();
225        write_leda(&g, None, None, &mut buf).unwrap();
226        let s = String::from_utf8(buf).unwrap();
227
228        assert!(s.contains("# Vertices\n0\n"));
229        assert!(s.contains("# Edges\n0\n"));
230    }
231
232    #[test]
233    fn test_no_edges() {
234        let g = Graph::with_vertices(3);
235
236        let mut buf = Vec::new();
237        write_leda(&g, None, None, &mut buf).unwrap();
238        let s = String::from_utf8(buf).unwrap();
239
240        assert!(s.contains("# Vertices\n3\n"));
241        assert!(s.contains("# Edges\n0\n"));
242    }
243
244    #[test]
245    fn test_label_mismatch_error() {
246        let g = Graph::with_vertices(3);
247        let labels = vec!["A".to_string()];
248        let mut buf = Vec::new();
249        assert!(write_leda(&g, Some(&labels), None, &mut buf).is_err());
250    }
251
252    #[test]
253    fn test_weight_mismatch_error() {
254        let mut g = Graph::with_vertices(2);
255        g.add_edge(0, 1).unwrap();
256        let weights = vec![1.0, 2.0];
257        let mut buf = Vec::new();
258        assert!(write_leda(&g, None, Some(&weights), &mut buf).is_err());
259    }
260
261    #[test]
262    fn test_newline_in_label_error() {
263        let g = Graph::with_vertices(2);
264        let labels = vec!["hello\nworld".to_string(), "ok".to_string()];
265        let mut buf = Vec::new();
266        assert!(write_leda(&g, Some(&labels), None, &mut buf).is_err());
267    }
268
269    #[test]
270    fn test_self_loop() {
271        let mut g = Graph::with_vertices(2);
272        g.add_edge(0, 0).unwrap();
273
274        let mut buf = Vec::new();
275        write_leda(&g, None, None, &mut buf).unwrap();
276        let s = String::from_utf8(buf).unwrap();
277
278        assert!(s.contains("1 1 0 |{}|\n"));
279    }
280
281    #[test]
282    fn test_one_based_vertex_ids() {
283        let mut g = Graph::with_vertices(4);
284        g.add_edge(2, 3).unwrap();
285
286        let mut buf = Vec::new();
287        write_leda(&g, None, None, &mut buf).unwrap();
288        let s = String::from_utf8(buf).unwrap();
289
290        assert!(s.contains("3 4 0 |{}|\n"));
291    }
292}