Solving The Secret Santa Selection "Problem"
My girlfriend’s family does a variation of secret santa for Christmas every year instead of the more traditional American “gift armageddon” strategy. I’m a fan because it encourages gift creativity and thoughtfulness while eliminating the crazy holiday consumerism that is prevalent here.
Every year the selection of giver-receiver pairs must be made and this can be a challenge because there are strict rules:
- everyone gives once, everyone receives once
- you can’t give to your partner
- you can’t give a gift to someone who is giving a gift to you
- it should be a secret until gift-giving time
Last year the selections were made by my girlfriend’s sister and Christmas was absolutely delightful. But then at some point afterward I opened my big fat mouth and said that a computer program should do the selections. The response was more or less “make it so.” Here’s my Pythonic attempt. For the web I’ve blanked out the PARTNERS map and my SMTP information. The rest is as I ran it.
My “Solution”
#!/usr/bin/python
""" Solve the complex issue of secret santa """
import random
import smtplib
import email.mime.text
import time
class TwoWayDict(dict):
""" dict subclass that provides a weak transparent reverse key search """
def __getitem__(self, search_key):
"""
Call dict's __getitem__, if that fails iterate through key value
pairs until we find a value matching the search_key
"""
try:
return super(TwoWayDict, self).__getitem__(search_key)
except KeyError:
for key, value in super(TwoWayDict, self).iteritems():
if value == search_key:
return key
raise
def __contains__(self, search_key):
""" Membership test based on our janky new __getitem__ method """
try:
self.__getitem__(search_key)
except KeyError:
return False
return True
def all(self):
""" Return a list of all keys and values """
return self.keys() + self.values()
def quick_mail(from_addr, to_addr, subject, content,
smtp_host, smtp_port=25, smtp_user=None, smtp_pass=None):
""" Send an SMTP email message """
if isinstance(to_addr, basestring):
to_addr = [to_addr]
# Create a text/plain message
message = email.mime.text.MIMEText(content)
message['From'] = from_addr
message['To'] = ", ".join(to_addr)
message['Subject'] = subject
message_date = time.ctime(time.time())
message.set_unixfrom("From {} {}".format(from_addr, message_date))
server = smtplib.SMTP(smtp_host, smtp_port)
if smtp_user and smtp_pass:
server.starttls()
server.login(smtp_user, smtp_pass)
server.sendmail(from_addr, to_addr, message.as_string() + "\n")
server.quit()
def choose_pairs(partners):
"""
Return a dictionary of giver:receiver pairs following STRICT RULES
"""
people = partners.all()
choices = TwoWayDict()
for giver in people:
exclusions = list()
# you can't give to yourself
exclusions.append(giver)
# you can't give to your partner
exclusions.append(partners[giver])
# you can't give to someone who is already getting a gift
exclusions.extend(choices.values())
# you can't give a gift to someone who is giving a gift to you
if giver in choices:
exclusions.append(choices[giver])
possible_receivers = set(people) - set(exclusions)
receiver = random.choice(list(possible_receivers))
choices[giver] = receiver
return choices
if __name__ == "__main__":
PARTNERS = TwoWayDict({
'K... <k...@example.com>': 'P... <p...@example.com>',
'R... <r...@example.com>': 'J... <j..@example.com>',
'F... <f...@example.com>': 'A... <a...@example.com>',
'F... <f...@example.com>': 'B... <b...@example.com>'
})
SMTP_HOST = 'smtp.example.com'
SMTP_PORT = 587
SMTP_USER = 'nobody@example.com'
SMTP_PASS = 'somereallygreatpassword'
SMTP_FROM = 'No Body <nobody@example.com>'
while True:
try:
CHOICES = choose_pairs(PARTNERS)
except IndexError:
# using random dice-throws, somtimes the only option left for the
# final giver is their partner, which is NOT ALLOWED, so if that
# happens, we just throw everything out and start over
pass
else:
break
for GIVER, RECEIVER in CHOICES.items():
MESSAGE = ("Hello {}. A computer has determined that this year you "
"will be giving a Christmas gift to {}. Nobody knows this "
"but you. Good luck!").format(GIVER.split()[0], RECEIVER)
quick_mail(SMTP_FROM, GIVER, "Secret Santa Calculation", MESSAGE,
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS)