1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
// Copyright (c) 2020 Delirious Penguin
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

//! This library can be used to fairly match students to categories (or activities). Or students to schools. Or anything to anything else.
//!
//! # Background
//!
//! The matching problem this library solves is mathematically know as the residence problem and is a subset of the [stable marriage problem](https://en.wikipedia.org/wiki/Stable_marriage_problem).
//! There are sereval known algorithms that solve this problem, each with there own pros and cons. For more information about the subject see also [Matching algorithms for the secondary school admission problem in Amsterdam](https://staff.fnwi.uva.nl/b.bredeweg/pdf/BSc/20152016/Klijnsma.pdf).
//!
//! # Algorithm
//!
//! At this time this library only implements the `Deferred Acceptance - Single Tie Break` algorithm. The library has been designed to make the implementation of other algorithms possible (it just needs to be done ;).
//!
//! # Usage
//!
//! ## Default matching
//!
//! Students are distributed over multiple categories, but each student can only be placed once.
//!
//! ```
//! use matchmaker::da_stb::match_students;
//! use matchmaker::{Category, Student};
//! use rand::thread_rng;
//! use std::collections::VecDeque;
//!
//! // Create categories
//! let cooking = Category::new("Cooking", 10);
//! let reading = Category::new("Reading", 10);
//! let walking = Category::new("Walking", 5);
//!
//! // Create student Bert
//! // Bert wishes to be placed in category cooking or reading (in that order)
//! let bert = Student::new(
//!     "Bert",
//!     VecDeque::from(vec![cooking.clone(), reading.clone()]),
//!     Vec::new(),
//! );
//!
//! // Create student Suze
//! // Suze wishes to be placed in category cooking or reading (in that order),
//! // but does not wish to be placed in category walking
//! let suze = Student::new(
//!     "Suze",
//!     VecDeque::from(vec![reading.clone(), cooking.clone()]),
//!     Vec::from([walking.clone()]),
//! );
//!
//! let mut rng = thread_rng();
//! let categories = Vec::from([cooking, reading, walking]);
//!
//! let match_result = match_students(Vec::from([bert, suze]), &categories, &mut rng);
//!
//! println!("Students matched to categories:");
//! println!();
//! for category in &categories {
//!     println!("{}:", &category.name);
//!     for student in match_result
//!         .placed
//!         .get(&category.name)
//!         .unwrap_or(&Vec::new())
//!     {
//!         println!(" - {}", &student.name);
//!     }
//! }
//!
//! if match_result.not_placable.is_empty() {
//!     println!();
//!     println!("All students could be placed.");
//! }
//! ```
//!
//! This should the following result:
//!
//! ```text
//! Students matched to categories:
//!
//! Cooking:
//!  - Bert
//! Reading:
//!  - Suze
//! Walking:
//!
//! All students could be placed.
//! ```
//!
//! ## Place students in multiple categories
//!
//! Students are distributed over multiple categories. A single student can be placed
//! in more than one category.
//!
//! # Example
//!
//! ```
//! use matchmaker::{Category, Student};
//! use matchmaker::da_stb::match_students_to_multiple_categories;
//! use rand::thread_rng;
//! use std::collections::VecDeque;
//!
//! // Create categories
//! let cooking = Category::new("Cooking", 10);
//! let reading = Category::new("Reading", 10);
//! let walking = Category::new("Walking", 5);
//!
//! // Create student Bert
//! // Bert wishes to be placed in category cooking or reading (in that order)
//! let bert = Student::new(
//!     "Bert",
//!     VecDeque::from(vec![cooking.clone(), reading.clone()]),
//!     Vec::new(),
//! );
//!
//! // Create student Suze
//! // Suze wishes to be placed in category cooking or reading (in that order),
//! // but does not wish to be placed in category walking
//! let suze = Student::new(
//!     "Suze",
//!     VecDeque::from(vec![cooking.clone(), reading.clone()]),
//!     Vec::from([walking.clone()]),
//! );
//!
//! let mut rng = thread_rng();
//! let categories = Vec::from([cooking, reading, walking]);
//!
//! let match_result = match_students_to_multiple_categories(
//!     Vec::from([bert, suze]),
//!     &categories,
//!     &mut rng);
//!
//! println!("Students matched to categories:");
//! println!();
//! for category in &categories {
//!     println!("{}:", &category.name);
//!     for student in match_result
//!         .placed
//!         .get(&category.name)
//!         .unwrap_or(&Vec::new())
//!     {
//!         println!(" - {}", &student.name);
//!     }
//! }
//!
//! if match_result.not_placable.is_empty() {
//!     println!();
//!     println!("All students could be placed.");
//! }
//! ```
//!
//! This should the following result:
//!
//! ```text
//! Students matched to categories:
//!
//! Cooking:
//!  - Suze
//!  - Bert
//! Reading:
//!  - Bert
//!  - Suze
//! Walking:
//!  - Bert
//!
//! All students could be placed.
//! ```
use core::fmt::Debug;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::{HashMap, VecDeque};

pub mod da_stb;

