1use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
36
37pub fn ring_graph(n: u32, directed: bool, mutual: bool, circular: bool) -> IgraphResult<Graph> {
63 if n == 0 {
64 return Graph::new(0, directed);
65 }
66
67 let n_usize = n as usize;
68 let forward_edges = if circular { n_usize } else { n_usize - 1 };
69 let edges_per_link = if directed && mutual { 2 } else { 1 };
70 let total_edges = forward_edges
71 .checked_mul(edges_per_link)
72 .ok_or(IgraphError::Internal("ring_graph edge count overflow"))?;
73
74 let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(total_edges);
75
76 for i in 0..n.saturating_sub(1) {
77 let next = i + 1;
78 edges.push((i, next));
79 if directed && mutual {
80 edges.push((next, i));
81 }
82 }
83 if circular {
84 let last = n - 1;
85 edges.push((last, 0));
86 if directed && mutual {
87 edges.push((0, last));
88 }
89 }
90
91 let mut g = Graph::new(n, directed)?;
92 g.add_edges(edges)?;
93 Ok(g)
94}
95
96pub fn path_graph(n: u32, directed: bool, mutual: bool) -> IgraphResult<Graph> {
108 ring_graph(n, directed, mutual, false)
109}
110
111pub fn cycle_graph(n: u32, directed: bool, mutual: bool) -> IgraphResult<Graph> {
123 ring_graph(n, directed, mutual, true)
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 fn collect_edges(g: &Graph) -> Vec<(VertexId, VertexId)> {
131 let m = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
132 (0..m)
133 .map(|eid| g.edge(eid).expect("edge id in bounds"))
134 .collect()
135 }
136
137 #[test]
138 fn empty_graph() {
139 for &directed in &[false, true] {
140 for &mutual in &[false, true] {
141 for &circular in &[false, true] {
142 let g = ring_graph(0, directed, mutual, circular).expect("ring n=0");
143 assert_eq!(g.vcount(), 0);
144 assert_eq!(g.ecount(), 0);
145 assert_eq!(g.is_directed(), directed);
146 }
147 }
148 }
149 }
150
151 #[test]
152 fn singleton_path_is_isolated_vertex() {
153 let g = ring_graph(1, false, false, false).expect("ring n=1 path");
154 assert_eq!(g.vcount(), 1);
155 assert_eq!(g.ecount(), 0);
156 }
157
158 #[test]
159 fn singleton_cycle_is_self_loop() {
160 let g = ring_graph(1, false, false, true).expect("ring n=1 cycle");
161 assert_eq!(g.vcount(), 1);
162 assert_eq!(g.ecount(), 1);
163 assert_eq!(collect_edges(&g), vec![(0, 0)]);
164 }
165
166 #[test]
167 fn two_vertex_undirected_cycle_has_parallel_edges() {
168 let g = ring_graph(2, false, false, true).expect("ring n=2 undirected cycle");
169 assert_eq!(g.ecount(), 2);
171 assert_eq!(collect_edges(&g), vec![(0, 1), (0, 1)]);
172 }
173
174 #[test]
175 fn undirected_path_p5_canonical_edges() {
176 let g = ring_graph(5, false, false, false).expect("ring P5");
177 assert_eq!(g.vcount(), 5);
178 assert_eq!(g.ecount(), 4);
179 assert!(!g.is_directed());
180 assert_eq!(collect_edges(&g), vec![(0, 1), (1, 2), (2, 3), (3, 4)]);
181 }
182
183 #[test]
184 fn undirected_cycle_c5_closes_with_wraparound() {
185 let g = ring_graph(5, false, false, true).expect("ring C5");
186 assert_eq!(g.ecount(), 5);
188 assert_eq!(
189 collect_edges(&g),
190 vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4)]
191 );
192 }
193
194 #[test]
195 fn directed_path_p4_forward_only() {
196 let g = ring_graph(4, true, false, false).expect("ring directed P4");
197 assert!(g.is_directed());
198 assert_eq!(collect_edges(&g), vec![(0, 1), (1, 2), (2, 3)]);
199 }
200
201 #[test]
202 fn directed_cycle_c4_forward_only() {
203 let g = ring_graph(4, true, false, true).expect("ring directed C4");
204 assert_eq!(collect_edges(&g), vec![(0, 1), (1, 2), (2, 3), (3, 0)]);
205 }
206
207 #[test]
208 fn directed_mutual_path_emits_back_arcs_in_order() {
209 let g = ring_graph(4, true, true, false).expect("ring directed mutual P4");
210 assert_eq!(g.ecount(), 6);
211 assert_eq!(
212 collect_edges(&g),
213 vec![(0, 1), (1, 0), (1, 2), (2, 1), (2, 3), (3, 2)]
214 );
215 }
216
217 #[test]
218 fn directed_mutual_cycle_emits_wraparound_back_arc_last() {
219 let g = ring_graph(4, true, true, true).expect("ring directed mutual C4");
220 assert_eq!(g.ecount(), 8);
221 assert_eq!(
222 collect_edges(&g),
223 vec![
224 (0, 1),
225 (1, 0),
226 (1, 2),
227 (2, 1),
228 (2, 3),
229 (3, 2),
230 (3, 0),
231 (0, 3),
232 ]
233 );
234 }
235
236 #[test]
237 fn undirected_ignores_mutual_flag() {
238 for &circular in &[false, true] {
241 let g_no_mutual = ring_graph(6, false, false, circular).expect("undirected no mutual");
242 let g_mutual = ring_graph(6, false, true, circular).expect("undirected w/ mutual");
243 assert_eq!(collect_edges(&g_no_mutual), collect_edges(&g_mutual));
244 }
245 }
246
247 #[test]
248 fn path_wrapper_matches_ring_circular_false() {
249 for &directed in &[false, true] {
250 for &mutual in &[false, true] {
251 let a = path_graph(7, directed, mutual).expect("path_graph");
252 let b = ring_graph(7, directed, mutual, false).expect("ring circular=false");
253 assert_eq!(collect_edges(&a), collect_edges(&b));
254 assert_eq!(a.is_directed(), b.is_directed());
255 }
256 }
257 }
258
259 #[test]
260 fn cycle_wrapper_matches_ring_circular_true() {
261 for &directed in &[false, true] {
262 for &mutual in &[false, true] {
263 let a = cycle_graph(8, directed, mutual).expect("cycle_graph");
264 let b = ring_graph(8, directed, mutual, true).expect("ring circular=true");
265 assert_eq!(collect_edges(&a), collect_edges(&b));
266 assert_eq!(a.is_directed(), b.is_directed());
267 }
268 }
269 }
270
271 #[test]
272 fn vertex_degrees_path_undirected() {
273 let g = ring_graph(10, false, false, false).expect("undirected P10");
274 for v in 0..10u32 {
276 let deg = g.neighbors(v).expect("neighbors").len();
277 let expected = if v == 0 || v == 9 { 1 } else { 2 };
278 assert_eq!(deg, expected, "vertex {v}");
279 }
280 }
281
282 #[test]
283 fn vertex_degrees_cycle_undirected() {
284 let g = ring_graph(10, false, false, true).expect("undirected C10");
285 for v in 0..10u32 {
286 let deg = g.neighbors(v).expect("neighbors").len();
287 assert_eq!(deg, 2, "vertex {v} (cycle)");
288 }
289 }
290}
291
292#[cfg(all(test, feature = "proptest-harness"))]
293mod proptest_tests {
294 use super::*;
295 use proptest::prelude::*;
296
297 proptest! {
298 #[test]
299 fn ring_edge_count_matches_formula(
300 n in 0u32..40,
301 directed in any::<bool>(),
302 mutual in any::<bool>(),
303 circular in any::<bool>(),
304 ) {
305 let g = ring_graph(n, directed, mutual, circular).unwrap();
306 prop_assert_eq!(g.vcount(), n);
307 prop_assert_eq!(g.is_directed(), directed);
308
309 let expected = if n == 0 {
310 0
311 } else {
312 let base = if circular { n } else { n - 1 };
313 let factor = if directed && mutual { 2 } else { 1 };
314 base * factor
315 };
316 prop_assert_eq!(u32::try_from(g.ecount()).unwrap(), expected);
317 }
318
319 #[test]
320 fn undirected_ignores_mutual_property(
321 n in 0u32..40,
322 circular in any::<bool>(),
323 ) {
324 let a = ring_graph(n, false, false, circular).unwrap();
325 let b = ring_graph(n, false, true, circular).unwrap();
326 let m_a = u32::try_from(a.ecount()).unwrap();
327 let m_b = u32::try_from(b.ecount()).unwrap();
328 let edges_a: Vec<_> = (0..m_a).map(|e| a.edge(e).unwrap()).collect();
329 let edges_b: Vec<_> = (0..m_b).map(|e| b.edge(e).unwrap()).collect();
330 prop_assert_eq!(edges_a, edges_b);
331 }
332
333 #[test]
334 fn path_cycle_wrappers_agree_with_ring(
335 n in 0u32..40,
336 directed in any::<bool>(),
337 mutual in any::<bool>(),
338 ) {
339 let p_wrap = path_graph(n, directed, mutual).unwrap();
340 let p_ring = ring_graph(n, directed, mutual, false).unwrap();
341 let c_wrap = cycle_graph(n, directed, mutual).unwrap();
342 let c_ring = ring_graph(n, directed, mutual, true).unwrap();
343
344 let collect = |g: &Graph| -> Vec<(VertexId, VertexId)> {
345 let m = u32::try_from(g.ecount()).unwrap();
346 (0..m).map(|e| g.edge(e).unwrap()).collect()
347 };
348
349 prop_assert_eq!(collect(&p_wrap), collect(&p_ring));
350 prop_assert_eq!(collect(&c_wrap), collect(&c_ring));
351 }
352 }
353}