Ruby Golf: The Results!

Back in November, I wrote a piece about Ruby Golf. At the end of the article I set a 5 holes as a challenge for readers to have a go at completing using the minumum number of bytes. There was a terrific response that saw a number of impressive ‘shots’.

The deadline passed at the end of 2011 and since then I’ve been going through all the entries to find the winners of each hole. There were some great solutions that showed some insightful uses of Ruby. It’s fair to say, though, that most of them looked like mind-boggling gobbledygook at first glance … although I think that is par for the course in code golf!

As well as using this post to anounce the winners, I thought it might be useful to go through some of the tricks that were used in each of the winning entries. So, if you feel like you’re stuck in a rough patch of code or a mental sand bunker, then read on – there’s plenty to learn here. Fore!

Example Hole

As part of the article, I presented this as an example of Ruby Golf:

Write a method that would find the sum of all multiples of a given number up to a given value. For example sum(5,24) would calculate the sum of all the multiples of 5 up to 24 (ie 5 + 10 + 15 + 20). Here was my method:

def sum(n,t)
  n*(1..t/n).to_a.inject(&:+)
end

Needless to say that my handicap could certainly be better and there were numerous improvements that the readers were only too keen to point out.

Kyle Putnam spotted that the to_a method isn’t needed as inject is a method of the Enumerable class which ranges inherit from, saving 5 bytes:

def sum(n,t)
  n*(1..t/n).inject(&:+)
end

Rein Henrichs also pointed out that you don’t need the & symbol in Ruby 1.9, and you can also use the reduce method, instead of inject (it has the same numnber of letters, but possibly has more meaning):

def sum(n,t)
  n*(1..t/n).reduce(:+)
end

But then Kyle Dean hit an amazing hole in 1 by employing a bit of elementary mathematics. There is already a formula for summing numbers, which can be used instead of iterating over a range:

def sum(n,t)
  k=t/n;n*k*(k+1)/2
end

This is a great example of eschewing fancy methods and obscure tricks and instead just relying on good old fashioned Mathematics to get the job done.

Hole 1: Fizz Buzz

Given a number the function returns “Fizz” if it is a multiple of 3, “Buzz” if it is a multiple of 5 and “FizzBuzz” if it is a multiple of 15. If the number is not a multiple of 3 or 5 then the number is returned as a string.

This was a fiercly contested hole, but Marcin Mańk drove straight down the fairway with this effort that weighs in at 41 bytes.

  def fizzbuzz n
    n%3<1&&f=:Fizz;n%5&lt;1?"#{f}Buzz":"#{f||n}"
  end

This entry uses the modulo operator to check if n is a multiple of 3. If it is, then n%3 will actually be 0, but since we are only dealing with integers, you can save a byte by testing if it is less than 1 (n%3<1) rather than equal to 0 (n%3==0).

The first statement uses the && operator to set f=:Fizz (note also the use of a symbol here, as it uses one less byte than a string) if the n%3<1 evaluates to true (ie if n is a multiple of 3). If n%3<1 is false (ie not a multiple of 3) then the second part is not reached, so the variable f is not set to anything.

After the ;, which signifies the end of a line of code, a similar test is then done to see if n is a multiple of 5. The ternary operator is then used to display one result if this is true and another result if it is false. If n is a multiple of 5 then the following string is returned:

"#{f}Buzz"

What is clever about this is that string interpolation is used to insert the value of f in front of the word Buzz. If f has been set to :Fizz (ie if n was also a multiple of 3 and therfore a multiple of 15), then it is automatically converted to a string and inserted in front of the word Buzz. If f has not been set (ie if n is not a multiple of 3, but still a multiple of 5) then #{f} is simply ignored and the string “Buzz” is all that is returned.

If n is not a multiple of 5 then the following string is returned:

"#{f||n}"

This is a smart use of the || operator to give two possible options. If f has been set, then ‘Fizz’ will be displayed (as a string rather than symbol because interpolation is again being used). The second part of the condition will only be reached if the first part returns false (ie f has not been set) and will then display the number entered (again, as a string due to interpolation).

A brilliant solution that built on the entry by Cyrus. Can it be beaten?

Hole 2: Caesar Cipher

Implement a Caesar Shift Cipher

Example:

    caesar("hello",3) => "khoor"

You should also be able to produce negative shifts.

I love codes and ciphers, so was particularly interested in the solution to this one. The best came in at a tiny 29 bytes, courtesy of Josh Cheek:

  def caesar(s,n)
    s.gsub(/./){|c|(c.ord+n).chr}
  end

This was a remarkably clean and efficient method that simply used the gsub method to substitute every letter using the regular expression /./ and a block that shifts each character forward n places. The foibles of Ruby dicatate that to add to a string you must first use the ord method to convert it to to it’s integer representation and then use the chr method to change it back to a string.

Hole 3: Rock,Paper,Scissors Game

Write a simple method that ‘plays’ this game, where the player enters their ‘move’ as an argument to the method. If the player enters an invalid option then the result should be ‘lose’. The computer should choose its move at random. The output gives the computer’s ‘move’ and the result as a comma-separated string.

This was my favorite hole and also had the most entries. The winning entry is a thing of beauty and I can’t get over how J–L has condensed the ubiquitous children’s game down into a measley 84 characters:

