[QUIZ] Secret Santas (#2)

The three rules of Ruby Quiz:

1. Please do not post any solutions or spoiler discussion for this quiz until
48 hours have passed from the time on this message.

2. Support Ruby Quiz by submitting ideas as often as you can:

http://www.grayproductions.net/ruby_quiz/

3. Enjoy!

···

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

Honoring a long standing tradition started by my wife's dad, my friends all play
a Secret Santa game around Christmas time. We draw names and spend a week
sneaking that person gifts and clues to our identity. On the last night of the
game, we get together, have dinner, share stories, and, most importantly, try to
guess who our Secret Santa was. It's a crazily fun way to enjoy each other's
company during the holidays.

To choose Santas, we use to draw names out of a hat. This system was tedious,
prone to many "Wait, I got myself..." problems. This year, we made a change to
the rules that further complicated picking and we knew the hat draw would not
stand up to the challenge. Naturally, to solve this problem, I scripted the
process. Since that turned out to be more interesting than I had expected, I
decided to share.

This weeks Ruby Quiz is to implement a Secret Santa selection script.

Your script will be fed a list of names on STDIN. An example might be:

  Luke Skywalker <luke@theforce.net>
  Leia Skywalker <leia@therebellion.org>
  Toula Portokalos <toula@manhunter.org>
  Gus Portokalos <gus@weareallfruit.net>
  Bruce Wayne <bruce@imbatman.com>
  Virgil Brigman <virgil@rigworkersunion.org>
  Lindsey Brigman <lindsey@iseealiens.net>

Note: If you cannot tell, I made those addresses up and you'll need to replace
them with something meaningful. Please don't pester those people, should they
happen to be real.

The format for these names is:

  FIRST_NAME space FAMILY_NAME space <EMAIL_ADDRESS> newline

We'll keep things simple and say that people only have two names, so you don't
have to worry about tricky names like Gray II.

Your script should then choose a Secret Santa for every name in the list.
Obviously, a person cannot be their own Secret Santa. In addition, my friends
no longer allow people in the same family to be Santas for each other and your
script should take this into account.

Output is obvious. E-mail the Santa and tell them who their person is.

The extra credit for this quiz is to convince all your friends how much fun this
can really be, so you can put your script to good use. Go forth spreading
holiday cheer into the world!

Hi James,

What should the script do if there is no solution for the input set, e.g. the set is two people in the same family?

Moses

I've never worked with STDIN before; does this mean a single call to #gets will return a newline-delimited list of names, or I should loop calls to #gets until it returns nil?

···

On Oct 1, 2004, at 6:50 AM, Ruby Quiz wrote:

Your script will be fed a list of names on STDIN.

--
(-, /\ \/ / /\/

Output is obvious. E-mail the Santa and tell them who their person is.

Will you accept solutions that just print out the e-mails instead of sending them? :slight_smile:

I'm still new to Ruby, but giving this a crack anyway.

I could have made more classes to model the problem better, but didn't bother. Everything's done by a Hat with higher intelligence than regular hats (that doesn't deserve a capital H).

To run, type "ruby santa.rb", then enter each person's name and email in the format specified. When you're done the script tells you the result of the selection.

The email part is a bit rogue, but that's just a matter of creating a more comprehensive email message. It's disabled by default, just uncomment the lines in Hat#notify.

It doesn't take into account of creating loops smaller than total number of people. i.e. If there're 4 people, 3 of them maybe Secret Santas of each other, leaving one stranded.

Robo

santa.rb (1.58 KB)

# This implementation is heavily optimized to be as fast to
# implement as possible. Please read the above sentence very
# carefull :wink: I have tried to find an implementation
# that is as simple to understand as possible, but does work.
require 'net/smtp'

Person = Struct.new("Person", :firstName, :familyName, :email)

# extract people from input stream
def readSantas(io)
   santas = Array.new
   io.read.scan(/(\S*)\s(\S*)\s\<(\S*)\>/) do |first, family, email|
      santas.push Person.new(first, family, email)
   end
   santas
end

# get an ordering where each santa gets a person who is
# outside his family. This implementation is extremely simple,
# as it assumes it is always possible to find a correct solution.
# Actally it is so simple that it hurts. I would never use this
# in production-level code.
def orderPeople(santas)
   isCorrectlyOrdered = false
   while !isCorrectlyOrdered
      # get a random order
      people = santas.sort_by { rand }
      
      # check if the random order does meet the requirements
      i = 0
      i += 1 while i<santas.size &&
santas[i].familyName==people[i].familyName
      isCorrectlyOrdered = i<santas.size
   end
   people
end

# send santa a mail that he/she is responsible for person's presents
def sendMail(santa, person)
   msg = ["Subject: Santa news", "\n",
      "You are santa for #{person.firstName} #{person.familyName}" ]
      
   Net::SMTP.start("localhost") do |smtp|
      smtp.sendmail(msg, "santa delegator", santa.email)
   end
end

if __FILE__ == $0
   santas = readSantas(STDIN)
   people = orderPeople(santas)
   
   santas.each_index do |i|
      santa = santas[i]
      person = people[i]
      puts "'#{santa.firstName} #{santa.familyName}' is santa for
'#{person.firstName} #{person.familyName}'"
      # if you really want to send mails, uncomment the following
line.
      #sendMail(santa, person)
   end
end

This is a bit later than most, but I have refrained from looking at this
thread until I was finished.

When thinking about this quiz and the constraints I came up with the
following realizations:
    
    1) It is impossible to have a secret santa with only 2 people. That
       one is pretty evident.

    2) It is impossible to have a secret santa if more than half of the
       participants are in the same family.

    3) The email address must be considered the unique identifier of a
       santa. This one is pretty evident too.

