1use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub enum StarMode {
42 Out,
44 In,
46 Mutual,
48 Undirected,
50}
51
52impl StarMode {
53 fn is_directed(self) -> bool {
54 !matches!(self, StarMode::Undirected)
55 }
56}
57
58pub fn star_graph(n: u32, mode: StarMode, center: u32) -> IgraphResult<Graph> {
81 if n == 0 {
82 return Graph::new(0, mode.is_directed());
83 }
84
85 if center >= n {
86 return Err(IgraphError::InvalidArgument(format!(
87 "star_graph: center vertex {center} is out of range for n = {n}"
88 )));
89 }
90
91 let directed = mode.is_directed();
92 let leaves = (n as usize) - 1;
93 let per_leaf = if matches!(mode, StarMode::Mutual) {
94 2
95 } else {
96 1
97 };
98 let total_edges = leaves
99 .checked_mul(per_leaf)
100 .ok_or(IgraphError::Internal("star_graph edge count overflow"))?;
101
102 let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(total_edges);
103
104 for leaf in 0..n {
105 if leaf == center {
106 continue;
107 }
108 match mode {
109 StarMode::Out => edges.push((center, leaf)),
110 StarMode::In | StarMode::Undirected => edges.push((leaf, center)),
111 StarMode::Mutual => {
112 edges.push((center, leaf));
113 edges.push((leaf, center));
114 }
115 }
116 }
117
118 let mut g = Graph::new(n, directed)?;
119 g.add_edges(edges)?;
120 Ok(g)
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 fn collect_edges(g: &Graph) -> Vec<(VertexId, VertexId)> {
128 let m = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
129 (0..m)
130 .map(|eid| g.edge(eid).expect("edge id in bounds"))
131 .collect()
132 }
133
134 #[test]
135 fn empty_graph_all_modes() {
136 for mode in [
137 StarMode::Out,
138 StarMode::In,
139 StarMode::Mutual,
140 StarMode::Undirected,
141 ] {
142 let g = star_graph(0, mode, 0).expect("star n=0");
143 assert_eq!(g.vcount(), 0);
144 assert_eq!(g.ecount(), 0);
145 assert_eq!(g.is_directed(), mode.is_directed());
146 }
147 }
148
149 #[test]
150 fn singleton_has_no_edges() {
151 for mode in [
152 StarMode::Out,
153 StarMode::In,
154 StarMode::Mutual,
155 StarMode::Undirected,
156 ] {
157 let g = star_graph(1, mode, 0).expect("star n=1");
158 assert_eq!(g.vcount(), 1);
159 assert_eq!(g.ecount(), 0);
160 }
161 }
162
163 #[test]
164 fn out_mode_emits_center_to_leaf_arcs() {
165 let g = star_graph(5, StarMode::Out, 0).expect("out star");
166 assert!(g.is_directed());
167 assert_eq!(g.ecount(), 4);
168 assert_eq!(collect_edges(&g), vec![(0, 1), (0, 2), (0, 3), (0, 4)]);
169 }
170
171 #[test]
172 fn in_mode_emits_leaf_to_center_arcs() {
173 let g = star_graph(5, StarMode::In, 0).expect("in star");
174 assert!(g.is_directed());
175 assert_eq!(g.ecount(), 4);
176 assert_eq!(collect_edges(&g), vec![(1, 0), (2, 0), (3, 0), (4, 0)]);
177 }
178
179 #[test]
180 fn mutual_mode_emits_both_arcs_per_leaf() {
181 let g = star_graph(4, StarMode::Mutual, 0).expect("mutual star");
182 assert!(g.is_directed());
183 assert_eq!(g.ecount(), 6);
184 assert_eq!(
186 collect_edges(&g),
187 vec![(0, 1), (1, 0), (0, 2), (2, 0), (0, 3), (3, 0)]
188 );
189 }
190
191 #[test]
192 fn undirected_mode_emits_one_edge_per_leaf() {
193 let g = star_graph(5, StarMode::Undirected, 0).expect("undirected star");
194 assert!(!g.is_directed());
195 assert_eq!(g.ecount(), 4);
196 assert_eq!(collect_edges(&g), vec![(0, 1), (0, 2), (0, 3), (0, 4)]);
198 }
199
200 #[test]
201 fn center_can_be_any_vertex_out_mode() {
202 let g = star_graph(5, StarMode::Out, 2).expect("centred at 2");
203 assert_eq!(collect_edges(&g), vec![(2, 0), (2, 1), (2, 3), (2, 4)]);
205 }
206
207 #[test]
208 fn center_can_be_any_vertex_undirected_mode() {
209 let g = star_graph(5, StarMode::Undirected, 3).expect("centred at 3");
210 assert_eq!(collect_edges(&g), vec![(0, 3), (1, 3), (2, 3), (3, 4)]);
213 }
214
215 #[test]
216 fn center_equal_to_n_minus_one_visits_only_lower_leaves() {
217 let g = star_graph(4, StarMode::Out, 3).expect("centred at last");
218 assert_eq!(g.ecount(), 3);
219 assert_eq!(collect_edges(&g), vec![(3, 0), (3, 1), (3, 2)]);
220 }
221
222 #[test]
223 fn center_out_of_range_errors() {
224 let err = star_graph(3, StarMode::Out, 3).unwrap_err();
225 assert!(matches!(err, IgraphError::InvalidArgument(_)));
226 let err = star_graph(3, StarMode::Out, 5).unwrap_err();
227 assert!(matches!(err, IgraphError::InvalidArgument(_)));
228 }
229
230 #[test]
231 fn center_zero_with_n_zero_is_empty_not_error() {
232 let g = star_graph(0, StarMode::Out, 0).expect("n=0 star");
234 assert_eq!(g.vcount(), 0);
235 assert_eq!(g.ecount(), 0);
236 }
237
238 #[test]
239 fn leaf_degrees_undirected_are_one_each() {
240 let g = star_graph(10, StarMode::Undirected, 0).expect("undirected K1,9");
241 for v in 0..10u32 {
242 let deg = g.neighbors(v).expect("neighbors").len();
243 let expected = if v == 0 { 9 } else { 1 };
244 assert_eq!(deg, expected, "vertex {v}");
245 }
246 }
247
248 #[test]
249 fn out_arcs_give_center_out_degree_and_leaves_in_degree_one() {
250 let g = star_graph(6, StarMode::Out, 0).expect("out star K1,5");
251 let center_out = g.neighbors(0).expect("center out").len();
253 assert_eq!(center_out, 5);
254 for v in 1..6u32 {
255 assert_eq!(g.neighbors(v).expect("leaf out").len(), 0);
256 }
257 }
258}
259
260#[cfg(all(test, feature = "proptest-harness"))]
261mod proptest_tests {
262 use super::*;
263 use proptest::prelude::*;
264
265 fn arb_mode() -> impl Strategy<Value = StarMode> {
266 prop_oneof![
267 Just(StarMode::Out),
268 Just(StarMode::In),
269 Just(StarMode::Mutual),
270 Just(StarMode::Undirected),
271 ]
272 }
273
274 proptest! {
275 #[test]
276 fn edge_count_matches_formula(
277 n in 0u32..40,
278 mode in arb_mode(),
279 center_raw in 0u32..40,
280 ) {
281 let center = if n == 0 { 0 } else { center_raw % n };
283 let g = star_graph(n, mode, center).unwrap();
284 prop_assert_eq!(g.vcount(), n);
285 prop_assert_eq!(g.is_directed(), mode.is_directed());
286
287 let leaves = if n == 0 { 0 } else { n - 1 };
288 let per_leaf = if matches!(mode, StarMode::Mutual) { 2 } else { 1 };
289 let expected = leaves * per_leaf;
290 let m = u32::try_from(g.ecount()).unwrap();
291 prop_assert_eq!(m, expected);
292 }
293
294 #[test]
295 fn invalid_center_returns_error(
296 n in 1u32..20,
297 mode in arb_mode(),
298 offset in 0u32..20,
299 ) {
300 let bad_center = n + offset;
301 prop_assert!(star_graph(n, mode, bad_center).is_err());
302 }
303
304 #[test]
305 fn every_leaf_is_connected_to_center(
306 n in 2u32..30,
307 mode in arb_mode(),
308 center_raw in 0u32..30,
309 ) {
310 let center = center_raw % n;
311 let g = star_graph(n, mode, center).unwrap();
312 let m = u32::try_from(g.ecount()).unwrap();
318 let edges: Vec<(VertexId, VertexId)> =
319 (0..m).map(|e| g.edge(e).unwrap()).collect();
320 for leaf in 0..n {
321 if leaf == center {
322 continue;
323 }
324 let found = edges.iter().any(|&(u, v)| {
325 (u == center && v == leaf) || (u == leaf && v == center)
326 });
327 prop_assert!(found, "leaf {leaf} missing edge with center {center}");
328 }
329 }
330 }
331}