def play(i)
  m=%w(Rock Paper Scissors);m[/c]+?,+%w(Draw Win Lose)[((m.index(i)||c-1)-c)%3]
end

It starts by using the shortcut notation of creating an array of the three ‘moves’ and assigning it to a variable. Assignment is usually something to avoid in programming golf as it wastes a couple of bytes. But in this case it is necessary as the array is going to be used again later.

The next statement then just builds up the string that will be returned by the method. It starts by selecting the computers move from the array of moves (m) at random, The computers ‘move’ is calculated using the rand method, but a neat trick is used to assign the random number to a variable, c, while at the same time using it to reference the index of the moves array. This means that c can be used again later:

  m

Next a comma is concatenated to the string. The comma is created using the shortcut ?, which is one byte less than the more obvious string literal ','.

+?,

Last of all the result of the game – win lose or draw – is concatenated to the end. The result is determined by the following bit of code:

+%w(Draw Win Lose)[((m.index(i)||c-1)-c)%3]

m.index(i) finds the index of the player’s move. If this is not ‘Rock’, ‘Paper’ or ‘Scissors’ then it is replaced by 1 less than the computers move (which was set as c earlier in the method). This will guarantee that the player will lose, as you will see soon. Next comes a clever little algorithm that calculates if you have won, drawn or lost using modulo 3 arithmetic. If you subtract the index of the computers move from the index of your own move, then the answer is different depending on the result of the game:

eg 
You play Rock (index = 0). Computer plays Rock(index = 0). This is a draw:
0 - 0 = 0 = 0 mod 3
You play Rock (index = 0). Computer plays Scissors(index = 2). This is a win:
0 - 2 = - 2 = 1 mod 3
You play Rock (index = 0). Computer plays Paper(index = 1). This is a loss:
0 - 1 = - 1 = 2 mod 3

The result is different for depending on the outcome of the game (win, lost or draw). These results are the same for the other other 6 possible results (try it) and means that if you construct the array as %w(draw win lose) then the index matches the result of subtracting the moves.

Hole 4: String Counter

Write a method that when given a string and substring, returns the number of times the substring occurs in that string (ignoring case).

This appeared to be a relatively straightforward solution that was fairly easy to solve using the scan method. Marcin Mańk was the first to use this method, but made the schoolboy error of using the length method instead of size, which is 2 bytes shorter. Christian Guenther was quick to point this out and therefore he wins the prize for this solution in 20 bytes:

  def count(s,x) 
    s.scan(/#{x}/i).size
  end

This method works by changing the required substring into a regular expression using interpolation. Each occurence of the string is then placed into an array that is returned by the scan method. To find how many times the string appeared it is then simply a case of finding the size of this array, so the size method can be chained on to the end.

Hole 5: Swingers Function

Write a function that replaces ‘putting your keys in a tin’. The argument to the function is an array of arrays that contain two objects. The function returns a new array where the pairs of objects have been mixed up. An object should not end up with it’s original ‘partner’.

Example:

swingers([["Homer","Marge"],["Micky","Minnie"],["Fred","Wilma"],["Peter","Lois"],["George","Judy"]])
=> [["Homer","Wilma"],["Micky","Lois"],["Fred","Judy"],["Peter","Marge"],["George","Minnie"]]

Josh Cheek had the lowest entry, with 30 bytes:

    def swingers(s)
      f,l=s.transpose;f.zip l.rotate
    end

This solution uses mass assignment to assign the result of transposing the array of couples to two variables x and y. What this effectively does is put all the men in x and all the women in y. The zip function is then used to merge the two arrays back togehter, but the rotate method is applied to the y array (of women) first. This effectively shifts every woman along one place. This means that the first man gets put with the next woman along in the array.

This fulfills the requirement that was specified, although there was an implicit requirement that the mixing up should be random. This is easily achieved by applying the shuffle method to the original array. This was actually done early on by Remi, who is awarded the prize for this hole:

def swingers(a)
  x,y=a.shuffle.transpose;x.zip y.rotate
end

Overall

After such tough competition, I’m pleased to anounce the overall winner with the lowest overall score for all five holes … and winner of the inagural Ruby Blazer … is… Josh Cheek! Congratulations to the RubySource Masters Champion of 2011!

If you were one of the indiviual winners mentioned above, then there is a Sitepoint book waiting for you. You simply need to get in touch by leaving a comment below. Congratulations to all those winners, as well as everybody who took part. Remember, it is the process of trying to get a solution that helps to improve your coding skills and, just like in real golf, practice makes perfect. I certainly learned a lot of Ruby by reading through all of the solutions. It was fascinating to see the multitude of different approaches used to reach the same solution. I also liked the fact that a number of the entries built on previous attempts – the equivalent of taking the same route to the green and then sinking a long putt to win the hole! But are these the best possible answer? We can never be 100% sure a solution is a small as possible, so can anybody beat them? Have you learnt any cool new Ruby tricks from these entries or do you have any others that haven’t been mentioned? Let us know in the comments.

But the best thing about Ruby Golf is that at the end of a gruelling round of programming, you can still make a trip to the 19th hole at the end of the day!

Win an Annual Membership to Learnable,

SitePoint's Learning Platform

No Reader comments

Comments on this post are closed.