Festive Code - Secret Santa Selector (TM)
Every year, a group of friends and I run a Secret Santa. If you’ve never heard of this concept, from Wikipedia:
Secret Santa is a Western Christmas tradition in which members of a group or community are randomly assigned a person to whom they anonymously give a gift.
In previous years we would simply put all the names on pieces of paper into a hat and each person would pick out their intended recipient. Sounds simple enough – however there are a couple of rules (one obvious, one not so):
- You can’t have yourself as the recipient (that’s the obvious one)
- A few of us are in couples, and we decided that you can’t buy for your partner
So, if your draw from the hat is either of the above, you need to pick again.
We would probably have done the same again this year, but we couldn’t all get together early enough in December to allow us enough time to buy presents before we met up to give them out on 20th. One suggestion was that one person would draw the names from the hat and email individuals telling them their recipient – but that would have meant breaking the secrecy rule, and that’s just not on. So instead, I volunteered to write an app to do it instead.
The basic flow I originally had in mind was:
- Pick the next “santa” from the people who haven’t been chosen as santa
- Remove this santa and partner from the people who haven’t been chosen as a recipient
- Randomly pick a recipient
- Repeat 1-3 until everybody has been picked.
- Email all the santas telling them their chosen recipients
It was only when I started thinking about the problem that I realised that you could end up with an impossible situation – eg. there is only one santa left and the only recipient is himself, or there are 2 partners left – I guess we’d just never had that when doing it manually. So, I had to allow for this and potentially re-run the selection process. I also noticed that picking the santas in order meant the selections were more predictable than they should be, so I ended up choosing them randomly (more like the manual drawing from a hat).
Although it seems like complete overkill for anyone that isn’t sold on TDD the app was developed in a test-driven styley – I’m so used to coding that way that it doesn’t add any time, and I wanted to check things such as not spamming all my friends with nonsense emails! I’ve open-sourced the code on bitbucket in case anybody else finds it useful.
The key classes are described below:
SantaSelector
This basically implements the loop above: randomly picking a santa and using a recipient picker to choose the recipient. As mentioned, it allows for reaching an impossible situation and will re-run (a maximum of 100 times) if that happens. Without logging code, it looks like this:
public IEnumerable<SantaSelection> SelectSantas(IEnumerable<SantaPerson> participants) { var selections = new List<SantaSelection>(); for (int i = 0; i < MaximumRecipientAttempts; i++) { selections.Clear(); var santas = new List<SantaPerson>(participants); var availableRecipients = new List<SantaPerson>(santas); if (availableRecipients.Count < 2) { throw new SecretSantaException("There needs to be at least 2 participants"); } var santaCount = santas.Count; for (int santaNumber = 0; santaNumber < santaCount; santaNumber++) { var santa = santas[_randomGenerator.Generate(0, santas.Count - 1)]; santas.Remove(santa); var recipient = _recipientPicker.SelectRecipient(santa, availableRecipients); if (recipient == null) { break; } selections.Add(new SantaSelection { Santa = santa, Receiver = recipient }); availableRecipients.Remove(recipient); } if (selections.Count == santaCount) { return selections; } } throw new SecretSantaException( String.Format("Failed to find recipients after {0} attempts", MaximumRecipientAttempts)); }
As you can see, choosing a recipient is delegated to another object – this class doesn’t (and shouldn’t) care about how that is done. That means in my tests I can force the recipient picker to behave in different ways, and check that the selector works as intended. Single responsibility, baby!
RandomRecipientPicker
This chooses a recipient, at random, for the supplied santa (ie. it doesn’t choose santa or santa’s partner). It’s fairly straightforward really:
public SantaPerson SelectRecipient(SantaPerson santa, IEnumerable<SantaPerson> participants) { var possibleRecipients = new List<SantaPerson>(participants); possibleRecipients.RemoveAll(p => ((p.Name == santa.Name) || (p.Partner == santa.Name))); if (possibleRecipients.Count < 1) { return null; } if (possibleRecipients.Count == 1) { return possibleRecipients[0]; } var selectedRecipientIndex = _randomGenerator.Generate(0, possibleRecipients.Count - 1); return possibleRecipients[selectedRecipientIndex]; }
Again, choosing a random number is someone else’s responsibility.
SmtpMailSender
Once the santas and recipients have been chosen, the app then sends the emails. The SmtpMailSender simply forms up the message to/from/subject/body and then delegates sending to an ISmtpClient. As I’m using an IoC container (LinFu in this case) it is trivial to switch in a dummy SMTP client in debug mode to stop emails actually being sent.
Summary
While this was a fairly simple problem, it’s nice to do some coding for fun. I bet you didn’t realise Secret Santa could be so complicated :)
Oh, and my Santa got me a great present.
Merry Christmas!