buddy_up_lib/input/
mod.rs

1use crate::BuddyError;
2use std::collections::HashMap;
3use std::io::BufReader;
4use std::io::Read;
5use tracing::info;
6
7/// Abstraction over all the people you may want to pair up.
8/// Give it a impl [`Read`], like a file, to get [`People`] back.
9///
10/// Example:
11/// ```ignore
12/// # use std::fs::File;
13/// # use buddy_up_lib::People;
14/// let f = File::open("people.csv")?;
15/// let people = People::from_csv(f)?;
16/// ```
17#[derive(Clone, Debug, Default)]
18pub struct People {
19    people: HashMap<usize, String>,
20    evenizer: bool,
21}
22
23impl People {
24    /// Reads people from a CSV file and creates a `People` struct from that.
25    /// The expected format is rows of people like `id,name`.
26    ///
27    /// Example CSV:
28    /// ```text
29    /// 1,John
30    /// 2,David
31    /// ```
32    ///
33    /// If the given input doesn't contain an even number of people, we will add our own with id
34    /// `usize::MAX`, so that id is reserved.
35    /// Having that extra user to make it even will keep the algorithm working, so that someone
36    /// will be "paired up" with our evenizer, which really means that person won't get paired.
37    /// The beautiful thing is that the algorithm will try and not repeat pairs, which now includes
38    /// the evenizer, so the same person will not end up getting not paired all the time.
39    pub fn from_csv<R: Read>(input: R) -> Result<Self, BuddyError> {
40        let reader = BufReader::new(input);
41        let mut rdr = csv::ReaderBuilder::new()
42            .has_headers(false)
43            .from_reader(reader);
44        let mut people = HashMap::new();
45        let mut tr_input_len = 0;
46        for rec in rdr.records() {
47            tr_input_len += 1;
48            let r = rec?;
49            let id = str::parse::<usize>(r.get(0).ok_or(BuddyError::CsvFormatError)?)
50                .map_err(|_| BuddyError::IdNotANumber)?;
51            let name = r.get(1).ok_or(BuddyError::CsvFormatError)?.to_string();
52
53            people.insert(id, name);
54        }
55
56        // if these are not the same, the ids weren't unique
57        if people.len() != tr_input_len {
58            return Err(BuddyError::IdsNotUnique);
59        }
60        let ret = if people.len() % 2 != 0 {
61            people.insert(usize::MAX, "EVENIZER".to_string());
62            tracing::warn!(
63                "Input people are not even in number, so we can't pair everyone. One person will be left unpaired."
64            );
65            People {
66                people,
67                evenizer: true,
68            }
69        } else {
70            People {
71                people,
72                evenizer: false,
73            }
74        };
75
76        info!("Found {} records in input file.", ret.len());
77
78        Ok(ret)
79    }
80
81    pub fn len(&self) -> usize {
82        if self.has_evenizer() {
83            self.people.len() - 1
84        } else {
85            self.people.len()
86        }
87    }
88
89    /// Whether this People set has our evenizer user to make the count even.
90    pub fn has_evenizer(&self) -> bool {
91        self.evenizer
92    }
93
94    pub fn is_empty(&self) -> bool {
95        self.len() == 0
96    }
97
98    pub(crate) fn as_ids(&self) -> Vec<usize> {
99        self.people.keys().copied().collect()
100    }
101
102    pub(crate) fn name_from_id(&self, id: usize) -> Option<String> {
103        Some(self.people.get(&id)?.to_string())
104    }
105}
106
107#[cfg(test)]
108mod test {
109    use super::*;
110
111    // we'll add our evenizer, but keep the length the same as the original data
112    #[test]
113    fn not_even() {
114        let csv = "1,Foo".as_bytes();
115        let r = People::from_csv(csv);
116        assert!(r.is_ok());
117        let r = r.unwrap();
118        assert_eq!(r.len(), 1);
119        assert!(r.has_evenizer());
120    }
121    #[test]
122    fn good() {
123        let csv = "1,Foo\n2,Bar".as_bytes();
124        let r = People::from_csv(csv);
125        assert!(r.is_ok());
126        assert_eq!(r.unwrap().len(), 2);
127    }
128    #[test]
129    fn id_not_number() {
130        let csv = "Baz,Foo\n2,Bar".as_bytes();
131        let r = People::from_csv(csv);
132        assert!(matches!(r, Err(BuddyError::IdNotANumber)));
133    }
134    #[test]
135    fn id_not_unique() {
136        let csv = "1,Foo\n1,Bar".as_bytes();
137        let r = People::from_csv(csv);
138        assert!(matches!(r, Err(BuddyError::IdsNotUnique)));
139    }
140    #[test]
141    fn csv_format_wrong() {
142        let csv = "1\n2".as_bytes();
143        let r = People::from_csv(csv);
144        assert!(matches!(r, Err(BuddyError::CsvFormatError)));
145    }
146}