The approach I took was to read in all the possible santas and check for
these constraints. Once the constraints were met I took the input as
the pool of possible santas and created essentially a circularly linked
list to represent the chain of senders and receivers of holiday cheer.

If you pick a person (A) in the chain then that person is the secret
santa of the next person in the chain. The person before (A) in the
chain is the secret santa of (A). Doing this also makes the assumption
that a single circular list of people can be created from the input.

The circular list is populated by randomly removing a person from the
pool and starting at the pseudo-beginning of the list. The person is
then walked around the list until a valid point in the list is found for
insertion.

A point is valid if both the person before the insertion point and after
the insertion point are not in the same family as the person to insert.

enjoy,

-jeremy

#!/bin/env ruby

···

########################################################################
#
# Ruby Quiz #2 - Secret Santa Selector
#
# On standard input is a set of names and addresses in the format:
#
# FIRST_NAME FAMILY_NAME <EMAIL_ADDRESS>
#
# The goal here is to pick secret santas or in a bit simpler terms,
# pick sender's and receivers of email but with the following caveats:
#
# 1) No 2 sender and receivers can have the same family name
#
# 2) No 2 sender and receivers can be each other's sender or
# receiver
#
# 3) A receiver cannot be their own sender
#
# With these caveats in mind, it is immediately apparent that there
# are situations when no solution can be found.
#
# 1) if there are only 2 santas total
# 2) if the number of santas from the same family is greater than
# 1/2 of all the total santas.
#
# Some assumptions that were made
#
# - the email address is the unique Santa Identifier. This seems
# to be a reasonable assumption since that is how the santas are
# notified.
#
# - it is possible for a single chain of senders and receivers to be
# created
#
########################################################################

class Santa
   
    attr_reader :first_name, :family, :email
    
    def initialize(first_name, family, email)
        @first_name = first_name
        @family = family
        @email = email.downcase
    end

    # since the input may not be totally clean, comparing one Santa's
    # family to another needs a bit of holiday magic
    def same_family_as(other)
        @family.downcase == other.family.downcase
    end

    # Santas will be compared sometimes. We only want the compare to happen
    # on the email address, since that is the only unique portion.
    def <=>(other)
        @email <=> other.email
    end

    def eql?(other)
        @email.eql?(other.email)
    end

    alias == eql?

    def to_s
        "#{first_name} #{family} #{email}"
    end
end

# The SantaCircle depicts who is the Secret Santa of who.
#
# From a computer science perspective, the SantaCircle can be thought of
# as a circularly linked list, where the person at one point in the list is
# the secret santa of the next person in the list.

