rusty_santa/
lib.rs

1//! Rusty Santa
2//! 
3//! A small Rust library (and command-line tool) for resolving [Secret
4//! Santa](https://en.wikipedia.org/wiki/Secret_Santa) assignments with additional
5//! constraints.
6//!
7//! It is possible to add the following constraints to the random assignments:
8//!
9//! - Person A and B should not draw each other (e.g. for couples)
10//! - Person A should not draw person B (e.g. if person B already received a gift
11//!   from person A the previous year, or if person A dislikes person B)
12//!
13//! While this is an interesting mathematical problem and can be solved with
14//! bipartite graphs and the hungarian algorithm, this library sticks to a simpler
15//! approach and tries to emulate the real name drawing from a basket. Up to 1000
16//! attempts are made at resolving the name assignments without conflict until the
17//! algorithm fails.
18#![doc(html_logo_url = "https://github.com/dbrgn/rusty-santa/raw/master/logo.png")]
19
20#[macro_use]
21extern crate log;
22extern crate rand;
23
24use std::collections::{HashMap, HashSet};
25
26use rand::{thread_rng, Rng};
27
28#[derive(Clone)]
29struct Matrix {
30    keys: Vec<String>,
31    indexes: HashMap<String, usize>,
32    data: Vec<Vec<bool>>,
33}
34
35impl Matrix {
36    pub fn new(keys: Vec<String>) -> Self {
37        // Get size of matrix
38        let size = keys.len();
39
40        // Initialize indexes lookup map
41        let mut indexes = HashMap::new();
42        for (i, key) in keys.iter().enumerate() {
43            indexes.insert(key.clone(), i);
44        }
45
46        // Initialize data vectors
47        let mut data = vec![vec![true; size]; size];
48
49        // Disallow giving gifts to oneself
50        for i in 0..size {
51            data[i][i] = false;
52        }
53
54        Matrix {
55            keys: keys,
56            indexes: indexes,
57            data: data,
58        }
59    }
60
61    /// Get the matrix value at the specified coordinates.
62    ///
63    /// Panics if the x or y keys are invalid.
64    pub fn get(&self, x: &str, y: &str) -> bool {
65        let ix = self.indexes.get(x).unwrap();
66        let iy = self.indexes.get(y).unwrap();
67        self.data[*ix][*iy]
68    }
69
70    /// Get the matrix row at the specified key.
71    ///
72    /// Panics if the key is invalid.
73    pub fn get_row(&self, x: &str) -> Vec<bool> {
74        let ix = self.indexes.get(x).unwrap();
75        self.data[*ix].clone()
76    }
77
78    /// Set the field at coordinates x/y.
79    ///
80    /// Panics if the x or y keys are invalid.
81    pub fn set(&mut self, x: &str, y: &str, val: bool) {
82        let ix = self.indexes.get(x).unwrap();
83        let iy = self.indexes.get(y).unwrap();
84        self.data[*ix][*iy] = val;
85    }
86
87    /// Set every value at the specified column.
88    ///
89    /// Panics if the key is invalid.
90    pub fn set_col(&mut self, y: &str, val: bool) {
91        let iy = self.indexes.get(y).unwrap();
92        for row in self.data.iter_mut() {
93            row[*iy] = val;
94        }
95    }
96
97    /// Return whether the key is contained in the matrix.
98    pub fn contains(&mut self, key: &str) -> bool {
99        self.indexes.contains_key(key)
100    }
101
102    /// Return the matrix size.
103    pub fn size(&self) -> usize {
104        self.keys.len()
105    }
106
107    /// Return the key at the specified index.
108    pub fn key_at(&self, index: usize) -> &str {
109        &self.keys[index]
110    }
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
114enum Constraint {
115    ExcludePair {
116        a: String,
117        b: String,
118    },
119    Exclude {
120        from: String,
121        to: String,
122    },
123}
124
125/// A group of people that wants to draw names.
126#[derive(Debug, Clone)]
127pub struct Group {
128    people_set: HashSet<String>,
129    constraints: Vec<Constraint>,
130
131    /// When trying to resolve group assignments, try up to `max_attempts`
132    /// times until giving up.
133    max_attempts: u32,
134}
135
136impl Group {
137    /// Create a new `Group`.
138    pub fn new() -> Self {
139        Group {
140            people_set: HashSet::new(),
141            constraints: vec![],
142            max_attempts: 1000,
143        }
144    }
145
146    /// Add a name to the group.
147    pub fn add(&mut self, name: String) {
148        self.people_set.insert(name);
149    }
150
151    fn add_constraint(&mut self, constraint: Constraint) {
152        self.constraints.push(constraint);
153    }
154
155    /// Make sure that person A does not have to give person B a gift.
156    pub fn exclude(&mut self, from: String, to: String) {
157        let constraint = Constraint::Exclude { from: from, to: to };
158        self.add_constraint(constraint);
159    }
160
161    /// Make sure that person A and B don't have to give each other gifts.
162    pub fn exclude_pair(&mut self, a: String, b: String) {
163        let constraint = Constraint::ExcludePair { a: a, b: b };
164        self.add_constraint(constraint);
165    }
166
167    /// Return whether the specified name is alread in the group.
168    pub fn contains_name(&self, name: &str) -> bool {
169        self.people_set.contains(name)
170    }
171
172    /// Run the name assignment!
173    pub fn assign(&self) -> Result<Vec<(String, String)>, AssignError> {
174        // Initialize the random number generator
175        let mut rng = thread_rng();
176
177        // Shuffle the people
178        let mut people: Vec<String> = self.people_set.iter().cloned().collect();
179        rng.shuffle(&mut people);
180
181        'attempt: for _ in 0..self.max_attempts {
182
183            // Initialize the gift possibility matrix
184            let mut matrix = Matrix::new(people.clone());
185
186            // Iterate over constraints, apply them to the matrix
187            for constraint in self.constraints.iter() {
188                match constraint {
189                    &Constraint::ExcludePair{ ref a, ref b } => {
190                        if !matrix.contains(a) {
191                            return Err(AssignError::BadConstraint(format!("Unknown person \"{}\"", a)));
192                        }
193                        if !matrix.contains(b) {
194                            return Err(AssignError::BadConstraint(format!("Unknown person \"{}\"", b)));
195                        }
196                        matrix.set(a, b, false);
197                        matrix.set(b, a, false);
198                    },
199                    &Constraint::Exclude { ref from, ref to } => {
200                        if !matrix.contains(from) {
201                            return Err(AssignError::BadConstraint(format!("Unknown person \"{}\"", from)));
202                        }
203                        if !matrix.contains(to) {
204                            return Err(AssignError::BadConstraint(format!("Unknown person \"{}\"", to)));
205                        }
206                        matrix.set(from, to, false);
207                    }
208                }
209            };
210
211            let mut assignments = vec![];
212            for person in people.iter() {
213                trace!("Drawing recipient for {}", person);
214
215                // Get the possible names
216                let mut basket = vec![];
217                {
218                    let row = matrix.get_row(&person);
219                    for i in 0..row.len() {
220                        if row[i] {
221                            basket.push(matrix.key_at(i).to_owned());
222                        }
223                    }
224                }
225                trace!("Options: {:?}", basket);
226
227                // Draw a random name
228                if basket.is_empty() {
229                    trace!("Attempt failed. Retrying...");
230                    continue 'attempt;
231                }
232                let choice = rng.choose(&basket).unwrap();
233                trace!("Picked {}!", choice);
234
235                // Clear that person as a receiver from all rows
236                matrix.set_col(choice, false);
237
238                // Register assignment
239                assignments.push((person.clone(), choice.clone()));
240            }
241
242            return Ok(assignments);
243        }
244        return Err(AssignError::GivingUp)
245    }
246}
247
248/// Errors that can happen while assigning names.
249#[derive(Debug)]
250pub enum AssignError {
251    BadConstraint(String),
252    GivingUp,
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    #[test]
260    fn matrix_init() {
261        let keys = vec!["a".into(), "b".into(), "c".into()];
262        let matrix = Matrix::new(keys);
263
264        assert!(!matrix.get("a", "a"));
265        assert!(!matrix.get("b", "b"));
266        assert!(!matrix.get("c", "c"));
267
268        assert!(matrix.get("a", "b"));
269        assert!(matrix.get("a", "c"));
270        assert!(matrix.get("b", "a"));
271        assert!(matrix.get("b", "c"));
272        assert!(matrix.get("c", "a"));
273        assert!(matrix.get("c", "b"));
274    }
275
276    #[test]
277    fn matrix_get_row() {
278        let keys = vec!["a".into(), "b".into(), "c".into()];
279        let mut matrix = Matrix::new(keys);
280        assert_eq!(matrix.get_row("a"), vec![false, true, true]);
281        assert_eq!(matrix.get_row("b"), vec![true, false, true]);
282        assert_eq!(matrix.get_row("c"), vec![true, true, false]);
283    }
284
285    #[test]
286    fn matrix_set() {
287        let keys = vec!["a".into(), "b".into(), "c".into()];
288        let mut matrix = Matrix::new(keys);
289
290        assert!(matrix.get("a", "b"));
291        matrix.set("a", "b", false);
292        assert!(!matrix.get("a", "b"));
293        matrix.set("a", "b", true);
294        assert!(matrix.get("a", "b"));
295    }
296
297    #[test]
298    fn matrix_contains() {
299        let keys = vec!["a".into(), "b".into(), "c".into()];
300        let mut matrix = Matrix::new(keys);
301
302        assert!(matrix.contains("a"));
303        assert!(matrix.contains("b"));
304        assert!(matrix.contains("c"));
305        assert!(!matrix.contains("d"));
306        assert!(!matrix.contains("aa"));
307    }
308
309    #[test]
310    fn matrix_size() {
311        let keys = vec!["a".into(), "b".into(), "c".into()];
312        let matrix = Matrix::new(keys);
313        assert_eq!(3, matrix.size());
314
315        let keys = vec!["a".into()];
316        let matrix = Matrix::new(keys);
317        assert_eq!(1, matrix.size());
318    }
319
320    /// Test a simple group assignment.
321    #[test]
322    fn group_simple() {
323        let mut group = Group::new();
324
325        group.add("a".into());
326        group.add("b".into());
327        group.add("c".into());
328
329        let assignments = group.assign().unwrap();
330        assert_eq!(assignments.len(), 3);
331
332        for (from, to) in assignments {
333            match from.as_ref() {
334                "a" => assert!(to == "b" || to == "c"),
335                "b" => assert!(to == "a" || to == "c"),
336                "c" => assert!(to == "a" || to == "b"),
337                _ => panic!(),
338            }
339        }
340    }
341
342    /// Test a group constellation that may fail.
343    #[test]
344    fn group_may_fail() {
345        let mut group = Group::new();
346
347        group.add("Sheldon".into());
348        group.add("Amy".into());
349        group.add("Leonard".into());
350        group.add("Penny".into());
351        group.add("Rajesh".into());
352
353        group.exclude_pair("Sheldon".into(), "Amy".into());
354        group.exclude_pair("Sheldon".into(), "Leonard".into());
355        group.exclude_pair("Leonard".into(), "Penny".into());
356
357        for i in 0..1000 {
358            group.assign();
359        }
360    }
361}