1use fforge_domain::{ClubId, Fixture, FixtureId};
5
6pub fn double_round_robin(clubs: &[ClubId]) -> Vec<Fixture> {
10 let n = clubs.len();
11 assert!(n >= 2 && n.is_multiple_of(2), "need an even number of clubs, got {n}");
12 let rounds = (n - 1) as u8;
13 let mut fixtures = Vec::with_capacity(n * (n - 1));
14 let mut next_id = 0u32;
15
16 let mut ring: Vec<ClubId> = clubs[1..].to_vec();
18
19 let mut first_half: Vec<Vec<(ClubId, ClubId)>> = Vec::with_capacity(rounds as usize);
20 for round in 0..rounds {
21 let mut pairs = Vec::with_capacity(n / 2);
22 let (a, b) = (clubs[0], ring[0]);
24 if round % 2 == 0 {
25 pairs.push((a, b));
26 } else {
27 pairs.push((b, a));
28 }
29 for i in 1..n / 2 {
30 let x = ring[i];
31 let y = ring[n - 1 - i];
32 if round % 2 == 0 {
33 pairs.push((y, x));
34 } else {
35 pairs.push((x, y));
36 }
37 }
38 first_half.push(pairs);
39 ring.rotate_right(1);
40 }
41
42 for (round, pairs) in first_half.iter().enumerate() {
43 for &(home, away) in pairs {
44 fixtures.push(Fixture {
45 id: FixtureId(next_id),
46 matchday: round as u8 + 1,
47 home,
48 away,
49 });
50 next_id += 1;
51 }
52 }
53 for (round, pairs) in first_half.iter().enumerate() {
55 for &(home, away) in pairs {
56 fixtures.push(Fixture {
57 id: FixtureId(next_id),
58 matchday: rounds + round as u8 + 1,
59 home: away,
60 away: home,
61 });
62 next_id += 1;
63 }
64 }
65 fixtures
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use std::collections::BTreeMap;
72
73 #[test]
74 fn round_robin_properties() {
75 let clubs: Vec<ClubId> = (0..20).map(ClubId).collect();
76 let fixtures = double_round_robin(&clubs);
77 assert_eq!(fixtures.len(), 20 * 19);
78
79 let mut seen: BTreeMap<(ClubId, ClubId), u32> = BTreeMap::new();
81 for f in &fixtures {
82 assert_ne!(f.home, f.away);
83 *seen.entry((f.home, f.away)).or_default() += 1;
84 }
85 assert!(seen.values().all(|&c| c == 1));
86 assert_eq!(seen.len(), 20 * 19);
87
88 for md in 1..=38u8 {
90 let mut playing = Vec::new();
91 for f in fixtures.iter().filter(|f| f.matchday == md) {
92 playing.push(f.home);
93 playing.push(f.away);
94 }
95 playing.sort();
96 playing.dedup();
97 assert_eq!(playing.len(), 20, "matchday {md}");
98 }
99 }
100}