Skip to main content

rust_igraph/algorithms/io/
edgelist.rs

1//! Plain edge-list I/O.
2//!
3//! Format: one edge per line, `"u v"` (whitespace-separated `u32` vertex ids).
4//! Lines starting with `#` and blank lines are ignored. The graph is sized to
5//! `max(u, v) + 1` vertices.
6//!
7//! Counterparts of `igraph_read_graph_edgelist` / `igraph_write_graph_edgelist`.
8
9use std::io::{BufRead, BufReader, Read, Write};
10
11use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
12
13/// Read an edge list from any [`Read`] into a fresh [`Graph`].
14///
15/// # Examples
16///
17/// ```
18/// use rust_igraph::read_edgelist;
19///
20/// let data = b"0 1\n1 2\n2 0\n";
21/// let g = read_edgelist(&data[..]).unwrap();
22/// assert_eq!(g.vcount(), 3);
23/// assert_eq!(g.ecount(), 3);
24/// ```
25pub fn read_edgelist<R: Read>(input: R) -> IgraphResult<Graph> {
26    let reader = BufReader::new(input);
27    let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
28    let mut max_id: VertexId = 0;
29    let mut had_any = false;
30
31    for (line_no, line) in reader.lines().enumerate() {
32        let line = line?;
33        let trimmed = line.trim();
34        if trimmed.is_empty() || trimmed.starts_with('#') {
35            continue;
36        }
37        let mut parts = trimmed.split_whitespace();
38        let u = parse_id(parts.next(), line_no + 1)?;
39        let v = parse_id(parts.next(), line_no + 1)?;
40        if parts.next().is_some() {
41            return Err(IgraphError::Parse {
42                line: line_no + 1,
43                message: "expected exactly two vertex ids".into(),
44            });
45        }
46        max_id = max_id.max(u).max(v);
47        edges.push((u, v));
48        had_any = true;
49    }
50
51    let n = if had_any {
52        max_id.checked_add(1).ok_or(IgraphError::Internal(
53            "vertex id u32::MAX cannot be sized into n",
54        ))?
55    } else {
56        0
57    };
58    let mut g = Graph::with_vertices(n);
59    g.add_edges(edges)?;
60    Ok(g)
61}
62
63fn parse_id(token: Option<&str>, line: usize) -> IgraphResult<VertexId> {
64    let s = token.ok_or_else(|| IgraphError::Parse {
65        line,
66        message: "missing vertex id".into(),
67    })?;
68    s.parse::<VertexId>().map_err(|e| IgraphError::Parse {
69        line,
70        message: format!("invalid vertex id `{s}`: {e}"),
71    })
72}
73
74/// Write a graph as an edge list.
75///
76/// Outputs one line per edge: `"source target\n"` using 0-based vertex
77/// IDs. Isolated vertices are not represented in the output.
78///
79/// # Examples
80///
81/// ```
82/// use rust_igraph::{Graph, write_edgelist};
83///
84/// let mut g = Graph::with_vertices(3);
85/// g.add_edge(0, 1).unwrap();
86/// g.add_edge(1, 2).unwrap();
87///
88/// let mut buf = Vec::new();
89/// write_edgelist(&g, &mut buf).unwrap();
90/// let s = String::from_utf8(buf).unwrap();
91/// assert_eq!(s, "0 1\n1 2\n");
92/// ```
93pub fn write_edgelist<W: Write>(graph: &Graph, writer: &mut W) -> IgraphResult<()> {
94    for eid in 0..graph.ecount() {
95        #[allow(clippy::cast_possible_truncation)]
96        let (from, to) = graph.edge(eid as u32)?;
97        writeln!(writer, "{from} {to}")?;
98    }
99    Ok(())
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn empty_input_gives_empty_graph() {
108        let g = read_edgelist(&b""[..]).unwrap();
109        assert_eq!(g.vcount(), 0);
110        assert_eq!(g.ecount(), 0);
111    }
112
113    #[test]
114    fn comments_and_blanks_ignored() {
115        let input = b"# hi\n\n0 1\n   2 3   \n";
116        let g = read_edgelist(&input[..]).unwrap();
117        assert_eq!(g.vcount(), 4);
118        assert_eq!(g.ecount(), 2);
119    }
120
121    #[test]
122    fn malformed_line_errors_with_line_number() {
123        let input = b"0 1\nbroken\n";
124        let err = read_edgelist(&input[..]).unwrap_err();
125        match err {
126            IgraphError::Parse { line, .. } => assert_eq!(line, 2),
127            other => panic!("unexpected error: {other:?}"),
128        }
129    }
130
131    #[test]
132    fn write_basic() {
133        let mut g = Graph::with_vertices(3);
134        g.add_edge(0, 1).unwrap();
135        g.add_edge(1, 2).unwrap();
136
137        let mut buf = Vec::new();
138        write_edgelist(&g, &mut buf).unwrap();
139        let s = String::from_utf8(buf).unwrap();
140        assert_eq!(s, "0 1\n1 2\n");
141    }
142
143    #[test]
144    fn write_empty_graph() {
145        let g = Graph::with_vertices(5);
146        let mut buf = Vec::new();
147        write_edgelist(&g, &mut buf).unwrap();
148        let s = String::from_utf8(buf).unwrap();
149        assert_eq!(s, "");
150    }
151
152    #[test]
153    fn write_directed() {
154        let mut g = Graph::new(3, true).unwrap();
155        g.add_edge(0, 1).unwrap();
156        g.add_edge(2, 0).unwrap();
157
158        let mut buf = Vec::new();
159        write_edgelist(&g, &mut buf).unwrap();
160        let s = String::from_utf8(buf).unwrap();
161        assert_eq!(s, "0 1\n2 0\n");
162    }
163
164    #[test]
165    fn round_trip() {
166        let mut g = Graph::with_vertices(4);
167        g.add_edge(0, 1).unwrap();
168        g.add_edge(2, 3).unwrap();
169        g.add_edge(1, 3).unwrap();
170
171        let mut buf = Vec::new();
172        write_edgelist(&g, &mut buf).unwrap();
173
174        let g2 = read_edgelist(&buf[..]).unwrap();
175        assert_eq!(g2.vcount(), g.vcount());
176        assert_eq!(g2.ecount(), g.ecount());
177    }
178}