class SantaCircle

    def initialize(first_santa=nil)
        @circle = Array.new
        circle.push(first_santa) unless first_santa.nil?
    end

    # this is where the magic happens. A Santa is added to the circle
    # by traversing the circle and finding the first location where
    # it fits. This means that:
    #
    # - The santa being added is not already in the circle
    #
    # - The location the santa fits into the circle is valid such that
    # the santa before it in the circle can give to it and it can give
    # to the santa after it in the circle. This condition is satisfied by
    # checking Santa.same_family_as
    #
    # the method throws an Exception if a santa that is already in the circle is
    # added again.
    def add(santa)
        raise "Santa #{santa.email} already in the Circle" if @circle.include?(santa)

        # add the first one
        if @circle.empty? then
            @circle.insert(0,santa)
            return
        end

        # for each santa currently in the circle, see if the new santa can fit
        # before it. The fact that array[-1] is the last element in the array
        # works out really nicely here, since the last santa in the array is the
        # secret santa of the first santa in the array.
        added = false
        @circle.each_index do |i|
            next if santa.same_family_as(@circle[i])
            next if santa.same_family_as(@circle[i-1])
            @circle.insert(i,santa)
            added = true
            break
        end
        raise "Santa #{santa.email} unable to be added to the Circle" if not added
    end

    def to_s
        s = ""
        @circle.each_index do |i|
            s = s + "#{@circle[i-1].email} secret santa of #{@circle[i].email}\n"
        end
        return s
    end

    def each_pair
        @circle.each_index do |i|
            yield @circle[i-1], @circle[i]
        end
    end
end

santa_pool = []
family_counts = {}
uniq_emails = {}

# pull in all the possible santas
ARGF.each do |line|
    first,last,email = line.split
    new_santa = Santa.new(first,last,email)

    # keep track of how many santas are in each family
    if family_counts.has_key?(new_santa.family) then
        family_counts[new_santa.family] += 1
    else
        family_counts[new_santa.family] = 1
    end

    # verify that all santas are unique
    if uniq_emails.has_key?(new_santa.email) then
        puts "Invalid data set: email address #{new_santa.email} exists more than once"
        exit 1
    else
        uniq_emails[new_santa.email] = 1
    end

    # santa has been a good person, so go jump in the pool.
    santa_pool.push(new_santa)
end

# if there are only 2 people, then it makes no sense to have a secret santa
if santa_pool.size == 2 then
    puts "It is not possible to have a Secret Santa, there are not enough people"
    exit 1
end

# some sanity checking and make sure that one family doesn't have more than 1/2
# of all the possible santas.
max_family_count = 0
max_family = nil
family_counts.each_pair do |family,count|
    if count > max_family_count then
        max_family_count = count
        max_family = family
    end
end

if max_family_count > (santa_pool.size / 2.0) then
    puts "It is not possible to have a Secret Santa, the #{max_family}'s have too many people"
    exit 1
end

# hmmm... secrets everywhere
secrets = SantaCircle.new

# create the secret santas. Randomly pick from the santa pool and add to the
# Santa Circle until the santa pool is empty.
until santa_pool.empty?
    santa = santa_pool.delete_at(rand(santa_pool.size))
    secrets.add(santa)
end

# If this were were a real secret santa then uncomment some of the following
# lines and see if it works. The email portion of this script is untested. I
# just followed one of the examples on p. 703 of PickAxe II.

selector = "Santa Selector <santa-selector@workshop.northpole>"

#require 'net/smtp'
#Net::SMTP.start('localhost', 25) do |smtp|
    secrets.each_pair do |sender, receiver|

        msg = <<SECRET_MESSAGE
From: #{selector}
To: #{sender.to_s}
Subject: Your Secret Mission

You are to be the Secret Santa of:

   #{receiver.to_s}

SECRET_MESSAGE
        
        puts "=" * 70
        puts msg

        # smtp.send_message(msg,"#{selector}","#{sender.to_s}")
        
    end
# end

--

Jeremy Hinegardner jeremy@hinegardner.org

Yay, I think I just made my first, hopefully functional, piece of ruby code :slight_smile:
Took me about 2.5 hours, including finding out about how to use
Comparable and define <=> :slight_smile: ... including a long time to realize
delete_if is destructive :frowning:

The solution does the following:
- groups persons by family,
- orders the families descending by number of members
- gets a santa as a member from the family with the most number of members
- for this santa, finds a santee as a member from the family with the
mose number of members, that is not the santa's family
rinse, repeat until there are no more santas and all is ok or, until
we can't find a santee for a santa and decide we can't hook these
people up

