tzot

Secret Santa algorithm

Nov 29th, 2017
294
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 4.54 KB | None | 0 0
  1. #!/usr/bin/python3 -O
  2.  
  3. import random
  4. import collections
  5. import codecs  # for obfuscation
  6.  
  7. OBFUSCATORS= "%$#^{}"
  8.  
  9. def obfuscate(text, max_len):
  10.     while len(text.encode("utf8")) < max_len:
  11.         text+= random.choice(OBFUSCATORS)
  12.     text= random.choice(OBFUSCATORS) + random.choice(OBFUSCATORS) + text
  13.     return codecs.encode(
  14.         codecs.encode(
  15.             codecs.encode(text, "rot13").encode("utf8"),
  16.             "base64").decode("utf8"),
  17.         "rot13")
  18.  
  19.  
  20. def deobfuscate(text):
  21.     return codecs.decode(
  22.         codecs.decode(
  23.             codecs.decode(text, "rot13").encode("utf8"),
  24.             "base64").decode("utf8"),
  25.         "rot13").rstrip(OBFUSCATORS)[2:]
  26.  
  27. class Statistics:
  28.  
  29.     def __init__(self):
  30.         self.tags= collections.defaultdict(int)
  31.  
  32.     def tags_value(self, tags):
  33.         return sum(1./self.tags[tag] for tag in tags)
  34.  
  35.     def account(self, tags):
  36.         for tag in tags:
  37.             self.tags[tag]+= 1
  38.  
  39.     def abbreviated_tags(self):
  40.         output= {}
  41.         abbreviations= set()
  42.         for tag in sorted(self.tags, key=len):
  43.             for prefixlen in range(1, len(tag)+1):
  44.                 prefix= tag[:prefixlen]
  45.                 if prefix not in abbreviations:
  46.                     abbreviations.add(prefix)
  47.                     output[tag]= prefix
  48.                     break
  49.         return output
  50.  
  51.     def most_disjoined(self, tags, groups):
  52.         return max(
  53.             groups.items(),
  54.             key=lambda kv: (
  55.                 -self.tags_value(kv[0] & tags),
  56.                 len(kv[1]),
  57.                 self.tags_value(tags - kv[0]) - self.tags_value(kv[0] - tags),
  58.             )
  59.         )
  60.  
  61.  
  62. class Person(collections.namedtuple("Person", "name tags")):
  63.     __slots__= ()
  64.  
  65.     def name_abbr_tags(self, abbreviated_tags):
  66.         return "%s [%s]" % (
  67.             self.name,
  68.             ",".join(abbreviated_tags[t] for t in self.tags),
  69.         )
  70.  
  71.  
  72. def secret_santa(people_and_their_tags, ignore_tags=()):
  73.     """Secret santa algorithm.
  74.  
  75.    The lottery function expects a sequence of:
  76.    (name, tags)
  77.  
  78.    For example:
  79.  
  80.    [
  81.        ("person1", ("male", "company1")),
  82.        ("person2", ("female", "company2")),
  83.        ("person3", ("male", "company1")),
  84.        ("husband1", ("male", "company2", "marriage1")),
  85.        ("wife1", ("female", "company1", "marriage1")),
  86.        ("husband2", ("male", "company3", "marriage2")),
  87.        ("wife2", ("female", "company2", "marriage2")),
  88.    ]
  89.  
  90.    husband1 is married to wife1 as seen by the common marriage1 tag
  91.    person1, person3 and wife1 work at the same company.
  92.  
  93.    The algorithm will try to match people with the least common characteristics
  94.    between them, to maximize entrop— ehm, mingling!
  95.  
  96.    Have fun."""
  97.  
  98.     # let's split the persons into groups
  99.    
  100.     groups= collections.defaultdict(list)
  101.     stats= Statistics()
  102.    
  103.     for person, tags in people_and_their_tags:
  104.         tags= frozenset(tag.lower() for tag in tags if tag not in ignore_tags)
  105.         stats.account(tags)
  106.         groups[tags].append(Person(person, tags))
  107.     del tags, person
  108.  
  109.     # shuffle all lists
  110.     for group in groups.values():
  111.         random.shuffle(group)
  112.     del group
  113.  
  114.     abbreviations= stats.abbreviated_tags()
  115.     output_chain= []
  116.     prev_tags= frozenset()
  117.     while 1:
  118.         next_tags, next_group= stats.most_disjoined(prev_tags, groups)
  119.         next_person= next_group.pop()
  120.         output_chain.append(next_person.name_abbr_tags(abbreviations))
  121.         if not next_group: # it just got empty
  122.             del groups[next_tags]
  123.             if not groups: break
  124.         prev_tags= next_tags
  125.  
  126.     return output_chain
  127.  
  128.  
  129. if __name__ == "__main__":
  130.     example_sequence= [
  131.         ("person1", ("male", "company1")),
  132.         ("person2", ("female", "company2")),
  133.         ("person3", ("male", "company1")),
  134.         ("husband1", ("male", "company2", "marriage1")),
  135.         ("wife1", ("female", "company1", "marriage1")),
  136.         ("husband2", ("male", "company3", "marriage2")),
  137.         ("wife2", ("female", "company2", "marriage2")),
  138.     ]
  139.  
  140.     print("Secret Santa assignments")
  141.     chain= secret_santa(example_sequence)
  142.     output_chain= []
  143.     max_len= max(len(i.encode("utf8")) for i in chain)
  144.     for i in reversed(range(len(chain))):
  145.         assignee= chain[i]
  146.         assignment= obfuscate(chain[i-1], max_len)
  147.         output_chain.append((assignee, assignment))
  148.     output_chain.sort()
  149.     for assignee, assignment in output_chain:
  150.         print("%s→%s" % (assignee, assignment.rstrip()))
Advertisement
Add Comment
Please, Sign In to add comment