/// Holds a student
#[derive(Debug, Eq, Clone, Deserialize, Serialize)]
pub struct Student {
    /// Name of the student (must be unique)
    pub name: String,
    /// Categories the student wishes to be placed in, in order of preference
    pub preferences: VecDeque<Category>,
    /// Categories the student wishes *not* to be placed in
    pub exclude: Vec<Category>,
}

impl Student {
    /// Return a new Student
    ///
    /// # Arguments
    ///
    /// * `name` - A &`str` that holds the name of the student (must be unique)
    /// * `preferences` - A `VecDeque` of [`Category`]s the student wishes to be placed in, in order of preference
    /// * `exclude` - A `Vec` of [`Category`]s the student wishes *not* to be placed in
    ///
    /// [`Category`]: struct.Category.html
    ///
    /// # Examples
    ///
    /// ```
    /// use std::collections::VecDeque;
    /// use matchmaker::{Category, Student};
    ///
    /// // Create categories
    /// let cooking = Category::new("Cooking", 10);
    /// let reading = Category::new("Reading", 10);
    ///
    /// // Create student Bert
    /// // Bert wishes to be placed in category cooking or reading (in that order)
    /// let bert = Student::new(
    ///     "Bert",
    ///     VecDeque::from(vec![cooking, reading]),
    ///     Vec::new(),
    /// );
    /// ```
    ///
    /// ```
    /// use std::collections::VecDeque;
    /// use matchmaker::{Category, Student};
    ///
    /// let cooking = Category::new("Cooking", 10);
    /// let reading = Category::new("Reading", 10);
    /// let walking = Category::new("Walking", 5);
    ///
    /// // Create student Suze
    /// // Suze wishes to be placed in category cooking or reading (in that order),
    /// // but does not wish to be placed in category walking
    /// let suze = Student::new(
    ///     "Suze",
    ///     VecDeque::from(vec![cooking, reading]),
    ///     Vec::from([walking]),
    /// );
    /// ```
    pub fn new(name: &str, preferences: VecDeque<Category>, exclude: Vec<Category>) -> Self {
        Student {
            name: name.into(),
            preferences,
            exclude,
        }
    }
}

impl PartialEq for Student {
    fn eq(&self, other: &Self) -> bool {
        self.name == other.name
    }
}

impl From<OrderedStudent> for Student {
    fn from(os: OrderedStudent) -> Self {
        Student {
            name: os.name,
            preferences: os.preferences,
            exclude: os.exclude,
        }
    }
}

impl Ord for Student {
    fn cmp(&self, other: &Self) -> Ordering {
        self.name.cmp(&other.name)
    }
}

impl PartialOrd for Student {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

#[derive(Debug, PartialEq, Eq, Clone)]
struct OrderedStudent {
    name: String,
    preferences: VecDeque<Category>,
    exclude: Vec<Category>,
    order: usize,
}

impl Ord for OrderedStudent {
    fn cmp(&self, other: &Self) -> Ordering {
        self.order.cmp(&other.order)
    }
}

impl PartialOrd for OrderedStudent {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

/// Holds a category
#[derive(Clone, Deserialize, Serialize)]
pub struct Category {
    /// Name of the category (must be unique)
    pub name: String,
    /// Maximum number of students that can be placed in category this category
    pub max_placements: usize,
}

impl Category {
    /// Return a new `Category`
    ///
    /// # Arguments
    ///
    /// * `name` - Name of the category (must be unique)
    /// * `max_placements` - Maximum number of students that can be placed in category this category
    ///
    /// # Example
    ///
    /// ```
    /// use matchmaker::Category;
    ///
    /// // Category cooking with capacity of 10 placements
    /// let cooking = Category::new("Cooking", 10);
    ///
    /// // Category reading with capacity of 5 placements
    /// let reading = Category::new("Reading", 5);
    /// ```
    pub fn new(name: &str, max_placements: usize) -> Self {
        Category {
            name: name.into(),
            max_placements,
        }
    }
}

impl std::hash::Hash for Category {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.name.hash(state);
    }
}

impl PartialEq for Category {
    fn eq(&self, other: &Self) -> bool {
        self.name == other.name
    }
}

impl Eq for Category {}

impl Debug for Category {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_fmt(format_args!("{} ({})", self.name, self.max_placements))
    }
}

/// Holds the result of a match
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct MatchResult {
    /// Hashmap containing a list of placed students per category name
    pub placed: HashMap<String, Vec<Student>>,
    /// List of students that could not be placed in any category
    pub not_placable: Vec<Student>,
}

impl MatchResult {
    fn from(
        mut placed: HashMap<String, Vec<OrderedStudent>>,
        not_placable: Vec<OrderedStudent>,
    ) -> Self {
        let mut new_placed = HashMap::with_capacity(placed.capacity());
        let mut new_not_placable = Vec::with_capacity(not_placable.capacity());

        for (key, value) in placed.iter_mut() {
            let ordered_students = std::mem::replace(value, Vec::new());
            let students: Vec<Student> = ordered_students.into_iter().map(|os| os.into()).collect();
            new_placed.insert(key.clone(), students);
        }

        for np in not_placable.into_iter() {
            new_not_placable.push(np.into());
        }

        MatchResult {
            placed: new_placed,
            not_placable: new_not_placable,
        }
    }
}