Skip to main content

rust_igraph/algorithms/constructors/
create.rs

1//! Basic edge-list graph constructor (ALGO-CN-022).
2//!
3//! Counterpart of `igraph_create()` in
4//! `references/igraph/src/constructors/basic_constructors.c:53-81`.
5//! Build a graph from a flat edge list, automatically extending the
6//! vertex count to accommodate the largest endpoint when `n == 0` or
7//! when `n` is smaller than `max(edges) + 1` (upstream behaviour —
8//! callers passing `0` get the smallest graph that holds every edge).
9//!
10//! Upstream `igraph_create` accepts a flat `igraph_vector_int_t` of
11//! length `2·|E|` (`[u0, v0, u1, v1, …]`) and reports `IGRAPH_EINVAL`
12//! on an odd length and `IGRAPH_EINVVID` on a negative vertex id. The
13//! Rust signature is `&[(u32, u32)]`, which eliminates both error
14//! paths at the type-system level (pairs cannot be odd; `u32` cannot
15//! be negative), so the only remaining runtime check is the silent
16//! `n := max(max_edge_endpoint + 1, n)` upper-bound logic.
17//!
18//! The `igraph_small` varargs convenience is intentionally not
19//! ported: Rust call sites use a `Vec`/slice literal like
20//! `create(&[(0, 1), (1, 2), (2, 3)], 4, false)` which is already
21//! terse, and Rust has no `va_list`.
22
23use crate::core::{Graph, IgraphError, IgraphResult};
24
25/// Build a graph from a flat edge list.
26///
27/// `edges` is a slice of `(source, target)` pairs interpreted under
28/// `directed`. If `n == 0` the vertex count is inferred from the
29/// largest endpoint (`max + 1`). If `n > 0` but smaller than
30/// `max + 1`, the vertex count is silently raised to `max + 1`
31/// (matches upstream `igraph_create` which calls `igraph_add_vertices`
32/// to extend).
33///
34/// # Errors
35///
36/// * [`IgraphError::InvalidArgument`] — if the implied vertex count
37///   would overflow `u32` (only possible when the maximum edge
38///   endpoint is `u32::MAX`).
39///
40/// # Examples
41///
42/// ```
43/// use rust_igraph::create;
44///
45/// // n = 0 infers vcount from the edges.
46/// let g = create(&[(0, 1), (1, 2), (2, 3)], 0, false).unwrap();
47/// assert_eq!((g.vcount(), g.ecount()), (4, 3));
48///
49/// // n > max+1 keeps the requested vertex count.
50/// let h = create(&[(0, 1)], 5, false).unwrap();
51/// assert_eq!((h.vcount(), h.ecount()), (5, 1));
52///
53/// // Directed graphs preserve arc orientation.
54/// let d = create(&[(0, 1), (1, 0)], 2, true).unwrap();
55/// assert_eq!((d.vcount(), d.ecount()), (2, 2));
56/// ```
57pub fn create(edges: &[(u32, u32)], n: u32, directed: bool) -> IgraphResult<Graph> {
58    let inferred = if edges.is_empty() {
59        0u32
60    } else {
61        let mut m = 0u32;
62        for &(u, v) in edges {
63            if u > m {
64                m = u;
65            }
66            if v > m {
67                m = v;
68            }
69        }
70        m.checked_add(1).ok_or_else(|| {
71            IgraphError::InvalidArgument(
72                "create: vertex id u32::MAX cannot be promoted to a vcount".into(),
73            )
74        })?
75    };
76
77    let vcount = n.max(inferred);
78    let mut g = Graph::new(vcount, directed)?;
79    g.add_edges(edges.iter().copied())?;
80    Ok(g)
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use std::collections::BTreeSet;
87
88    fn canonical_undirected_edges(g: &Graph) -> BTreeSet<(u32, u32)> {
89        let mut s = BTreeSet::new();
90        for v in 0..g.vcount() {
91            for &u in &g.neighbors(v).expect("neighbors") {
92                let key = if v <= u { (v, u) } else { (u, v) };
93                s.insert(key);
94            }
95        }
96        s
97    }
98
99    fn directed_arc_list(g: &Graph) -> Vec<(u32, u32)> {
100        (0..u32::try_from(g.ecount()).expect("ecount fits u32"))
101            .map(|eid| g.edge(eid).expect("edge"))
102            .collect()
103    }
104
105    #[test]
106    fn empty_edges_n_zero_yields_null_graph() {
107        let g = create(&[], 0, false).expect("create");
108        assert_eq!(g.vcount(), 0);
109        assert_eq!(g.ecount(), 0);
110        assert!(!g.is_directed());
111    }
112
113    #[test]
114    fn empty_edges_n_positive_yields_isolated_vertices() {
115        let g = create(&[], 5, true).expect("create");
116        assert_eq!(g.vcount(), 5);
117        assert_eq!(g.ecount(), 0);
118        assert!(g.is_directed());
119    }
120
121    #[test]
122    fn upstream_simple_example_n_zero_infers_four_vertices() {
123        // From references/igraph/examples/simple/igraph_create.c:
124        // edges = [0,1, 1,2, 2,3, 2,2] with n=0 should give 4 vertices.
125        let g = create(&[(0, 1), (1, 2), (2, 3), (2, 2)], 0, false).expect("create");
126        assert_eq!(g.vcount(), 4);
127        assert_eq!(g.ecount(), 4);
128    }
129
130    #[test]
131    fn upstream_example_n_ten_keeps_ten_vertices() {
132        // Same edges with n=10 should keep all 10 vertices.
133        let g = create(&[(0, 1), (1, 2), (2, 3), (2, 2)], 10, false).expect("create");
134        assert_eq!(g.vcount(), 10);
135        assert_eq!(g.ecount(), 4);
136    }
137
138    #[test]
139    fn n_smaller_than_max_silently_extends() {
140        // Upstream: if n is smaller than max(edges)+1, add the missing vertices.
141        let g = create(&[(0, 1), (5, 6)], 3, false).expect("create");
142        assert_eq!(g.vcount(), 7); // max edge id is 6, so 7 vertices
143        assert_eq!(g.ecount(), 2);
144    }
145
146    #[test]
147    fn directed_preserves_arc_order() {
148        // Arc emission must match input order for directed graphs.
149        let arcs = vec![(0, 1), (1, 0), (1, 2), (2, 1)];
150        let g = create(&arcs, 3, true).expect("create");
151        assert_eq!(g.vcount(), 3);
152        assert_eq!(g.ecount(), 4);
153        assert_eq!(directed_arc_list(&g), arcs);
154    }
155
156    #[test]
157    fn undirected_canonicalises_edges() {
158        let g = create(&[(2, 1), (3, 0)], 0, false).expect("create");
159        let want: BTreeSet<(u32, u32)> = [(0, 3), (1, 2)].into_iter().collect();
160        assert_eq!(canonical_undirected_edges(&g), want);
161    }
162
163    #[test]
164    fn self_loops_accepted() {
165        let g = create(&[(0, 0), (1, 1), (0, 1)], 0, false).expect("create");
166        assert_eq!(g.vcount(), 2);
167        assert_eq!(g.ecount(), 3);
168    }
169
170    #[test]
171    fn parallel_edges_preserved() {
172        let g = create(&[(0, 1), (0, 1), (0, 1)], 0, false).expect("create");
173        assert_eq!(g.ecount(), 3);
174        // All three should land on the same vertex pair.
175        assert_eq!(g.degree(0).unwrap(), 3);
176        assert_eq!(g.degree(1).unwrap(), 3);
177    }
178
179    #[test]
180    fn u32_max_endpoint_errors_rather_than_wraps() {
181        // max edge id == u32::MAX would need vcount = u32::MAX + 1 → overflow.
182        let r = create(&[(0, u32::MAX)], 0, false);
183        assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
184    }
185
186    #[test]
187    fn n_max_vcount_works_when_max_endpoint_lt_n() {
188        // n=10 with max=2 — no extension needed, succeeds.
189        let g = create(&[(0, 1), (1, 2)], 10, false).expect("create");
190        assert_eq!(g.vcount(), 10);
191    }
192
193    #[test]
194    fn star_via_create_matches_handrolled_star() {
195        let n: u32 = 8;
196        let edges: Vec<(u32, u32)> = (1..n).map(|i| (0, i)).collect();
197        let g = create(&edges, n, false).expect("create");
198        assert_eq!(g.vcount(), n);
199        assert_eq!(g.ecount(), (n - 1) as usize);
200        assert_eq!(g.degree(0).unwrap(), (n - 1) as usize);
201        for v in 1..n {
202            assert_eq!(g.degree(v).unwrap(), 1);
203        }
204    }
205}
206
207#[cfg(all(test, feature = "proptest-harness"))]
208mod proptests {
209    use super::*;
210    use proptest::prelude::*;
211
212    fn arb_pairs(max_n: u32) -> impl Strategy<Value = Vec<(u32, u32)>> {
213        prop::collection::vec((0..max_n, 0..max_n), 0..30)
214    }
215
216    proptest! {
217        #[test]
218        fn vcount_at_least_n_and_at_least_max_plus_one(
219            edges in arb_pairs(20),
220            n in 0u32..30,
221            directed: bool,
222        ) {
223            let g = create(&edges, n, directed).expect("create");
224            let inferred = edges
225                .iter()
226                .flat_map(|&(u, v)| [u, v])
227                .max()
228                .map_or(0u32, |m| m + 1);
229            prop_assert!(g.vcount() >= n);
230            prop_assert!(g.vcount() >= inferred);
231            prop_assert_eq!(g.vcount(), n.max(inferred));
232        }
233
234        #[test]
235        fn ecount_equals_input_length(
236            edges in arb_pairs(20),
237            n in 0u32..30,
238            directed: bool,
239        ) {
240            let g = create(&edges, n, directed).expect("create");
241            prop_assert_eq!(g.ecount(), edges.len());
242        }
243
244        #[test]
245        fn directed_preserves_arc_order_property(
246            edges in arb_pairs(15),
247        ) {
248            let g = create(&edges, 0, true).expect("create");
249            let got: Vec<(u32, u32)> = (0..u32::try_from(g.ecount()).unwrap())
250                .map(|eid| g.edge(eid).unwrap())
251                .collect();
252            prop_assert_eq!(got, edges);
253        }
254
255        #[test]
256        fn directedness_flag_round_trips(
257            edges in arb_pairs(15),
258            n in 0u32..20,
259            directed: bool,
260        ) {
261            let g = create(&edges, n, directed).expect("create");
262            prop_assert_eq!(g.is_directed(), directed);
263        }
264    }
265}