Twelve Days of Christmas

Exercism - Twelve Days of Christmas

The premise for this problem’s fairly simple. Your mission, should you choose to accept it, is to output the lyrics to the 12 Days of Christmas.

The starter code you’re provided is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

defmodule TwelveDays do
@doc """
Given a `number`, return the song's verse for that specific day, including
all gifts for previous days in the same line.
"""
@spec verse(number :: integer) :: String.t()
def verse(number) do
end

@doc """
Given a `starting_verse` and an `ending_verse`, return the verses for each
included day, one per line.
"""
@spec verses(starting_verse :: integer, ending_verse :: integer) :: String.t()
def verses(starting_verse, ending_verse) do
end

@doc """
Sing all 12 verses, in order, one verse per line.
"""
@spec sing() :: String.t()
def sing do
end
end

I knew that there was repetition involved in this problem, and I was originally thinking about how to go about generating the previous days’ gifts, but I psyched myself out. You’re overthinking again! You could just hardcode it!

So that’s what I did, the first time.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@spec verse(number :: integer) :: String.t()
def verse(number) do
case number do
1 ->
"On the first day of Christmas my true love gave to me: a Partridge in a Pear Tree."

2 ->
"On the second day of Christmas my true love gave to me: two Turtle Doves, and a Partridge in a Pear Tree."

3 ->
"On the third day of Christmas my true love gave to me: three French Hens, two Turtle Doves, and a Partridge in a Pear Tree."

4 ->
"On the fourth day of Christmas my true love gave to me: four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree."

5 ->
"On the fifth day of Christmas my true love gave to me: five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree."

6 ->
"On the sixth day of Christmas my true love gave to me: six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree."

7 ->
"On the seventh day of Christmas my true love gave to me: seven Swans-a-Swimming, six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree."

8 ->
"On the eighth day of Christmas my true love gave to me: eight Maids-a-Milking, seven Swans-a-Swimming, six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree."

9 ->
"On the ninth day of Christmas my true love gave to me: nine Ladies Dancing, eight Maids-a-Milking, seven Swans-a-Swimming, six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree."

10 ->
"On the tenth day of Christmas my true love gave to me: ten Lords-a-Leaping, nine Ladies Dancing, eight Maids-a-Milking, seven Swans-a-Swimming, six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree."

11 ->
"On the eleventh day of Christmas my true love gave to me: eleven Pipers Piping, ten Lords-a-Leaping, nine Ladies Dancing, eight Maids-a-Milking, seven Swans-a-Swimming, six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree."

12 ->
"On the twelfth day of Christmas my true love gave to me: twelve Drummers Drumming, eleven Pipers Piping, ten Lords-a-Leaping, nine Ladies Dancing, eight Maids-a-Milking, seven Swans-a-Swimming, six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree."

_ ->
nil
end
end

Yeah, I know, cheating. But it wasn’t like I could avoid recursion if I wanted to write the verses function.

This part was actually difficult for me. Recursion and I have not exactly historically been friends and I couldn’t use my usual changing variables technique that I could use in JavaScript, since mutation isn’t possible in Elixir. SO RECURSION IT IS.

All right, if I’m going to do recursion, I’m going to need to know the base case. At what point am I done with recursion? Thinking about it in the paradigm I’m more used to, this would be the end condition of a while loop.

The first thing I generally do when approaching problems like this is say in English what I want to happen. Sometimes, I even do this aloud (if no one’s around to hear me).

Make a variable current_verse equal to starting_verse. While current_verse is less than or equal to ending_verse, we want to call verse on the current_verse and add that verse to the verses I’ve already got from calling verse, making sure to add a new line between them.

In JavaScript, that’d look like:

1
2
3
4
5
6
7
8
function verses(starting_verse, ending_verse) {
let current_verse = starting_verse;
let complete_verses = verse(current_verse);
while (current_verse <= ending_verse) {
complete_verses += "\n" + verse(++current_verse);
}
return complete_verses;
}

Thankfully for me, you can do string concatenation in Elixir too! <> is the way to accomplish it.

1
2
3
4
5
6
7
8
9
10
def verses(starting_verse, ending_verse) do
if starting_verse === ending_verse do
verse(ending_verse)
else
combined_verses = verse(starting_verse)

current_verse = starting_verse + 1
combined_verses <> "\n" <> verses(current_verse, ending_verse)
end
end

If the starting_verse equals the ending_verse, return verse(ending_verse). If the starting_verse doesn’t equal the ending_verse, take the value of the return of verse(starting_verse) and assign it to the variable combined_verses. Initialize a variable current_verse with the value of one greater than starting_verse. Return combined_verses concatenated with a newline concatenated with the return of the call to verses(current_verse, ending_verse).

Given verses(1, 3), the execution would go something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def verses(1, 3) do
if 1 === 3 do
verse(3)
else
combined_verses = verse(1) ( "On the first day of Christmas my true love gave to me: a Partridge in a Pear Tree.")

current_verse = 1 + 1
combined_verses <> "\n" <> verses(2, 3)
end
end

