Skip to main content

rust_igraph/algorithms/operators/
to_directed.rs

1//! Undirected → directed conversion (ALGO-OP-021).
2//!
3//! Converts an undirected graph to a directed graph using various edge
4//! conversion modes. Counterpart of `igraph_to_directed`.
5
6use crate::core::{Graph, IgraphResult};
7
8/// Mode for converting an undirected graph to directed.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ToDirectedMode {
11    /// Each undirected edge becomes two directed edges: u→v and v→u.
12    Mutual,
13    /// Each undirected edge is assigned an arbitrary direction (smaller
14    /// vertex → larger vertex, matching igraph C's canonical order).
15    Arbitrary,
16}
17
18/// Converts an undirected graph to a directed graph.
19///
20/// If the graph is already directed, returns a copy.
21///
22/// # Modes
23///
24/// - `Mutual`: each undirected edge {u,v} becomes two directed edges
25///   u→v and v→u.
26/// - `Arbitrary`: each undirected edge {u,v} becomes a single directed
27///   edge from min(u,v) → max(u,v).
28///
29/// # Examples
30///
31/// ```
32/// use rust_igraph::{Graph, to_directed, ToDirectedMode};
33///
34/// let mut g = Graph::with_vertices(3);
35/// g.add_edge(0, 1).unwrap();
36/// g.add_edge(1, 2).unwrap();
37///
38/// // Mutual: each edge becomes two directed edges
39/// let d = to_directed(&g, ToDirectedMode::Mutual).unwrap();
40/// assert!(d.is_directed());
41/// assert_eq!(d.ecount(), 4); // 2 edges × 2 directions
42///
43/// // Arbitrary: each edge gets one direction (canonical)
44/// let d = to_directed(&g, ToDirectedMode::Arbitrary).unwrap();
45/// assert!(d.is_directed());
46/// assert_eq!(d.ecount(), 2);
47/// ```
48pub fn to_directed(graph: &Graph, mode: ToDirectedMode) -> IgraphResult<Graph> {
49    let n = graph.vcount();
50
51    if graph.is_directed() {
52        // Already directed — clone the structure
53        let mut result = Graph::new(n, true)?;
54        let ecount = graph.ecount();
55        for eid in 0..ecount {
56            #[allow(clippy::cast_possible_truncation)]
57            let (src, tgt) = graph.edge(eid as u32)?;
58            result.add_edge(src, tgt)?;
59        }
60        return Ok(result);
61    }
62
63    let mut result = Graph::new(n, true)?;
64    let ecount = graph.ecount();
65
66    match mode {
67        ToDirectedMode::Mutual => {
68            for eid in 0..ecount {
69                #[allow(clippy::cast_possible_truncation)]
70                let (src, tgt) = graph.edge(eid as u32)?;
71                result.add_edge(src, tgt)?;
72                if src != tgt {
73                    result.add_edge(tgt, src)?;
74                }
75            }
76        }
77        ToDirectedMode::Arbitrary => {
78            for eid in 0..ecount {
79                #[allow(clippy::cast_possible_truncation)]
80                let (src, tgt) = graph.edge(eid as u32)?;
81                // Undirected graphs store (min, max) canonically
82                result.add_edge(src, tgt)?;
83            }
84        }
85    }
86
87    Ok(result)
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn test_already_directed() {
96        let mut g = Graph::new(3, true).unwrap();
97        g.add_edge(0, 1).unwrap();
98        g.add_edge(1, 2).unwrap();
99        let d = to_directed(&g, ToDirectedMode::Mutual).unwrap();
100        assert!(d.is_directed());
101        assert_eq!(d.vcount(), 3);
102        assert_eq!(d.ecount(), 2);
103    }
104
105    #[test]
106    fn test_mutual_mode_single_edge() {
107        let mut g = Graph::with_vertices(2);
108        g.add_edge(0, 1).unwrap();
109        let d = to_directed(&g, ToDirectedMode::Mutual).unwrap();
110        assert!(d.is_directed());
111        assert_eq!(d.ecount(), 2);
112    }
113
114    #[test]
115    fn test_mutual_mode_triangle() {
116        let mut g = Graph::with_vertices(3);
117        g.add_edge(0, 1).unwrap();
118        g.add_edge(1, 2).unwrap();
119        g.add_edge(0, 2).unwrap();
120        let d = to_directed(&g, ToDirectedMode::Mutual).unwrap();
121        assert_eq!(d.ecount(), 6); // 3 edges × 2 directions
122    }
123
124    #[test]
125    fn test_arbitrary_mode_single_edge() {
126        let mut g = Graph::with_vertices(2);
127        g.add_edge(0, 1).unwrap();
128        let d = to_directed(&g, ToDirectedMode::Arbitrary).unwrap();
129        assert!(d.is_directed());
130        assert_eq!(d.ecount(), 1);
131    }
132
133    #[test]
134    fn test_arbitrary_mode_path() {
135        let mut g = Graph::with_vertices(4);
136        g.add_edge(0, 1).unwrap();
137        g.add_edge(1, 2).unwrap();
138        g.add_edge(2, 3).unwrap();
139        let d = to_directed(&g, ToDirectedMode::Arbitrary).unwrap();
140        assert_eq!(d.ecount(), 3);
141    }
142
143    #[test]
144    fn test_empty_graph() {
145        let g = Graph::with_vertices(0);
146        let d = to_directed(&g, ToDirectedMode::Mutual).unwrap();
147        assert!(d.is_directed());
148        assert_eq!(d.vcount(), 0);
149        assert_eq!(d.ecount(), 0);
150    }
151
152    #[test]
153    fn test_no_edges() {
154        let g = Graph::with_vertices(5);
155        let d = to_directed(&g, ToDirectedMode::Arbitrary).unwrap();
156        assert_eq!(d.vcount(), 5);
157        assert_eq!(d.ecount(), 0);
158    }
159
160    #[test]
161    fn test_self_loop_mutual() {
162        let mut g = Graph::with_vertices(2);
163        g.add_edge(0, 0).unwrap();
164        g.add_edge(0, 1).unwrap();
165        let d = to_directed(&g, ToDirectedMode::Mutual).unwrap();
166        // Self-loop: only one direction (src==tgt), regular edge: two
167        assert_eq!(d.ecount(), 3);
168    }
169
170    #[test]
171    fn test_self_loop_arbitrary() {
172        let mut g = Graph::with_vertices(2);
173        g.add_edge(0, 0).unwrap();
174        g.add_edge(0, 1).unwrap();
175        let d = to_directed(&g, ToDirectedMode::Arbitrary).unwrap();
176        assert_eq!(d.ecount(), 2);
177    }
178
179    #[test]
180    fn test_vcount_preserved() {
181        let mut g = Graph::with_vertices(10);
182        g.add_edge(3, 7).unwrap();
183        let d = to_directed(&g, ToDirectedMode::Mutual).unwrap();
184        assert_eq!(d.vcount(), 10);
185    }
186
187    #[test]
188    fn test_mutual_then_to_undirected_roundtrip() {
189        // to_directed(Mutual) then to_undirected(Collapse) should recover
190        // the same edge set
191        let mut g = Graph::with_vertices(4);
192        g.add_edge(0, 1).unwrap();
193        g.add_edge(1, 2).unwrap();
194        g.add_edge(2, 3).unwrap();
195        let d = to_directed(&g, ToDirectedMode::Mutual).unwrap();
196        assert_eq!(d.ecount(), 6);
197
198        // Collapse should give back 3 edges
199        let u = super::super::to_undirected::to_undirected(
200            &d,
201            super::super::to_undirected::ToUndirectedMode::Collapse,
202        )
203        .unwrap();
204        assert_eq!(u.ecount(), 3);
205    }
206}