this seems ok, acts ok, but i can't really prove that it actually
works across all possible cases :slight_smile:

Here's the code, sorry, it doesn't read from stdin, but it generates
it's own random input.
beware, this thing is probably badly coded :slight_smile:

···

#####################

#!ruby
# evilchelu@irc
# mental@gmail

def generate_input(persons=10, families=3)
  fams = Array.new(families) {|i| 1}
  srand Time.now.to_i
  (persons-families).times do
    fams[rand(families)] += 1
  end
  s = ''
  fams.each_with_index do |fam, famnr|
    fam.times do |i|
      s += (97+i).chr + ' ' + (90-famnr).chr + ' ' + '<' + (97+i).chr +
'@' + (90-famnr).chr + ".fam>\n"
    end
  end
  return s.sort_by {rand}
end

class Object
  def deep_clone
    Marshal::load(Marshal.dump(self))
  end
end

class Families < Array
  def sum_persons
    s = 0
    self.each { |el| s+= el.persons.size}
    return s
  end
  def get_person(exceptfamily='')
    self.find_all{ |fam| fam.name != exceptfamily
}.sort.reverse.first.persons.shift
  end
end

class Family
  include Comparable
  attr_accessor :name, :persons
  def initialize(name)
    @name = name
    @persons = Array.new
  end
  def to_s
    s = "Family: #{@name}\n"
    persons.each {|p| s+= " " + p.to_s + "\n"}
    return s
  end
  def <=>(other)
    persons.size <=> other.persons.size
  end
end

class Person
  include Comparable
  attr_accessor :name, :family, :email
  def initialize(args)
    @name = args[0]
    @family = args[1]
    @email = args[2]
  end
  def to_s
    return [@name, @family, @email].join(" ").strip
  end
  def <=>(other)
    tmp = family <=> other.family
    tmp = name <=> other.name if tmp == 0
    return tmp
  end
end

def read_data(input)
  puts "Input\n#{input}\n"
  fams = Families.new
  persons = Families.new
  input.each do |line|
    persons.push Person.new(line.split(/\s/, 3))
    fam = fams.find { |fam| fam.name == persons.last.family}
    if fam.nil?
      fam = Family.new(persons.last.family)
      fams.push fam
    end
    fam.persons.push persons.last unless fam.persons.find{ |pers|
pers.name == persons.last.name}
  end
  return fams
end

def make_santas(fams)
  fams = fams.sort.reverse
  
  santas = fams.deep_clone
  santees = fams.deep_clone
  s = "List of santas (SANTA - SANTEE)\n"
  while santas.sum_persons > 0
    begin
      santa = santas.get_person
      santee = santees.get_person(santa.family)
    rescue
    end
    if (!santa || !santee)
      puts "Bad input. Red Sleigh Down"
      return
    end
    s += santa.to_s + ' - ' + santee.to_s + "\n"
  end
  puts s
end

make_santas(read_data(generate_input()))

#####################

--
Cristi BALAN

My solution.

···

--------------------------------------------------

SENDMAIL = "/usr/sbin/sendmail -oi -t"

class Array
     def random_member(&block)
         return select(&block).random_member if block
         return self[rand(size)]
     end
     def count(&block)
         return select(&block).size
     end
end