def verses(2, 3) do
if 2 === 3 do
verse(3)
else
combined_verses = verse(2) ( "On the second day of Christmas my true love gave to me: two Turtle Doves, and a Partridge in a Pear Tree.")

current_verse = 2 + 1
combined_verses <> "\n" <> verses(3, 3)
end
end

verses(2, 3) returns "On the second day of Christmas my true love gave to me: two Turtle Doves, and a Partridge in a Pear Tree.\nOn the third day of Christmas my true love gave to me: three French Hens, two Turtle Doves, and a Partridge in a Pear Tree."

def verses(3, 3) do
if 3 === 3 do
verse(3) ("On the third day of Christmas my true love gave to me: three French Hens, two Turtle Doves, and a Partridge in a Pear Tree.")

-- else doesn't evaluate
end
end

verses(3, 3) returns “On the third day of Christmas my true love gave to me: three French Hens, two Turtle Doves, and a Partridge in a Pear Tree.” So when we evaluate verses(2, 3), we can plug that value in to get the output of verses(2, 3). So the output of verses(2, 3) becomes “On the second day of Christmas my true love gave to me: two Turtle Doves, and a Partridge in a Pear Tree.\nOn the third day of Christmas my true love gave to me: three French Hens, two Turtle Doves, and a Partridge in a Pear Tree.” Then the output of verses(2, 3) can be plugged into the function call to verses(1, 3) to give us “On the first day of Christmas my true love gave to me: a Partridge in a Pear Tree.\nOn the second day of Christmas my true love gave to me: two Turtle Doves, and a Partridge in a Pear Tree.\nOn the third day of Christmas my true love gave to me: three French Hens, two Turtle Doves, and a Partridge in a Pear Tree.”

The final function was easier - sing all 12 verses in order:

1
2
3
def sing do
verses(1, 12)
end

Then after talking it over with some other programmers whom I respect the opinion of, it was pointed out to me that I completely ignored the point 9of the exercise in that I wasn’t really employing the functional paradigm that Elixir is known for to solve the problem. And that one of the best things with Erlang and Elixir is pattern-matching, so I might as well use it.

sigh

Not wanting to spend a million years trying to solve a problem while fighting syntax, I went the easy route and tried to solve the problem using functional programming in JavaScript (I am ever thankful that you can use multiple paradigms):

Pattern-Matching in JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const giftMap = {
1: "a Partridge in a Pear Tree",
2: "two Turtle Doves",
3: "three French Hens",
4: "four calling birds",
5: "five Golden Rings",
6: "six Geese-a-laying",
7: "seven Swans-a-Swimming",
8: "eight Maids-a-Milking",
9: "nine Ladies Dancing",
10: "ten Lords-a-Leaping",
11: "eleven Pipers Piping",
12: "twelve Drummers Drumming"
};

const ordinal = {
1: "first",
2: "second",
3: "third",
4: "fourth",
5: "fifth",
6: "sixth",
7: "seventh",
8: "eighth",
9: "ninth",
10: "tenth",
11: "eleventh",
12: "twelfth"
};

function verse(number) {
return `On the ${
ordinal[number]
} day of Christmas, my true love gave to me: ${composeVerse(number)}`;
}

function composeVerse(number) {
let verse = "";
while (number > 1) {
verse += `${giftMap[number]}, `;
number--;
}
verse += `and ${giftMap[number]}.`;
return verse;
}

Example execution:

1
2
3
4
5
6
verse(5)

function verse(5) {
return `On the ${ordinal[5]} day of Christmas, my true love gave to me:
${composeVerse(5)}`;
}
  • ordinal[5] evaluates to fifth
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function composeVerse(5) {
let verse = ''
while (5 > 1) {
verse += `${giftMap[5]}, `;
// giftMap[5] is "five Golden Rings", so:
// "five Golden Rings, "
5--; // 4
}
while (4 > 1) {
verse += `${giftMap[4]}, `;
// giftMap[4] is "four calling birds", so:
// "five Golden Rings, four calling birds, "
4--; // 3
}
while (3 > 1) {
verse += `${giftMap[3]}, `;
// giftMap[3] is "three French Hens", so:
// "five Golden Rings, four calling birds, three French Hens, "
3--; // 2
}
while (2 > 1) {
verse += `${giftMap[2]}, `;
// giftMap[2] is "two Turtle Doves", so:
// "five Golden Rings, four calling birds, three French Hens, two Turtle Doves, "
2--; // 1
}
while (1 > 1) // evaluates to false, so while loop ends
verse += `and ${giftMap[1]}.`
// giftMap[1] is "a Patridge in a Pear Tree", so:
// "five Golden Rings, four calling birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree."
return verse;
}
  • composeVerse(5) returns “five Golden Rings, four calling birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree.”

The call to verse(5), then, returns:

“On the fifth day of Christmas, my true love gave to me: five Golden Rings, four calling birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree.”

