use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
pub fn ring_graph(n: u32, directed: bool, mutual: bool, circular: bool) -> IgraphResult<Graph> {
if n == 0 {
return Graph::new(0, directed);
}
let n_usize = n as usize;
let forward_edges = if circular { n_usize } else { n_usize - 1 };
let edges_per_link = if directed && mutual { 2 } else { 1 };
let total_edges = forward_edges
.checked_mul(edges_per_link)
.ok_or(IgraphError::Internal("ring_graph edge count overflow"))?;
let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(total_edges);
for i in 0..n.saturating_sub(1) {
let next = i + 1;
edges.push((i, next));
if directed && mutual {
edges.push((next, i));
}
}
if circular {
let last = n - 1;
edges.push((last, 0));
if directed && mutual {
edges.push((0, last));
}
}
let mut g = Graph::new(n, directed)?;
g.add_edges(edges)?;
Ok(g)
}
pub fn path_graph(n: u32, directed: bool, mutual: bool) -> IgraphResult<Graph> {
ring_graph(n, directed, mutual, false)
}
pub fn cycle_graph(n: u32, directed: bool, mutual: bool) -> IgraphResult<Graph> {
ring_graph(n, directed, mutual, true)
}
#[cfg(test)]
mod tests {
use super::*;
fn collect_edges(g: &Graph) -> Vec<(VertexId, VertexId)> {
let m = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
(0..m)
.map(|eid| g.edge(eid).expect("edge id in bounds"))
.collect()
}
#[test]
fn empty_graph() {
for &directed in &[false, true] {
for &mutual in &[false, true] {
for &circular in &[false, true] {
let g = ring_graph(0, directed, mutual, circular).expect("ring n=0");
assert_eq!(g.vcount(), 0);
assert_eq!(g.ecount(), 0);
assert_eq!(g.is_directed(), directed);
}
}
}
}
#[test]
fn singleton_path_is_isolated_vertex() {
let g = ring_graph(1, false, false, false).expect("ring n=1 path");
assert_eq!(g.vcount(), 1);
assert_eq!(g.ecount(), 0);
}
#[test]
fn singleton_cycle_is_self_loop() {
let g = ring_graph(1, false, false, true).expect("ring n=1 cycle");
assert_eq!(g.vcount(), 1);
assert_eq!(g.ecount(), 1);
assert_eq!(collect_edges(&g), vec![(0, 0)]);
}
#[test]
fn two_vertex_undirected_cycle_has_parallel_edges() {
let g = ring_graph(2, false, false, true).expect("ring n=2 undirected cycle");
assert_eq!(g.ecount(), 2);
assert_eq!(collect_edges(&g), vec![(0, 1), (0, 1)]);
}
#[test]
fn undirected_path_p5_canonical_edges() {
let g = ring_graph(5, false, false, false).expect("ring P5");
assert_eq!(g.vcount(), 5);
assert_eq!(g.ecount(), 4);
assert!(!g.is_directed());
assert_eq!(collect_edges(&g), vec![(0, 1), (1, 2), (2, 3), (3, 4)]);
}
#[test]
fn undirected_cycle_c5_closes_with_wraparound() {
let g = ring_graph(5, false, false, true).expect("ring C5");
assert_eq!(g.ecount(), 5);
assert_eq!(
collect_edges(&g),
vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4)]
);
}
#[test]
fn directed_path_p4_forward_only() {
let g = ring_graph(4, true, false, false).expect("ring directed P4");
assert!(g.is_directed());
assert_eq!(collect_edges(&g), vec![(0, 1), (1, 2), (2, 3)]);
}
#[test]
fn directed_cycle_c4_forward_only() {
let g = ring_graph(4, true, false, true).expect("ring directed C4");
assert_eq!(collect_edges(&g), vec![(0, 1), (1, 2), (2, 3), (3, 0)]);
}
#[test]
fn directed_mutual_path_emits_back_arcs_in_order() {
let g = ring_graph(4, true, true, false).expect("ring directed mutual P4");
assert_eq!(g.ecount(), 6);
assert_eq!(
collect_edges(&g),
vec![(0, 1), (1, 0), (1, 2), (2, 1), (2, 3), (3, 2)]
);
}
#[test]
fn directed_mutual_cycle_emits_wraparound_back_arc_last() {
let g = ring_graph(4, true, true, true).expect("ring directed mutual C4");
assert_eq!(g.ecount(), 8);
assert_eq!(
collect_edges(&g),
vec![
(0, 1),
(1, 0),
(1, 2),
(2, 1),
(2, 3),
(3, 2),
(3, 0),
(0, 3),
]
);
}
#[test]
fn undirected_ignores_mutual_flag() {
for &circular in &[false, true] {
let g_no_mutual = ring_graph(6, false, false, circular).expect("undirected no mutual");
let g_mutual = ring_graph(6, false, true, circular).expect("undirected w/ mutual");
assert_eq!(collect_edges(&g_no_mutual), collect_edges(&g_mutual));
}
}
#[test]
fn path_wrapper_matches_ring_circular_false() {
for &directed in &[false, true] {
for &mutual in &[false, true] {
let a = path_graph(7, directed, mutual).expect("path_graph");
let b = ring_graph(7, directed, mutual, false).expect("ring circular=false");
assert_eq!(collect_edges(&a), collect_edges(&b));
assert_eq!(a.is_directed(), b.is_directed());
}
}
}
#[test]
fn cycle_wrapper_matches_ring_circular_true() {
for &directed in &[false, true] {
for &mutual in &[false, true] {
let a = cycle_graph(8, directed, mutual).expect("cycle_graph");
let b = ring_graph(8, directed, mutual, true).expect("ring circular=true");
assert_eq!(collect_edges(&a), collect_edges(&b));
assert_eq!(a.is_directed(), b.is_directed());
}
}
}
#[test]
fn vertex_degrees_path_undirected() {
let g = ring_graph(10, false, false, false).expect("undirected P10");
for v in 0..10u32 {
let deg = g.neighbors(v).expect("neighbors").len();
let expected = if v == 0 || v == 9 { 1 } else { 2 };
assert_eq!(deg, expected, "vertex {v}");
}
}
#[test]
fn vertex_degrees_cycle_undirected() {
let g = ring_graph(10, false, false, true).expect("undirected C10");
for v in 0..10u32 {
let deg = g.neighbors(v).expect("neighbors").len();
assert_eq!(deg, 2, "vertex {v} (cycle)");
}
}
}
#[cfg(all(test, feature = "proptest-harness"))]
mod proptest_tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn ring_edge_count_matches_formula(
n in 0u32..40,
directed in any::<bool>(),
mutual in any::<bool>(),
circular in any::<bool>(),
) {
let g = ring_graph(n, directed, mutual, circular).unwrap();
prop_assert_eq!(g.vcount(), n);
prop_assert_eq!(g.is_directed(), directed);
let expected = if n == 0 {
0
} else {
let base = if circular { n } else { n - 1 };
let factor = if directed && mutual { 2 } else { 1 };
base * factor
};
prop_assert_eq!(u32::try_from(g.ecount()).unwrap(), expected);
}
#[test]
fn undirected_ignores_mutual_property(
n in 0u32..40,
circular in any::<bool>(),
) {
let a = ring_graph(n, false, false, circular).unwrap();
let b = ring_graph(n, false, true, circular).unwrap();
let m_a = u32::try_from(a.ecount()).unwrap();
let m_b = u32::try_from(b.ecount()).unwrap();
let edges_a: Vec<_> = (0..m_a).map(|e| a.edge(e).unwrap()).collect();
let edges_b: Vec<_> = (0..m_b).map(|e| b.edge(e).unwrap()).collect();
prop_assert_eq!(edges_a, edges_b);
}
#[test]
fn path_cycle_wrappers_agree_with_ring(
n in 0u32..40,
directed in any::<bool>(),
mutual in any::<bool>(),
) {
let p_wrap = path_graph(n, directed, mutual).unwrap();
let p_ring = ring_graph(n, directed, mutual, false).unwrap();
let c_wrap = cycle_graph(n, directed, mutual).unwrap();
let c_ring = ring_graph(n, directed, mutual, true).unwrap();
let collect = |g: &Graph| -> Vec<(VertexId, VertexId)> {
let m = u32::try_from(g.ecount()).unwrap();
(0..m).map(|e| g.edge(e).unwrap()).collect()
};
prop_assert_eq!(collect(&p_wrap), collect(&p_ring));
prop_assert_eq!(collect(&c_wrap), collect(&c_ring));
}
}
}