class String
     def clean_here_string
         indent = self[/^\s*/]
         return self.gsub(/^#{indent}/, "")
     end
end

class Person
     attr_reader :first, :family, :mail
     def initialize(first, family, mail)
         @first, @family, @mail = first, family, mail
     end
     def to_s() "#{first} #{family} <#{mail}>" end
end

class AssignSanta
     def initialize(persons)
         @persons = persons.dup
         @santas = persons.dup
         @families = persons.collect {|p| p.family}.uniq
         @families.each {|f| raise "No santa configuration possible" if santa_surplus(f) < 0}
     end

     # Key function -- extra santas available for a family
     # if this is negative -- no santa configuration is possible
     # if this is 0 -- next santa must be assigned to this family
     def santa_surplus(family)
         return @santas.count {|s| s.family != family} - @persons.count {|p| p.family == family}
     end

     def call
         while @persons.size() > 0
             family = @families.detect {|f| santa_surplus(f)==0 and @persons.count{|p| p.family == f} > 0}
             person = @persons.random_member {|p| family == nil || p.family == family}
             santa = @santas.random_member{ |s| s.family != person.family }
             yield(person, santa)
             @persons.delete(person)
             @santas.delete(santa)
         end
     end
end

def mail(from, to, subject, message)
     File.popen(SENDMAIL, 'w') { |pipe|
         pipe.puts( (<<-HERE).clean_here_string)
             From: #{from}
             To: #{to}
             Subject: #{subject}

             #{message}
             HERE
     }
end

def main
     persons = []
     STDIN.each_line { |line|
         line.scan(/(\w+)\s+(\w+)\s+<(.*)>/) {|first, family, mail|
             persons << Person.new(first, family, mail)
         }
     }

     AssignSanta.new(persons).call {|person, santa| mail(person, santa, "Dear santa", "You are my secret santa!")}
end

main

An interesting problem. It seems trivial at first, but when you start to think about whether a solution is possible or not and in which way you can assign santas as random as possible, but without painting yourself into a corner it gets quite complex. Here is a mathematical analysis:

At any stage in the solution we have a number of persons from different families that need to be assigned a santa, and a number of available santas. We define the "santa surplus number" SS(F) for a family F as:

   SS(F) := (number of available santas for this family) -
            (number of family members that haven't been assigned a santa)

The available santas for a family are the available santas that doesn't belong to that family.

It is clear that if SS(F) < 0 for some family then no santa assignment is possible, because there are not enough santas for that family.

Suppose that SS(F) >= 0 for all families, then a santa assignment is always possible. To see this, we look at what happens to the SS number when a santa is assigned.

Suppose that a person from family F is assigned a santa from family S. Let O be any other family. The assignment will cause the following changes to the SS numbers.

   SS(F) := SS(F)

     The family F looses an available santa, but the number of santas
     needed is also reduced by one, so SS stays the same.

   SS(S) := SS(S)

     The S family does not loose an available santa because the santa
     comes from the S family and could never be a santa for the S
     family.

   SS(O) := SS(O) - 1

     All other families loose an available santa.

If SS(O) = 0, for some family O, then the assignment will lead to an impossible configuration, because SS(O) will become negative. This gives us the rule for assigning santas without creating an impossible configuration:

   If SS(F) = 0 for some family F, then in the next santa assignment,
   either the santa or the person that is assigned a santa must come
   from the family F.

We can always make our santa assignments so that they follow this rule
*unless* there are three or more families with SS(F) = 0. But if SS(F) >= 0 for all families, there can never be three families with SS(F) = 0.

To see this, let N be the total number of persons without santas (and also the number of available santas). Let S(F) be the number of available santas from family F and P(F) be the number of persons without santas from F, then

  SS(F) = (N - S(F)) - P(F) = N - S(F) - P(F)

So if SS(F) = 0 then:

         N = S(F) + P(F)

Suppose that there are two families A and B with SS(A) == 0 and SS(B) == 0 then:

         N = S(A) + P(A)
         N = S(B) + P(B)

         2N = S(A) + S(B) + P(A) + P(B)

         (N - S(A) - S(B)) + (N - P(A) - P(B)) = 0

         N - P(A) - P(B) = 0 (since N - S(A) - S(B) >=0)
         N - S(A) - S(B) = 0 (and N - P(A) - P(B) >= 0)

         N = P(A) + P(B)
         N = S(A) + S(B)

In other words, then all santas and all persons come from those two families. So if two families have SS(F) = 0, there can be no other families with unassigned santas.

Since there will always be at most two families with SS(F) = 0, we can always select a santa according to the rule above. And we MUST select a santa according to that rule in order to not create an impossible situation. This means that if we randomize the choice of santa while following the rule, we get a selection that is as random as possible while obeying the constraints.

// Niklas

Hi,

these is my solution.
In order to solve the problem I've proceed like that:
  -Start to compute all permutations possibles of emails addresses
  -Remove all of them where there is a couple of persons in the same family.
  -Return, randomly, one of the permutations.

Example:
$ ./santa.rb < friends
<luke@theforce.net> -> <gus@weareallfruit.net>
<leia@therebellion.org> -> <virgil@rigworkersunion.org>
<toula@manhunter.org> -> <bruce@imbatman.com>
<gus@weareallfruit.net> -> <lindsey@iseealiens.net>
<bruce@imbatman.com> -> <leia@therebellion.org>
<virgil@rigworkersunion.org> -> <toula@manhunter.org>
<lindsey@iseealiens.net> -> <luke@theforce.net>

This is the script:

#!/usr/bin/ruby

class Friends
         attr_reader :email, :family, :nb
         def initialize
                 @email = Hash.new
                 @members=0
                 @nb = []
         end
         def add (first_name,family_name,mail)
                 @email[mail] = family_name
                 @nb[@members] = mail
                 @members += 1
         end
end
# compute all permutation in a list
def permute(items, perms=[], res=[])
     unless items.length > 0
         res << perms
     else
         for i in items
             newitems = items.dup
             newperms = perms.dup
             newperms.unshift(newitems.delete(i))
             permute(newitems, newperms,res)
         end
     end
     return res
end

friends = Friends.new
while line = gets
         friends.add(*line.split(' '))
end
perms = permute(friends.email.keys)
perms.reject!{|tab|
         res = false
         for i in 0..tab.length-1
                 if friends.email[tab[i]] == friends.email[friends.nb[i]]
                         # same family
                         res = true
                 end
         end
         res
}
res = perms[rand(perms.length)]
for i in 0..res.length-1
         puts "#{friends.nb[i]} -> #{res[i]}"
end

Egad, knew I forgot to mention something! My bad, sorry. Told ya'll I still need some breaking in at quiz writing. <laughs>

I will only feed your script valid combinations, so if you don't want to worry about it, don't. If you do want to build in a sanity check, a simple error message should be fine. Handle it however you are comfortable.

James Edward Gray II

···

On Oct 1, 2004, at 4:37 PM, Moses Hohman wrote:

Hi James,

What should the script do if there is no solution for the input set, e.g. the set is two people in the same family?

Gavin Kistner wrote:

Your script will be fed a list of names on STDIN.

I've never worked with STDIN before; does this mean a single call to #gets will return a newline-delimited list of names, or I should loop calls to #gets until it returns nil?

STDIN and ARGF are both IOs. STDIN reads from the standard input. ARGF reads from the standard input and file names that are in ARGV.

IO#gets returns one line with a \n at the end or nil when there are no more lines available.

There's also IO#read which returns the whole file as a String and IO#readlines which returns the whole file as an Array of lines. (Each with a \n at the end.)

Regards,
Florian Gross

Why? Are you not yet familiar with "net/smtp"? It's a standard lib, so this is a good excuse to learn it and it's surprisingly simple... :smiley:

There's no right and wrong answers to a Ruby Quiz. The goal is to think, learn and have a little fun. Submit whatever does that for you.

James Edward Gray II

···

On Oct 2, 2004, at 5:09 PM, Joe Cheng wrote:

Output is obvious. E-mail the Santa and tell them who their person is.

Will you accept solutions that just print out the e-mails instead of sending them? :slight_smile:

And here is my version of the second quiz. It does not use 'net/smtp' but shows the chosen santas on the console.

Thomas

···

-----------------------------------------------
#!/usr/bin/env ruby

Person = Struct.new( :first, :last, :email )

# Parses one line and extract the data items
def parse_name( name )
  person = Person.new( *name.split[0..2] )
  if person.first.nil? || person.last.nil? || person.email.nil?
    puts "Invalid input: #{name.inspect}"
    exit
  end
  return person
end

# Reads lines from the given IO object and returns a Hash with all persons as keys
def parse_names( io )
  list = {}
  list[parse_name( STDIN.readline )] = nil until io.eof
  return list
end

# Associates each person with a list of possibile Santas
def build_santa_lists( list )
  list.each_key do |person|
    possible_santas = list.keys
    possible_santas.reject! { |other_person| other_person.last == person.last }
    list[person] = possible_santas
  end
end

# A Santa is correct if there is no other person for whom only the selected Santa is left
def verify_santa( list, person, santa )
  list.each do |key, value|
    return false if key != person && value == [santa]
  end
  return true
end

# Choose a Santa for each person
def choose_santas( list )
  list.each do |person, possible_santas|
    begin santa = possible_santas[rand( possible_santas.length )] end until verify_santa( list, person, santa )
    list.each_value { |value| value.delete( santa ) if Array === value }
    list[person] = santa
  end
end

# Mail the Santas which person they have got
def mail_santas( list )
  list.each do |person, santa|
    santa = Person.new('<no valid santa found>', '', '') if santa.nil?
    puts "Santa #{santa.first} #{santa.last} #{santa.email} for #{person.first} #{person.last} #{person.email}"
  end
end

list = parse_names( STDIN )
build_santa_lists list
choose_santas list
mail_santas list

My solution to the Quiz #2 is at http://phrogz.net/RubyLibs/quiz/2/

The general approach is:
a) Create a randomized array of Person objects (having parsed the input).
b) Turn the array into a circular list, and separate any adjacent family members.
c) Send email (or, if $DEBUG is set, spit out a message), treating adjacent pairs in the list as giver/receivers.