Converting it to Elixir

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@giftMap %{
1 => "a Partridge in a Pear Tree",
2 => "two Turtle Doves",
3 => "three French Hens",
4 => "four Calling Birds",
5 => "five Gold Rings",
6 => "six Geese-a-Laying",
7 => "seven Swans-a-Swimming",
8 => "eight Maids-a-Milking",
9 => "nine Ladies Dancing",
10 => "ten Lords-a-Leaping",
11 => "eleven Pipers Piping",
12 => "twelve Drummers Drumming"
}

@ordinal %{
1 => "first",
2 => "second",
3 => "third",
4 => "fourth",
5 => "fifth",
6 => "sixth",
7 => "seventh",
8 => "eighth",
9 => "ninth",
10 => "tenth",
11 => "eleventh",
12 => "twelfth"
}

These are pretty much the exact same thing as the giftMap and ordinal in the JavaScript implementation. In both languages, I’m using maps to represent the key-value pairs for the gifts and the ordinal day. So far so good.

1
2
def start(number),
do: "On the #{Map.get(@ordinal, number)} day of Christmas my true love gave to me: "

This is almost the same as the JavaScript implementation. Only, this time I’m not calling another function to compose the verse. At least not in this function. This function is literally just to start us off. Map.get(map, key) is a way to get the value of a given key from a map in Elixir, so it’s equivalent to what we saw earlier with ordinal[number]. #{} is like the equivalent of ${} in JavaScript - an indication to evaluate what’s inside before turning it into a string.

I wrote some helper functions to return the gifts out of the giftMap. One of the cool things with Elixir is you can have multi-clause functions – the first function whose function clause matches the argument you provided will execute.

1
2
3
4
5
6
7
8
@spec gift(number :: integer) :: String.t()
def gift(1) do
"and #{Map.get(@giftMap, 1)}"
end

def gift(num) do
"#{Map.get(@giftMap, num)}, "
end

So the first gift is only run if the number 1 is passed in, else the second function definition is used.

1
2
3
4
5
6
7
8
9
10
11
def verse(1) do
"#{start(1)}#{Map.get(@giftMap, 1)}."
end

def verse(number) do
"#{start(number)}#{
Enum.reduce(number..1, "", fn verse, verses ->
verses <> gift(verse)
end)
}."
end

So… if we call verse(1), the first function definition for verse is executed.

The result of verse(1) still being:

“On the first day of Christmas, my true love gave to me: and a Partridge in a Pear Tree.”

Now let’s take a closer look at that second function:

1
2
3
4
5
6
7
def verse(number) do
"#{start(number)}#{
Enum.reduce(number..1, "", fn verse, verses ->
verses <> gift(verse)
end)
}."
end

Enum.reduce(enumerable, acc, fun) is the reduce being used here.
number..1 is a range from number to 1 - if number is 3, for example, this would be [3, 2, 1]. acc stands for accumulator - the initial value being passed into the function. The result of the function is used as the accumulator in the next run of the function. In this case, the original accumulator is an empty string (like the verse variable in the earlier JavaScript implementation). The function, then, takes the verse being iterated on (the current number in the range) and the accumulator and returns the accumulator concatenated with the value of the call to gift(verse).

Say you wanted to get the third verse, the Enum.reduce evaluation would go like this:

1
2
3
4
5
6
7
8
9
10
11
Enum.reduce(3..1, "",
fn 3, "" ->
"" <> gift(3)
// "three French Hens, "
fn 2, "three French Hens, " ->
"three French Hens, " <> gift(2)
// "three French Hens, two Turtle Doves, "
fn 1, "three French Hens, two Turtle Doves, " ->
"three French Hens, two Turtle Doves, " <> gift(1)
// "three French Hens, two Turtle Doves, and a Partridge in a Pear Tree"
end)

#{start(3)} would return:

“On the third day of Christmas my true love gave to me: “

Enum.reduce(number..1, "", fn verse, verses -> verses <> gift(verse) end) returns:

“three French Hens, two Turtle Doves, and a Partridge in a Pear Tree”

And the end of the function is to add a period, giving us:

“On the third day of Christmas my true love gave to me: three French Hens, two Turtle Doves, and a Partridge in a Pear Tree.”

Hurray!

1
2
3
4
@spec verses(starting_verse :: integer, ending_verse :: integer) :: String.t()
def verses(starting_verse, ending_verse) do
Enum.map_join(starting_verse..ending_verse, "\n", &verse(&1))
end

I will admit this looks intimidating if you don’t know Elixir. I literally wrote a note to myself under this function when I was writing it to remind myself what the &verse(&1) was equivalent to so that I wouldn’t freak out when next I cast eyes upon it.

Enum.map_join(enumerable, joiner \ “”, mapper. Joiner is something you want to join the enumerable things on. It’s like if you call String.join(‘,’) in JavaScript. Mapper is going to be a function that you call on the elements in the enumerable (since you’re mapping over them as well as joining – very intuitive naming).

In our case, & is a capture operator to partially apply a function and &1 is a value placeholder. So if we wanted to write code that was less confusing for other people, we could write &verse(&1) as fn x -> verse(x) end.

Anyway, it’s getting late and I spent a good hour on this longer than I meant to because I had a cat fall asleep on my arm. (Want to know how difficult it is to write code blocks with one arm? so hard)

See ya!

- Amy