Skip to main content

pickems/datatypes/
teams.rs

1use std::collections::BTreeMap;
2use std::{fs::read_to_string, io::Write, path::PathBuf};
3
4use anyhow::anyhow;
5use serde::{Deserialize, Serialize};
6
7use crate::datatypes::{Index, Name, Rating, Seed, Set};
8
9/// Input data for a single team.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Team {
12    /// Initial one-based tournament seed.
13    pub seed: Seed,
14    /// Rating points used by the simulation model.
15    pub rating: Rating,
16}
17
18/// TOML-friendly collection of teams keyed by team name.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct Map(BTreeMap<Name, Team>);
21
22impl Map {
23    /// Parse a team map from a TOML file.
24    pub fn parse_toml(filepath: PathBuf) -> anyhow::Result<Self> {
25        Ok(toml::from_str::<Self>(&read_to_string(filepath)?)?)
26    }
27
28    /// Write a team map to a TOML file.
29    pub fn write_toml(&self, filepath: PathBuf) -> anyhow::Result<()> {
30        let contents = toml::to_string_pretty(self)?;
31
32        std::fs::OpenOptions::new()
33            .write(true)
34            .create(true)
35            .truncate(true)
36            .open(filepath)?
37            .write_all(contents.as_bytes())?;
38
39        Ok(())
40    }
41}
42
43impl<I: IntoIterator<Item = (Name, Team)>> From<I> for Map {
44    fn from(teams: I) -> Self {
45        Self(teams.into_iter().collect())
46    }
47}
48
49/// Seed-ordered team data optimized for simulation.
50///
51/// The arrays are indexed by [`Index`], so element `0` corresponds to seed `1`.
52#[derive(Debug, Clone)]
53pub struct Teams {
54    /// Team names sorted by ascending initial seed.
55    pub names: [Name; 16],
56    /// Team ratings sorted by ascending initial seed.
57    pub ratings: [Rating; 16],
58}
59
60impl Teams {
61    /// Produce dummy data for testing purposes.
62    #[must_use]
63    pub fn dummy() -> Self {
64        // Safety: names and ratings produced are all valid
65        unsafe {
66            Self {
67                names: std::array::from_fn(|i| Name::new_unchecked(format!("Team {}", i + 1))),
68                ratings: std::array::from_fn(|i| Rating::new_unchecked(2000 - 50 * i as u16)),
69            }
70        }
71    }
72
73    /// Parse, validate, and convert TOML input into seed-ordered team arrays.
74    pub fn parse_toml(filepath: PathBuf) -> anyhow::Result<Self> {
75        Self::try_from(Map::parse_toml(filepath)?)
76    }
77
78    /// Convert seed-ordered team arrays back into TOML input format.
79    pub fn write_toml(&self, filepath: PathBuf) -> anyhow::Result<()> {
80        let map = Map::from(self);
81        map.write_toml(filepath)
82    }
83}
84
85impl TryFrom<Map> for Teams {
86    type Error = anyhow::Error;
87
88    fn try_from(teams_map: Map) -> Result<Self, Self::Error> {
89        // Convert seeds to a bitset first so we can cheaply detect missing
90        // seeds before doing the more expensive duplicate check.
91        let set = teams_map
92            .0
93            .values()
94            .map(|team| Index::from(team.seed))
95            .collect::<Set>();
96
97        if set != Set::full() {
98            for i in Index::iter_all() {
99                if !set.contains(i) {
100                    return Err(anyhow!("missing seed: {}", Seed::from(i)));
101                }
102            }
103
104            // If no seed is missing but the set is still not complete, at least
105            // one seed was duplicated.
106            let indices = teams_map
107                .0
108                .values()
109                .map(|team| Index::from(team.seed))
110                .collect::<Vec<_>>();
111
112            for i in Index::iter_all() {
113                if indices.iter().filter(|&&index| index == i).count() > 1 {
114                    return Err(anyhow!("duplicate seed: {}", Seed::from(i)));
115                }
116            }
117        }
118
119        if teams_map.0.len() != 16 {
120            return Err(anyhow!(
121                "there must be exactly 16 teams ({} teams recognised)",
122                teams_map.0.len(),
123            ));
124        }
125
126        // Sorting here establishes the central invariant for `Teams`: every
127        // parallel array is indexed by zero-based initial seed.
128        let mut teams = teams_map.0.into_iter().collect::<Vec<_>>();
129        teams.sort_by_key(|(_, data)| data.seed);
130
131        let teams = teams
132            .into_iter()
133            .map(|(name, data)| (name, data.rating))
134            .collect::<Vec<_>>();
135
136        let ratings = teams
137            .iter()
138            .map(|(_, rating)| *rating)
139            .collect::<Vec<_>>()
140            .try_into()
141            .map_err(|_| anyhow!("failed to allocate array"))?;
142
143        let names = teams
144            .into_iter()
145            .map(|(name, _)| name)
146            .collect::<Vec<_>>()
147            .try_into()
148            .map_err(|_| anyhow!("failed to allocate array"))?;
149
150        Ok(Self { names, ratings })
151    }
152}
153
154impl From<&Teams> for Map {
155    fn from(teams: &Teams) -> Self {
156        Self(
157            (0..16)
158                .map(|i| {
159                    (
160                        teams.names[i].clone(),
161                        Team {
162                            seed: Seed::try_new(i as u16 + 1).unwrap(),
163                            rating: teams.ratings[i],
164                        },
165                    )
166                })
167                .collect(),
168        )
169    }
170}