I initially wrote it treating the array as a circular list, but then decided I wanted a real circular list class. It's not well tested yet, but if you like, the Ouroboros class I created is free for the horking. (It's in the above url, as well as documented and available in main http:/phrogz.net/RubyLibs/)

I thought I had a better, less-hackish solution than the pseduo-bubble-sort method I used to separate adjacent duplicates. I sorted people into family bins, randomized the families, and then pulled people out from one bin at a time. The benefit was supposed to be better family separation (to limit occurrances of John Doe from giving to Bob Smith who gave right back to Jane Doe). Unfortunately, I didn't think of the case of a single many-member family overpowering the pool, where early even distribution ends up with only members from that family left at the end.

I was thinking about weighting my choices by the number of people in each bin, but since the above solution works, I decided to scrap it.

The interesting thing about this quiz is...my extended family does the same thing, and up until now no one had thought to automate the no-same-family rule. The list of members was randomized in excel (much like Ruby: sort names by an adjacent random number column) and then the list manually massaged to ensure no family members were adjacent.

I'd never worked with STDIN or SMTP before, so this was a great exercise for me. Looking forward to looking through other's solutions. Thanks again for a fun quiz!

···

--
(-, /\ \/ / /\/

Very nice, Robo. Whenever I've thought of "random except for ____" type solutions, I've never gotten beyond thinking of "keep picking random items until you meet the criteria" (which is a dumb way to go about it).

Filtering the pool is quite nice.

Hrm, which gives me a good idea for my Ouroboros#separate_duplicates method; currently if I find a family duplicate I just swap the second one with it's following neighbor, and either hope that it's not the same family or wait until the next pass in the loop to fix that one. I suspect I would get much quicker sorting if I 'filtered' the pool...search ahead for the next person who isn't in the same family and swap them.

That smells like it should be an O(n) performer, rather than ~O(n^2).

···

On Oct 3, 2004, at 6:24 AM, Robo wrote:

  #filter out people who're in the same family as Secret Santa
  def filter(santa)
    @pool.select do |member|
      member.lastName != santa.lastName
    end
  end

--
(-, /\ \/ / /\/

My solution first divides people into their families. I build up a list of santas (where each person gives to the next in the list, and the tail gives to the head) by:

1) For the first santa, choose from the family with the most people.
2) For all other santas, choose from the family with the most remaining people that is not the family of the last santa.

