Secret Santa Ruby Quiz
Tags: ruby, testing
Published: 2020-12-22 17:06:25


Happy Holidays everyone! I wanted to do something in the spirit of the season and thought the [Secret Santa Ruby quiz](http://rubyquiz.com/quiz2.html) would be the perfect puzzle to tackle. This puzzle is right on theme, but it's also really well-defined and small enough in scope that we can go through the process of solving it in a blog post. Let's get started.

_To make sure everyone is on the same page, this won't be strictly for beginners to Ruby or programming. I will gloss over specific steps like initializing a repository with RSpec, running tests, and general Ruby conventions. Instead, I will link to references that provide more information._

## The Puzzle Requirements
The quiz defines the rules as the following:

Feed the script a list of names on `STDIN`. It might look like

```text
Alice Jones <alice@example.com>
Bob Jones <bob@example.com>
Carlos Jones <ricky@example.com>
Demi Smith <demi@example.com>
Eli Smith <eli@example.com>
Florence Williams <florence@example.com>
```

With a format of

```text
FIRST_NAME space FAMILY_NAME space <EMAIL_ADDRESS> newline
```

**To keep things simple**: people only have two names (no Jrs, IIs, etc.).

The script should choose a Secret Santa for every name on the list. A person can't be their own Secret Santa. And you can't be assigned to anyone with the same `FAMILY_NAME`.
Finally, email the Santa and tell them who their person is.

To make the content more focused, I made the scope a little bit smaller by tweaking a few of the requirements. I'm reading from a text file instead of feeding the list through `STDIN`, and I will not be sending emails. I also gave myself about an hour to work on this puzzle.

<!--more-->

## My Thought Process
When starting a new Ruby project, the first thing I do is to initialize my directory with a testing framework. I use [RSpec](https://rspec.info/).

In an "ideal" scenario, whenever I sit down to tackle a problem, I start by thinking about the very first test. It should test exactly one thing, and it should fail. After I get that test to pass, I write another test that will fail, write enough code to make that test pass, and maintain passing status for all previous tests. I repeat this cycle, this is known as [test-driven development.](https://www.agilealliance.org/glossary/tdd/#q=~(infinite~false~filters~(postType~(~'page~'post~'aa_book~'aa_event_session~'aa_experience_report~'aa_glossary~'aa_research_paper~'aa_video)~tags~(~'tdd))~searchTerm~'~sort~false~sortDirection~'asc~page~1))

However, real-life and what's ideal often diverge. I don't always practice "perfect" test-driven development, but I still try to let tests guide me.

These are the steps I wanted to take, removed from implementation:

1. Receive a list of names and emails and from that list create Santas.
2. From the Santas list, for each Santa, generate all possibilities for who their assignees could be.
3. Pick a random assignee from each potential assignment for each Santa.

I thought the steps above would be my basic algorithm going into this problem.

I would discover that I would need to tweak it a little bit for step three to work reliably. Picking a random person from each potential assignment for each Santa means that sometimes the same person gets assigned to multiple Santas. I would later change step three to look like this:

- Pick a random person from the potential assignment for each Santa, keep re-picking until all the assignees are unique.

Not the most sophisticated or performant algorithm, but we're getting things done, and we're testing our expectations. This puzzle is still a great learning opportunity.


## Code Walk Through

For this section, I want to walk you through each step I worked through, starting with my first test.

### Step 1: Receive a list of names and make Santas


```ruby
it "receives a list of names and emails and creates santas" do
  secret_santa = SecretSanta.new('./santas.txt')
  expect(secret_santa.santas).to_not be_empty
end
```

When it comes to writing the first test, I want to be as general as possible. I know that I will have a class called `SecretSanta`, and I want to initialize it with my text file full of my Secret Santas in the format we defined above. However, I don't want to tie myself to a particular implementation regarding how I store the `santas` attribute.

> Tests that aren't dependent on implementation details are much easier to maintain.

After initializing the `SecretSanta` class, I want to make sure that there is _something_ inside the `santas` attribute. That could be any number of data structures. I want to keep my tests general, so they don't have to change.

If I tied myself right away to expecting `santas` to be a particular data structure, but down the line, I decided I want them to be a _different_ data structure, I would have to change my test. But, is it truly a failing test? It's still receiving a list of names and creating `santas`, but because it's tied to a specific implementation, the expectation needs to change.

Next, I write enough code to make the test pass.

```ruby
class SecretSanta
  attr_reader :santas

  def initialize(file)
    @santas = File.open(file).read.split("\n")
  end
end
```

### Step 2: For each Santa, find their potential assignees

I broke my rule here from above when I initially wrote this test. I tied myself to the implementation. And I tied myself to my outside inputs (the textfile I pass in to initialize).

```ruby
it "doesn't allow someone to be assigned to their family member" do
  secret_santa = SecretSanta.new('./santas.txt')
  expect(secret_santa.potential_assignees[0]["Alice Jones"]).to eql [["Demi", "Smith", "<demi@example.com>"], ["Eli", "Smith", "<eli@example.com>"], ["Florence", "Williams", "<florence@example.com>"]]
end
```
I had an idea in my mind of how I wanted these `potential_assignees` to look. And I let it drive the test rather than the other way around. Not to mention, this isn't testing what it purports to _it doesn't allow someone to be assigned to their family member_. The test is setting up an expectation for how a data structure looks, nothing about behavior. We'll see how this bites us below.

The code below makes the test above pass.
```ruby
class SecretSanta
  attr_reader :santas

  def initialize(file)
    @santas = File.open(file).read.split("\n").map { |s| s.split }
  end

  def potential_assignees
    @santas.map do |santa|
      { "#{santa[0]} #{santa[1]} "} => @santas.reject { |s| s[1] == santa[1] }}
    end
  end
end
```

I decided to assign each Santa an array of emails instead of the full name and email for each potential assignee. But the test is tied to the implementation above, so it fails.

The code that makes this test break:
```ruby
def potential_assignees
  @santas.map do |santa|
    { "#{santa[0]} #{santa[1]}" => @santas.reject { |s| s[1] == santa[1] }.map { |s| s[2].flatten }
  end
end
```

Ouch. Testing this behavior is difficult with our current setup. I want a little more structure inside the `santas` attribute.

Maybe something like this?

```ruby
class SecretSanta
  Santa = Struct.new(:first_name, :family_name, :email)

  attr_reader :santas

  def initialize(file)
    santa_info = File.open(file).read.split("\n")
    @santas = santa_info.each_with_object([]) do |santa, santas|
      first_name, family_name, email = santa.split
      santas << Santa.new(first_name, family_name, email)
    end
  end
end
```

The `santas` attribute is a collection of Structs; this gives us the attributes of `first_name`, `family_name`, and `email` on each `Santa`. Plus, our first test was general enough that even refactoring our `santas` attribute to look different keeps the test passing.

For the test to ensure no one gets assigned to a family member, I had to test a fair bit of the implementation. But this test isn't _also_ tied to the text file we pass into the initializer.

```ruby
it "doesn't allow a santa to be assigned a family member" do
  secret_santa = SecretSanta.new('./santas.txt')
  expect(secret_santa.santas.first.potential_assignees.map(&:family_name)).to_not include secret_santa.santas.first.family_name
end
```

I've also decided to include the `potential_assignees` as an attribute on each `Santa` Struct.

This test might know too much about how `@santas` look, but I feel it's valid for our use case. We need to understand how `@santas` looks because the way we're assigning `potential_assignees` depends on it.

```ruby
class SecretSanta
  Santa = Struct.new(:first_name, :family_name, :email, :potential_assignees)

  attr_reader :santas

  def initialize(file)
    santa_info = File.open(file).read.split("\n")
    @santas = santa_info.each_with_object([]) do |santa, santas|
      first_name, family_name, email = santa.split
      santas << Santa.new(first_name, family_name, email)
    end
    set_potential_assignees
  end

  def set_potential_assignees
    @santas.map do |santa|
      santa.potential_assignees = @santas.reject { |s| s.family_name == santa.family_name }
    end
  end
end
```
I've also gone ahead and moved the creation of `potential_assignees` to the initializer; one less thing we have to call in the tests explicitly.
If I had more than an hour, I could develop a more elegant solution, but I am happy with this small puzzle solution. I've moved away from being dependent on two things: the text file AND the implementation. I'm dependent on the implementation only now, but at least the test is
testing what it says it is. The expectation is that the `potential_assignee` attribute won't include the `family_name` for that `Santa`.

### Step 3: Pick randomly for each Santa

For this step, I initially had a naive implementation for the algorithm. I picked a random assignee for each Santa; this led to assignees that were not unique and some people not assigned. The code below relies on the code in step two before we refactored to Structs.

```ruby
it "picks a random person from the potential assignees list" do
  secret_santa = SecretSanta.new('./santas.txt')
  potentials = secret_santa.potential_assignees
  picked = secret_santa.pick_assignees(potentials)
  assigned = picked.map do |santa|
    santa.map do |k, v|
      v
    end
  end

  expect(assigned.flatten.uniq.count).to eql secret_santa.santas.count
end
```

This test created the `potential_assignees`, called the `pick_assignees` function, and then created an array of the picked (`assigned`). The expectation was that all the unique values in `assigned` would be equal to the total number of `santas`. Each Santa _should_ be assigned a unique person. This test was flaky, and I think I saw it pass one time out of the dozens and dozens of times I ran it. Here is the code for step three:

```ruby
def pick_assignees(potentials)
  potentials.map do |potential|
    { "#{potential.keys[0]}" => potential.values.flatten.sample }
  end
end
```

This naive implementation maps through the `potential_assignees` (at this point that data structure looked like:
```ruby
[{"First Name Family Name" => ["email", "email", "email"]}, {"First Name Family Name" => ["email", "email"]}]`)
```

for each potential it just picks a random email from it's `values` using `sample`, returning an array that looks like:
```ruby
[{"First Name Last Name" => "email" }, {"First Name Last Name" => "email"}]
```

### Step 3a. Keep re-picking until everyone has a unique assignee

I had to make the algorithm a little bit smarter. The simplest way I could think to do that was to keep calling `pick_assignees` _until_ every Santa had a unique assignee.

```ruby
def pick_assignees(potentials)
  picked = potentials.map do |potential|
    { "#{potential.keys[0]}" => potential.values.flatten.sample }
  end

  assigned = picked.map do |santa|
    santa.map do |k, v|
      v
    end
  end

  if assigned.flatten.uniq.count != @santas.count
    pick_assignees(potentials)
  else
    return picked
  end
end
```

Now the test passes consistently. We're using recursion (a function that calls itself) to solve this problem in a kind of brute-force way. If the unique assigned values don't equal the total number of `@santas`, try again; if they do return the assignments.

Let's refactor this to use our `Santa` Structs.

```ruby
class SecretSanta
  Santa = Struct.new(:first_name, :family_name, :email, :potential_assignees, :assignee)

  attr_reader :santas

  def initialize(file)
    ...
    set_assignee
  end

  ...

  def set_assignee
    @santas.map do |santa|
      santa.assignee = santa.potential_assignees.sample
    end

    if @santas.map(&:assignee).map(&:email).uniq.count != @santas.count
      set_assignee
    else
      return
    end
  end
end
```

```ruby
it "picks a random person from the potential santas list" do
  secret_santa = SecretSanta.new('./santas.txt')
  expect(secret_santa.santas.map(&:assignee).map(&:email).uniq.count).to eql secret_santa.santas.count
end
```

The test, at this point, reiterates what `set_assignee` does internally. We could figure out a better way to approach this test, but once again, I am happy with where we got for a short time-boxed puzzle.

Let's look at the `SecretSanta` class before our refactoring!

```ruby
class SecretSanta
  attr_reader :santas

  def initialize(file)
    @santas = File.open(file).read.split("\n").map { |s| s.split }
  end

  def potential_santas
    # Gives a data structure that looks like:
    # [{ "First Name Family Name" => ["email", "email", "email"]}]
    @santas.map do |santa|
      { "#{santa[0]} #{santa[1]}" => @santas.reject { |s| s[1] == santa[1] }.map { |s| s[2]}.flatten }
    end
  end

  def pick_santas(potentials)
    picked = potentials.map do |potential|
      { "#{potential.keys[0]}" => potential.values.flatten.sample }
    end

    assigned = get_assigned(picked)

    if unique_assigneds(assigned) != @santas.count
      pick_santas(potentials)
    else
      return picked
    end
  end

  private
  def get_assigned(picked)
    picked.map do |santa|
      santa.map do |k, v|
        v
      end
    end
  end

  def unique_assigneds(assigned)
    assigned.flatten.uniq.count
  end
end
```

Not bad, but room for improvement. Something we didn't touch on here is pulling out methods into private methods. [I am conflicted if this is the right way to approach a private method](https://www.codewithjason.com/purpose-private-methods-use/), but it's what I'm keen to do.

Our refactored version:

```ruby
class SecretSanta
  Santa = Struct.new(:first_name, :family_name, :email, :potential_assignees, :assignee)

  attr_reader :santas

  def initialize(file)
    santa_info = File.open(file).read.split("\n")
    @santas = santa_info.each_with_object([]) do |santa, santas|
      first_name, family_name, email = santa.split
      santas << Santa.new(first_name, family_name, email)
    end
    set_potential_assignees
    set_assignee
  end

  def set_potential_assignees
    @santas.map do |santa|
      santa.potential_assignees = @santas.reject { |s| s.family_name == santa.family_name }
    end
  end

  def set_assignee
    @santas.map do |santa|
      santa.assignee = santa.potential_assignees.sample
    end

    if @santas.map(&:assignee).map(&:email).uniq.count != @santas.count
      set_assignee
    else
      return
    end
  end
end
```

I think this version is much easier to understand. The Struct makes our code more flexible while also providing the benefit of named attributes. No more passing around square brackets with index numbers inside. Human-readable names make the code much more approachable.

## Conclusion & Further Reading
I had a lot of fun doing this Ruby Quiz. I attempted to do it during my apprenticeship and got stuck on my own. It reminds me of how much progress I have made over the past six-plus years. I'd love to see your solutions to this quiz.

- [View the code on GitHub](https://github.com/meaganewaller/secretsanta).
- [Structs](https://ruby-doc.org/core-2.5.0/Struct.html)
- [Test Behavior, Not Implementation](https://testing.googleblog.com/2013/08/testing-on-toilet-test-behavior-not.html)
- [Testing Behavior vs Implementation](https://mdswanson.com/blog/2015/10/05/testing-behavior-vs-implementation.html)