The Problem

Last night at pdx.rb, Markus Roberts gave us a code golf exercise on dealing hands of a given size from a deck of cards.

1
2
3
4
5
6
7
8
9
10
11
def deck
# ["♣A", "♣2", "♣3", ..., "♦Q", "♦K"]
%w(♣ ♥ ♠ ♦).product(%w(A 2 3 4 5 6 7 8 9 10 J Q K)).map(&:join)
end
hand1, hand2, hand3, hand4, rest = deck.shuffle.deal(5,5,7,7)
puts hand1.join(' ') # 5 cards
puts hand2.join(' ') # 5 cards
puts hand3.join(' ') # 7 cards
puts hand4.join(' ') # 7 cards
puts rest.join(' ') # the remaining 28 cards

The Solution

My team checked with Markus to make sure it was okay if we clobbered the original array, then came up with this approach:

1
2
3
4
5
class Array
def deal(*hand_sizes)
[*hand_sizes.map{|hand_size| pop(hand_size)}, self]
end
end

To fully explain: deal can take an arbitrary number of arguments, which will be available in the method definition as an array called hand_sizes. We turn each member of hand_sizes into an actual hand of that size using Array’s pop, which removes n elements from the end of the array and returns those removed elements. pop is destructive, so the original array gets shorter every time we execute map’s block. Once we’ve popped off hands of the sizes we need, the array itself (self) is now everything that wasn’t dealt into a hand. The splat makes it so that we can concisely build the five-element array we need. (A simpler example if what the splat is doing isn’t obvious: [*[1,2,3], 4] gives you [1,2,3,4].)

But since this is code golf, we had to make it as short as possible:

1
2
3
4
5
class Array
def deal*t
[*t.map{|s|pop s},self]
end
end

That’s as good as our team got, but as it turns out you can do two characters better:

1
2
3
4
5
class Array
def deal*t
t.map{|s|pop s}<<self
end
end

What I Learned

I knew it was possible to leave out the parentheses around the arguments in a method definition (though I never do it), but I had no idea that it was valid Ruby to put the splat right up against the method name without any space. I was very surprised to discover that def deal*t isn’t a parse error.

I wasn’t certain whether I could use a splat in [*four_element_list, fifth_element] to get the five element list I wanted, and was pleased to discover that splats can indeed be used in this way.

I had forgotten that Array#pop could be called with an argument. I almost always use it without an argument, which pops just one element (and returns that element rather than a one-element list of that element like pop(1) would).