Skip to main content

rust_igraph/algorithms/io/
leda.rs

1//! LEDA native graph format reader and writer (ALGO-IO-007 / IO-012).
2//!
3//! The LEDA native graph format is:
4//!
5//! ```text
6//! LEDA.GRAPH
7//! string
8//! void
9//! -2
10//! # Vertices
11//! 3
12//! |{Alice}|
13//! |{Bob}|
14//! |{Carol}|
15//! # Edges
16//! 2
17//! 1 2 0 |{}|
18//! 2 3 0 |{}|
19//! ```
20//!
21//! The header lines specify vertex/edge attribute types (`void`, `string`,
22//! `double`). The directedness flag is `-1` for directed, `-2` for
23//! undirected. Each edge line has: source target reversal label.
24//!
25//! Counterpart of `igraph_write_graph_leda` / `igraph_read_graph_leda`.
26
27use std::io::{BufRead, BufReader, Read, Write};
28
29use crate::core::attributes::AttributeValue;
30use crate::core::{Graph, IgraphError, IgraphResult};
31
32/// Result of reading a LEDA file.
33#[derive(Debug, Clone)]
34pub struct LedaGraph {
35    /// The parsed graph.
36    pub graph: Graph,
37    /// Vertex labels (if the vertex attribute type is `string`).
38    pub labels: Option<Vec<String>>,
39    /// Edge weights (if the edge attribute type is `double`).
40    pub weights: Option<Vec<f64>>,
41}
42
43/// Read a graph from LEDA native graph format.
44///
45/// Parses the header (`LEDA.GRAPH`), attribute type declarations,
46/// directedness flag, vertex section, and edge section. Vertex IDs in
47/// the file are 1-based and converted to 0-based internally.
48///
49/// # Examples
50///
51/// ```
52/// use rust_igraph::read_leda;
53///
54/// let input = b"LEDA.GRAPH\nvoid\nvoid\n-2\n# Vertices\n3\n|{}|\n|{}|\n|{}|\n# Edges\n2\n1 2 0 |{}|\n2 3 0 |{}|\n";
55/// let result = read_leda(&input[..]).unwrap();
56/// assert_eq!(result.graph.vcount(), 3);
57/// assert_eq!(result.graph.ecount(), 2);
58/// assert!(!result.graph.is_directed());
59/// ```
60pub fn read_leda<R: Read>(input: R) -> IgraphResult<LedaGraph> {
61    let reader = BufReader::new(input);
62    let mut content_lines: Vec<String> = Vec::new();
63    for line_result in reader.lines() {
64        let line = line_result?;
65        let trimmed = line.trim().to_string();
66        if !trimmed.is_empty() && !trimmed.starts_with('#') {
67            content_lines.push(trimmed);
68        }
69    }
70
71    let get = |i: usize, msg: &str| -> IgraphResult<&str> {
72        content_lines
73            .get(i)
74            .map(String::as_str)
75            .ok_or_else(|| leda_parse_err(i, msg))
76    };
77
78    if get(0, "empty LEDA file")? != "LEDA.GRAPH" {
79        return Err(leda_parse_err(0, "LEDA file must start with 'LEDA.GRAPH'"));
80    }
81    let has_labels = get(1, "missing vertex attribute type")?.eq_ignore_ascii_case("string");
82    let has_weights = get(2, "missing edge attribute type")?.eq_ignore_ascii_case("double");
83    let directed = match get(3, "missing directedness flag")? {
84        "-1" => true,
85        "-2" => false,
86        other => {
87            return Err(leda_parse_err(
88                3,
89                &format!("invalid directedness flag '{other}', expected -1 or -2"),
90            ));
91        }
92    };
93    let n: u32 = get(4, "missing vertex count")?
94        .parse()
95        .map_err(|e| leda_parse_err(4, &format!("invalid vertex count: {e}")))?;
96
97    let mut pos = 5;
98    let mut labels: Vec<String> = Vec::with_capacity(n as usize);
99    for _ in 0..n {
100        let vline = get(pos, "unexpected end in vertex section")?;
101        let label = extract_leda_label(vline)
102            .ok_or_else(|| leda_parse_err(pos, &format!("invalid vertex entry: {vline}")))?;
103        labels.push(label);
104        pos += 1;
105    }
106
107    let parsed = parse_leda_edges(&content_lines, pos, n, has_weights)?;
108
109    let mut graph = Graph::new(n, directed)?;
110    graph.add_edges(parsed.edges)?;
111
112    if has_labels {
113        graph.set_vertex_attribute_all(
114            "name",
115            labels
116                .iter()
117                .map(|l| AttributeValue::String(l.clone()))
118                .collect(),
119        )?;
120    }
121    if has_weights {
122        graph.set_edge_attribute_all(
123            "weight",
124            parsed
125                .weights
126                .iter()
127                .map(|&w| AttributeValue::Numeric(w))
128                .collect(),
129        )?;
130    }
131
132    Ok(LedaGraph {
133        graph,
134        labels: if has_labels { Some(labels) } else { None },
135        weights: if has_weights {
136            Some(parsed.weights)
137        } else {
138            None
139        },
140    })
141}
142
143struct LedaEdges {
144    edges: Vec<(u32, u32)>,
145    weights: Vec<f64>,
146}
147
148fn parse_leda_edges(
149    lines: &[String],
150    start: usize,
151    n: u32,
152    has_weights: bool,
153) -> IgraphResult<LedaEdges> {
154    let m_str = lines
155        .get(start)
156        .ok_or_else(|| leda_parse_err(start, "missing edge count"))?;
157    let m: usize = m_str
158        .parse()
159        .map_err(|e| leda_parse_err(start, &format!("invalid edge count: {e}")))?;
160
161    let mut edges: Vec<(u32, u32)> = Vec::with_capacity(m);
162    let mut weights: Vec<f64> = Vec::with_capacity(m);
163    for i in 0..m {
164        let pos = start + 1 + i;
165        let eline = lines
166            .get(pos)
167            .ok_or_else(|| leda_parse_err(pos, "unexpected end in edge section"))?;
168        let tokens: Vec<&str> = eline.splitn(4, ' ').collect();
169        if tokens.len() < 3 {
170            return Err(leda_parse_err(
171                pos,
172                &format!("edge line needs at least 3 fields: {eline}"),
173            ));
174        }
175        let from: u32 = tokens[0]
176            .parse()
177            .map_err(|e| leda_parse_err(pos, &format!("invalid source id: {e}")))?;
178        let to: u32 = tokens[1]
179            .parse()
180            .map_err(|e| leda_parse_err(pos, &format!("invalid target id: {e}")))?;
181        if from == 0 || to == 0 || from > n || to > n {
182            return Err(leda_parse_err(
183                pos,
184                &format!("vertex ID out of range: {from}->{to} (n={n})"),
185            ));
186        }
187        edges.push((from - 1, to - 1));
188
189        if has_weights {
190            let label = tokens
191                .get(3)
192                .and_then(|s| extract_leda_label(s))
193                .unwrap_or_default();
194            let w: f64 = if label.is_empty() {
195                0.0
196            } else {
197                label
198                    .parse()
199                    .map_err(|e| leda_parse_err(pos, &format!("invalid edge weight: {e}")))?
200            };
201            weights.push(w);
202        }
203    }
204    Ok(LedaEdges { edges, weights })
205}
206
207fn leda_parse_err(line: usize, msg: &str) -> IgraphError {
208    IgraphError::Parse {
209        line,
210        message: msg.to_string(),
211    }
212}
213
214fn extract_leda_label(s: &str) -> Option<String> {
215    let trimmed = s.trim();
216    let inner = trimmed.strip_prefix("|{")?.strip_suffix("}|")?;
217    Some(inner.to_string())
218}
219
220/// Write a graph in LEDA native graph format.
221///
222/// Vertex labels are written as string attributes if provided.
223/// Edge weights are written as double attributes if provided.
224/// The `reversal` field for edges is always 0 (no reverse edge pointer).
225///
226/// # Examples
227///
228/// ```
229/// use rust_igraph::{Graph, write_leda};
230///
231/// let mut g = Graph::with_vertices(3);
232/// g.add_edge(0, 1).unwrap();
233/// g.add_edge(1, 2).unwrap();
234///
235/// let labels = vec!["A".to_string(), "B".to_string(), "C".to_string()];
236/// let mut buf = Vec::new();
237/// write_leda(&g, Some(&labels), None, &mut buf).unwrap();
238/// let s = String::from_utf8(buf).unwrap();
239/// assert!(s.contains("LEDA.GRAPH"));
240/// assert!(s.contains("|{A}|"));
241/// ```
242pub fn write_leda<W: Write>(
243    graph: &Graph,
244    vertex_labels: Option<&[String]>,
245    edge_weights: Option<&[f64]>,
246    writer: &mut W,
247) -> IgraphResult<()> {
248    if let Some(l) = vertex_labels {
249        if l.len() != graph.vcount() as usize {
250            return Err(IgraphError::InvalidArgument(format!(
251                "vertex_labels length {} does not match vcount {}",
252                l.len(),
253                graph.vcount()
254            )));
255        }
256        for (i, lbl) in l.iter().enumerate() {
257            if lbl.contains('\n') {
258                return Err(IgraphError::InvalidArgument(format!(
259                    "vertex label at index {i} contains a newline character"
260                )));
261            }
262        }
263    }
264    if let Some(w) = edge_weights {
265        if w.len() != graph.ecount() {
266            return Err(IgraphError::InvalidArgument(format!(
267                "edge_weights length {} does not match ecount {}",
268                w.len(),
269                graph.ecount()
270            )));
271        }
272    }
273
274    let has_attr_labels =
275        vertex_labels.is_none() && graph.vertex_attribute_names().contains(&"name");
276    let has_attr_weights =
277        edge_weights.is_none() && graph.edge_attribute_names().contains(&"weight");
278
279    // Header
280    writeln!(writer, "LEDA.GRAPH")?;
281
282    // Vertex attribute type
283    if vertex_labels.is_some() || has_attr_labels {
284        writeln!(writer, "string")?;
285    } else {
286        writeln!(writer, "void")?;
287    }
288
289    // Edge attribute type
290    if edge_weights.is_some() || has_attr_weights {
291        writeln!(writer, "double")?;
292    } else {
293        writeln!(writer, "void")?;
294    }
295
296    // Directedness: -1 = directed, -2 = undirected
297    if graph.is_directed() {
298        writeln!(writer, "-1")?;
299    } else {
300        writeln!(writer, "-2")?;
301    }
302
303    // Vertices section
304    writeln!(writer, "# Vertices")?;
305    writeln!(writer, "{}", graph.vcount())?;
306
307    for v in 0..graph.vcount() {
308        match vertex_labels {
309            Some(labels) => writeln!(writer, "|{{{}}}|", labels[v as usize])?,
310            None => {
311                if has_attr_labels {
312                    let label = graph
313                        .vertex_attribute("name", v)
314                        .and_then(AttributeValue::as_str)
315                        .unwrap_or("");
316                    writeln!(writer, "|{{{label}}}|")?;
317                } else {
318                    writeln!(writer, "|{{}}|")?;
319                }
320            }
321        }
322    }
323
324    // Edges section
325    writeln!(writer, "# Edges")?;
326    writeln!(writer, "{}", graph.ecount())?;
327
328    for eid in 0..graph.ecount() {
329        #[allow(clippy::cast_possible_truncation)]
330        let (from, to) = graph.edge(eid as u32)?;
331
332        if let Some(w) = edge_weights {
333            writeln!(writer, "{} {} 0 |{{{}}}|", from + 1, to + 1, w[eid])?;
334        } else {
335            #[allow(clippy::cast_possible_truncation)]
336            let eid_u32 = eid as u32;
337            if let Some(w) = graph
338                .edge_attribute("weight", eid_u32)
339                .and_then(AttributeValue::as_f64)
340            {
341                writeln!(writer, "{} {} 0 |{{{w}}}|", from + 1, to + 1)?;
342            } else {
343                writeln!(writer, "{} {} 0 |{{}}|", from + 1, to + 1)?;
344            }
345        }
346    }
347
348    Ok(())
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354
355    #[test]
356    fn test_basic_undirected() {
357        let mut g = Graph::with_vertices(3);
358        g.add_edge(0, 1).unwrap();
359        g.add_edge(1, 2).unwrap();
360
361        let mut buf = Vec::new();
362        write_leda(&g, None, None, &mut buf).unwrap();
363        let s = String::from_utf8(buf).unwrap();
364
365        assert!(s.starts_with("LEDA.GRAPH\n"));
366        assert!(s.contains("void\nvoid\n-2\n"));
367        assert!(s.contains("# Vertices\n3\n"));
368        assert!(s.contains("|{}|\n|{}|\n|{}|\n"));
369        assert!(s.contains("# Edges\n2\n"));
370        assert!(s.contains("1 2 0 |{}|\n"));
371        assert!(s.contains("2 3 0 |{}|\n"));
372    }
373
374    #[test]
375    fn test_directed() {
376        let mut g = Graph::new(2, true).unwrap();
377        g.add_edge(0, 1).unwrap();
378
379        let mut buf = Vec::new();
380        write_leda(&g, None, None, &mut buf).unwrap();
381        let s = String::from_utf8(buf).unwrap();
382
383        assert!(s.contains("-1\n"));
384    }
385
386    #[test]
387    fn test_with_labels() {
388        let mut g = Graph::with_vertices(3);
389        g.add_edge(0, 1).unwrap();
390
391        let labels = vec!["Alice".to_string(), "Bob".to_string(), "Carol".to_string()];
392        let mut buf = Vec::new();
393        write_leda(&g, Some(&labels), None, &mut buf).unwrap();
394        let s = String::from_utf8(buf).unwrap();
395
396        assert!(s.contains("string\nvoid\n"));
397        assert!(s.contains("|{Alice}|\n"));
398        assert!(s.contains("|{Bob}|\n"));
399        assert!(s.contains("|{Carol}|\n"));
400    }
401
402    #[test]
403    fn test_with_weights() {
404        let mut g = Graph::with_vertices(2);
405        g.add_edge(0, 1).unwrap();
406
407        let weights = vec![3.5];
408        let mut buf = Vec::new();
409        write_leda(&g, None, Some(&weights), &mut buf).unwrap();
410        let s = String::from_utf8(buf).unwrap();
411
412        assert!(s.contains("void\ndouble\n"));
413        assert!(s.contains("1 2 0 |{3.5}|\n"));
414    }
415
416    #[test]
417    fn test_with_labels_and_weights() {
418        let mut g = Graph::with_vertices(2);
419        g.add_edge(0, 1).unwrap();
420
421        let labels = vec!["X".to_string(), "Y".to_string()];
422        let weights = vec![1.25];
423        let mut buf = Vec::new();
424        write_leda(&g, Some(&labels), Some(&weights), &mut buf).unwrap();
425        let s = String::from_utf8(buf).unwrap();
426
427        assert!(s.contains("string\ndouble\n"));
428        assert!(s.contains("|{X}|\n"));
429        assert!(s.contains("|{Y}|\n"));
430        assert!(s.contains("1 2 0 |{1.25}|\n"));
431    }
432
433    #[test]
434    fn test_empty_graph() {
435        let g = Graph::with_vertices(0);
436
437        let mut buf = Vec::new();
438        write_leda(&g, None, None, &mut buf).unwrap();
439        let s = String::from_utf8(buf).unwrap();
440
441        assert!(s.contains("# Vertices\n0\n"));
442        assert!(s.contains("# Edges\n0\n"));
443    }
444
445    #[test]
446    fn test_no_edges() {
447        let g = Graph::with_vertices(3);
448
449        let mut buf = Vec::new();
450        write_leda(&g, None, None, &mut buf).unwrap();
451        let s = String::from_utf8(buf).unwrap();
452
453        assert!(s.contains("# Vertices\n3\n"));
454        assert!(s.contains("# Edges\n0\n"));
455    }
456
457    #[test]
458    fn test_label_mismatch_error() {
459        let g = Graph::with_vertices(3);
460        let labels = vec!["A".to_string()];
461        let mut buf = Vec::new();
462        assert!(write_leda(&g, Some(&labels), None, &mut buf).is_err());
463    }
464
465    #[test]
466    fn test_weight_mismatch_error() {
467        let mut g = Graph::with_vertices(2);
468        g.add_edge(0, 1).unwrap();
469        let weights = vec![1.0, 2.0];
470        let mut buf = Vec::new();
471        assert!(write_leda(&g, None, Some(&weights), &mut buf).is_err());
472    }
473
474    #[test]
475    fn test_newline_in_label_error() {
476        let g = Graph::with_vertices(2);
477        let labels = vec!["hello\nworld".to_string(), "ok".to_string()];
478        let mut buf = Vec::new();
479        assert!(write_leda(&g, Some(&labels), None, &mut buf).is_err());
480    }
481
482    #[test]
483    fn test_self_loop() {
484        let mut g = Graph::with_vertices(2);
485        g.add_edge(0, 0).unwrap();
486
487        let mut buf = Vec::new();
488        write_leda(&g, None, None, &mut buf).unwrap();
489        let s = String::from_utf8(buf).unwrap();
490
491        assert!(s.contains("1 1 0 |{}|\n"));
492    }
493
494    #[test]
495    fn test_one_based_vertex_ids() {
496        let mut g = Graph::with_vertices(4);
497        g.add_edge(2, 3).unwrap();
498
499        let mut buf = Vec::new();
500        write_leda(&g, None, None, &mut buf).unwrap();
501        let s = String::from_utf8(buf).unwrap();
502
503        assert!(s.contains("3 4 0 |{}|\n"));
504    }
505
506    // --- read_leda tests ---
507
508    #[test]
509    fn test_read_basic_undirected() {
510        let input = b"LEDA.GRAPH\nvoid\nvoid\n-2\n# Vertices\n3\n|{}|\n|{}|\n|{}|\n# Edges\n2\n1 2 0 |{}|\n2 3 0 |{}|\n";
511        let result = read_leda(&input[..]).unwrap();
512        assert_eq!(result.graph.vcount(), 3);
513        assert_eq!(result.graph.ecount(), 2);
514        assert!(!result.graph.is_directed());
515        assert!(result.labels.is_none());
516        assert!(result.weights.is_none());
517    }
518
519    #[test]
520    fn test_read_directed() {
521        let input =
522            b"LEDA.GRAPH\nvoid\nvoid\n-1\n# Vertices\n2\n|{}|\n|{}|\n# Edges\n1\n1 2 0 |{}|\n";
523        let result = read_leda(&input[..]).unwrap();
524        assert!(result.graph.is_directed());
525        assert_eq!(result.graph.ecount(), 1);
526    }
527
528    #[test]
529    fn test_read_with_labels() {
530        let input = b"LEDA.GRAPH\nstring\nvoid\n-2\n# Vertices\n3\n|{Alice}|\n|{Bob}|\n|{Carol}|\n# Edges\n1\n1 2 0 |{}|\n";
531        let result = read_leda(&input[..]).unwrap();
532        let labels = result.labels.unwrap();
533        assert_eq!(labels, vec!["Alice", "Bob", "Carol"]);
534    }
535
536    #[test]
537    fn test_read_with_weights() {
538        let input =
539            b"LEDA.GRAPH\nvoid\ndouble\n-2\n# Vertices\n2\n|{}|\n|{}|\n# Edges\n1\n1 2 0 |{3.5}|\n";
540        let result = read_leda(&input[..]).unwrap();
541        let w = result.weights.unwrap();
542        assert!((w[0] - 3.5).abs() < 1e-10);
543    }
544
545    #[test]
546    fn test_read_with_labels_and_weights() {
547        let input = b"LEDA.GRAPH\nstring\ndouble\n-2\n# Vertices\n2\n|{X}|\n|{Y}|\n# Edges\n1\n1 2 0 |{1.25}|\n";
548        let result = read_leda(&input[..]).unwrap();
549        let labels = result.labels.unwrap();
550        assert_eq!(labels, vec!["X", "Y"]);
551        let w = result.weights.unwrap();
552        assert!((w[0] - 1.25).abs() < 1e-10);
553    }
554
555    #[test]
556    fn test_read_empty_graph() {
557        let input = b"LEDA.GRAPH\nvoid\nvoid\n-2\n# Vertices\n0\n# Edges\n0\n";
558        let result = read_leda(&input[..]).unwrap();
559        assert_eq!(result.graph.vcount(), 0);
560        assert_eq!(result.graph.ecount(), 0);
561    }
562
563    #[test]
564    fn test_read_no_edges() {
565        let input = b"LEDA.GRAPH\nvoid\nvoid\n-2\n# Vertices\n3\n|{}|\n|{}|\n|{}|\n# Edges\n0\n";
566        let result = read_leda(&input[..]).unwrap();
567        assert_eq!(result.graph.vcount(), 3);
568        assert_eq!(result.graph.ecount(), 0);
569    }
570
571    #[test]
572    fn test_read_self_loop() {
573        let input =
574            b"LEDA.GRAPH\nvoid\nvoid\n-2\n# Vertices\n2\n|{}|\n|{}|\n# Edges\n1\n1 1 0 |{}|\n";
575        let result = read_leda(&input[..]).unwrap();
576        assert_eq!(result.graph.ecount(), 1);
577        let (from, to) = result.graph.edge(0).unwrap();
578        assert_eq!(from, 0);
579        assert_eq!(to, 0);
580    }
581
582    #[test]
583    fn test_read_bad_header() {
584        let input = b"NOT_LEDA\nvoid\nvoid\n-2\n";
585        assert!(read_leda(&input[..]).is_err());
586    }
587
588    #[test]
589    fn test_read_bad_directedness() {
590        let input = b"LEDA.GRAPH\nvoid\nvoid\n-3\n# Vertices\n0\n# Edges\n0\n";
591        assert!(read_leda(&input[..]).is_err());
592    }
593
594    #[test]
595    fn test_read_vertex_id_out_of_range() {
596        let input =
597            b"LEDA.GRAPH\nvoid\nvoid\n-2\n# Vertices\n2\n|{}|\n|{}|\n# Edges\n1\n1 5 0 |{}|\n";
598        assert!(read_leda(&input[..]).is_err());
599    }
600
601    #[test]
602    fn test_roundtrip_undirected() {
603        let mut g = Graph::with_vertices(4);
604        g.add_edge(0, 1).unwrap();
605        g.add_edge(1, 2).unwrap();
606        g.add_edge(2, 3).unwrap();
607
608        let mut buf = Vec::new();
609        write_leda(&g, None, None, &mut buf).unwrap();
610        let result = read_leda(&buf[..]).unwrap();
611
612        assert_eq!(result.graph.vcount(), g.vcount());
613        assert_eq!(result.graph.ecount(), g.ecount());
614        assert_eq!(result.graph.is_directed(), g.is_directed());
615    }
616
617    #[test]
618    fn test_roundtrip_directed() {
619        let mut g = Graph::new(3, true).unwrap();
620        g.add_edge(0, 1).unwrap();
621        g.add_edge(1, 2).unwrap();
622
623        let mut buf = Vec::new();
624        write_leda(&g, None, None, &mut buf).unwrap();
625        let result = read_leda(&buf[..]).unwrap();
626
627        assert_eq!(result.graph.vcount(), g.vcount());
628        assert_eq!(result.graph.ecount(), g.ecount());
629        assert!(result.graph.is_directed());
630    }
631
632    #[test]
633    fn test_roundtrip_with_labels() {
634        let mut g = Graph::with_vertices(3);
635        g.add_edge(0, 1).unwrap();
636        g.add_edge(1, 2).unwrap();
637
638        let labels = vec!["A".to_string(), "B".to_string(), "C".to_string()];
639        let mut buf = Vec::new();
640        write_leda(&g, Some(&labels), None, &mut buf).unwrap();
641        let result = read_leda(&buf[..]).unwrap();
642
643        assert_eq!(result.labels.unwrap(), labels);
644    }
645
646    #[test]
647    fn test_roundtrip_with_weights() {
648        let mut g = Graph::with_vertices(2);
649        g.add_edge(0, 1).unwrap();
650
651        let weights = vec![2.75];
652        let mut buf = Vec::new();
653        write_leda(&g, None, Some(&weights), &mut buf).unwrap();
654        let result = read_leda(&buf[..]).unwrap();
655
656        let w = result.weights.unwrap();
657        assert!((w[0] - 2.75).abs() < 1e-10);
658    }
659
660    #[test]
661    fn test_read_empty_label() {
662        let input = b"LEDA.GRAPH\nstring\nvoid\n-2\n# Vertices\n2\n|{}|\n|{hello}|\n# Edges\n0\n";
663        let result = read_leda(&input[..]).unwrap();
664        let labels = result.labels.unwrap();
665        assert_eq!(labels[0], "");
666        assert_eq!(labels[1], "hello");
667    }
668
669    // --- Attribute integration tests ---
670
671    #[test]
672    fn test_read_stores_label_attribute() {
673        let input =
674            b"LEDA.GRAPH\nstring\nvoid\n-2\n# Vertices\n2\n|{Alice}|\n|{Bob}|\n# Edges\n1\n1 2 0 |{}|\n";
675        let result = read_leda(&input[..]).unwrap();
676        assert_eq!(
677            result
678                .graph
679                .vertex_attribute("name", 0)
680                .and_then(AttributeValue::as_str),
681            Some("Alice")
682        );
683        assert_eq!(
684            result
685                .graph
686                .vertex_attribute("name", 1)
687                .and_then(AttributeValue::as_str),
688            Some("Bob")
689        );
690    }
691
692    #[test]
693    fn test_read_stores_weight_attribute() {
694        let input =
695            b"LEDA.GRAPH\nvoid\ndouble\n-2\n# Vertices\n2\n|{}|\n|{}|\n# Edges\n1\n1 2 0 |{4.5}|\n";
696        let result = read_leda(&input[..]).unwrap();
697        let w = result
698            .graph
699            .edge_attribute("weight", 0)
700            .and_then(AttributeValue::as_f64)
701            .unwrap();
702        assert!((w - 4.5).abs() < 1e-10);
703    }
704
705    #[test]
706    fn test_read_no_label_attribute_when_void() {
707        let input = b"LEDA.GRAPH\nvoid\nvoid\n-2\n# Vertices\n2\n|{}|\n|{}|\n# Edges\n0\n";
708        let result = read_leda(&input[..]).unwrap();
709        assert!(result.graph.vertex_attribute("name", 0).is_none());
710    }
711
712    #[test]
713    fn test_write_fallback_to_label_attribute() {
714        let mut g = Graph::with_vertices(2);
715        g.add_edge(0, 1).unwrap();
716        g.set_vertex_attribute("name", 0, AttributeValue::String("X".into()))
717            .unwrap();
718        g.set_vertex_attribute("name", 1, AttributeValue::String("Y".into()))
719            .unwrap();
720
721        let mut buf = Vec::new();
722        write_leda(&g, None, None, &mut buf).unwrap();
723        let s = String::from_utf8(buf).unwrap();
724        assert!(s.contains("string"));
725        assert!(s.contains("|{X}|"));
726        assert!(s.contains("|{Y}|"));
727    }
728
729    #[test]
730    fn test_write_fallback_to_weight_attribute() {
731        let mut g = Graph::with_vertices(2);
732        g.add_edge(0, 1).unwrap();
733        g.set_edge_attribute("weight", 0, AttributeValue::Numeric(7.5))
734            .unwrap();
735
736        let mut buf = Vec::new();
737        write_leda(&g, None, None, &mut buf).unwrap();
738        let s = String::from_utf8(buf).unwrap();
739        assert!(s.contains("double"));
740        assert!(s.contains("|{7.5}|"));
741    }
742
743    #[test]
744    fn test_roundtrip_via_attributes() {
745        let input = b"LEDA.GRAPH\nstring\ndouble\n-2\n# Vertices\n2\n|{Alice}|\n|{Bob}|\n# Edges\n1\n1 2 0 |{1.5}|\n";
746        let result = read_leda(&input[..]).unwrap();
747
748        let mut buf = Vec::new();
749        write_leda(&result.graph, None, None, &mut buf).unwrap();
750
751        let result2 = read_leda(&buf[..]).unwrap();
752        assert_eq!(result2.graph.vcount(), 2);
753        assert_eq!(result2.graph.ecount(), 1);
754        assert!(result2.labels.is_some());
755        assert!(result2.weights.is_some());
756        let labels = result2.labels.unwrap();
757        assert_eq!(labels, vec!["Alice", "Bob"]);
758    }
759
760    #[test]
761    fn test_explicit_params_override_attributes() {
762        let mut g = Graph::with_vertices(2);
763        g.add_edge(0, 1).unwrap();
764        g.set_vertex_attribute("name", 0, AttributeValue::String("attr_A".into()))
765            .unwrap();
766        g.set_vertex_attribute("name", 1, AttributeValue::String("attr_B".into()))
767            .unwrap();
768
769        let labels = vec!["explicit_A".to_string(), "explicit_B".to_string()];
770        let mut buf = Vec::new();
771        write_leda(&g, Some(&labels), None, &mut buf).unwrap();
772        let s = String::from_utf8(buf).unwrap();
773        assert!(s.contains("|{explicit_A}|"));
774        assert!(!s.contains("attr_A"));
775    }
776}