I wasn't able to think of any situations that would thwart this strategy.

I, too, just print out the names at the console instead of e-mailing. And there is also nothing random about my strategy, so given the same input you will always get the same output. It would be a simple matter to shuffle the members within the families though; that would mix things up a little.

Note to other submitters, the following is a good edge case to test:

Luke1 Skywalker <luke@theforce.net>
Luke2 Skywalker <luke@theforce.net>
Luke3 Skywalker <luke@theforce.net>
Luke4 Skywalker <luke@theforce.net>
Leia Skywalker <leia@therebellion.org>
Toula Portokalos <toula@manhunter.org>
Gus Portokalos <gus@weareallfruit.net>
Bruce Wayne <bruce@imbatman.com>
Virgil Brigman <virgil@rigworkersunion.org>

···

---

# Represents a family.
class Family
  attr_reader :name, :members

  def initialize(name)
    @name = name
    @members = []
  end
  
  # Number of people in family.
  def count
    @members.length
  end
  
  # Pop the last member off.
  def pop
    @members.pop
  end

  # Compare by size.
  def <=>(other)
    count <=> other.count
  end
end

class Person
  attr_reader :first_name, :last_name, :email
  
  def initialize(first_name, last_name, email)
    @first_name = first_name
    @last_name = last_name
    @email = email
  end
  
  def to_s
    "#{@first_name} #{@last_name} <#{@email}>"
  end
end

familyTable = Hash.new {|h,k| h[k] = Family.new(k)}

while line = gets
  line =~ /(\w+) (\w+) <(.+)>/
  first, last, email = $1, $2, $3
  
  familyTable[last].members << Person.new(first, last, email)
end

puts
puts "Processing..."

families = familyTable.values
santas = []

while families.length > 0

  families.sort!.reverse!

  if families.first.count == 0
    # nobody is left; we're done
    break
  end
  
  if santas.length == 0
    # for the very first santa, choose someone from
    # the largest family
    santas << families.first.pop
  else
    success = false
    
    # find largest family that is not one's own
    families.each do |family|
      if family.name != santas.last.last_name
        santas << family.pop
        success = santas.last
        break
      end
    end
    
    raise "No solution." unless success
  end
end

puts "Success!"
puts

lastSanta = santas.last
santas.each do |santa|
  puts santa.to_s + " => " + lastSanta.to_s
  lastSanta = santa
end

I had to rewrite my solution, because the true program takes more things into account than I mentioned in the quiz, but this is pretty much a direct simplification.

I used a random sort, which of course, isn't at all elegant.

James Edward Gray II

#!/usr/bin/env ruby

require "net/smtp"

unless ARGV.size == 1
  puts "Usage: #{$0} SMTP_SERVER\n"
  exit
end

$players = STDIN.readlines.map { |line| line.chomp }
$santas = $players.sort_by { rand }

def check_families?
  $players.each_with_index do |who, i|
    return false if who[/\S+ </] == $santas[i][/\S+ </]
  end
  return true
end

$santas = $players.sort_by { rand } until check_families?

Net::SMTP.start(ARGV[0]) do |server|
  $santas.each_with_index do |santa, i|
    message = <<END_OF_MESSAGE

···

From: Secret Santa Script <james@grayproductions.net>
To: #{santa}
Subject: Secret Santa Pick

#{santa[/\S+/]}:

You have been chosen as the Secret Santa for #{$players[i]}. Merry Christmas.

Secret Santa Selection Script (by James)
END_OF_MESSAGE
    begin
      server.send_message(
          message,
          "james@grayproductions.net",
          santa[(santa.index("<") + 1)...santa.index(">")] )
    rescue
      puts "A message could not be sent to #{santa}.\n#{$!}"
    end
  end
end

__END__

Hi!

I didn't have the time to implement a complete solution, but at least I'd like to show my algorithm for assigning the santas.
I start by assigning a random santa to everyone without caring about the constraints. Then I go through the list of people and for each one not having a correct santa I swap santas with someone else, so that both have correct santas afterwards.
As far as I can see, this will only fail when no solution is possible and should be reasonably unbiased.

Here is the code:

class Person
   attr_reader :first, :last, :email
   attr_accessor :santa

   def initialize(line)
     m = /(\S+)\s+(\S+)\s+<(.*)>/.match(line)
     raise unless m
     @first = m[1].capitalize
     @last = m[2].capitalize
     @email = m[3]
   end

   def can_be_santa_of?(other)
     @last != other.last
   end
end

input = $stdin.read

people = []
input.each_line do |line|
   line.strip!
   people << Person.new(line) unless line.empty?
end

santas = people.dup
people.each do |person|
   person.santa = santas.delete_at(rand(santas.size))
end

people.each do |person|
   unless person.santa.can_be_santa_of? person
     candidates = people.select {|p| p.santa.can_be_santa_of?(person) && person.santa.can_be_santa_of?(p)}
     raise if candidates.empty?
     other = candidates[rand(candidates.size)]
     temp = person.santa
     person.santa = other.santa
     other.santa = temp
     finished = false
   end
end

people.each do |person|
   printf "%s %s -> %s %s\n", person.santa.first, person.santa.last, person.first, person.last
end

···

--
exoticorn/farbrausch