A fellow developer, which is an employee of a client of mine, published this link on the client's Slack. When it's not for an interview, I am always keen to give a shot at any test even though lately I've lacked discipline and spent way too much time on Netfouix.

Tonight, I felt like it and since I've been working with JavaScript almost exclusively for a few months, I wanted to go back to ruby a little.

The Challenge

You start with a file containing 0+ journeys. A journey is made of 3 lines looking like this:

0 3 W
LLFFFLFLFL
2 4 S

The first line is the current position of the Robot.
The second line is the list of movements of the Robot.
The third line is where the Robot is supposed to be at the end of its journey.

Position

A position is made of the coordinates (x, y) and the direction the Robot is facing toward (East, South, West, North).

Moves

A move is either the Robot moving Forward, or turning on itself either Left or Right.

The implementation

Build the data

The first thing I went through was building the data and cleaning the inputs.

Remember, you are getting the journeys from a file:

inputs = []
File.open('journeys.txt', 'r') do |file|
  file.each_line do |line|
    inputs << line.gsub("\n", "")
  end
end

As you can see, we already started cleaning the data. A catch here is the carriage "\n". If you leave it as it is, you will never reach equality because what you will see looks like 1 2 E but what the computer reads is actually 1 2 E\n so when you compared what you get from the file and your result, you will always get a false return.

Then, for ease of usability, we want to delete blank lines:

inputs.delete("")

Since Array#delete transforms the original array (sic), we don't have to assign the result.

The TDD-ish approach

I decided to write the final code the way I intended it to work. At first, it would fail since nothing was implemented but then, incrementally implement the code.

inputs.each_slice(3).each do |slice|
  wallee = Robot.new(slice[0])
  wallee.move(slice[1])
  puts "Actual: #{wallee.position}"
  puts "Expected: #{slice[2]}"
  puts wallee.position == slice[2]
end

Nothing serious here. Notice just the use of Array#each_slice to facilitate the job.

I love functional but when writing ruby I am used to OOP and as you'll see, I did not change this habit :)

The Robot directions

Here is the skeleton of what we've seen so far. This is the minimum list of functions we will have to implement to make our test pass.

class Robot
  def initialize position
  end

  def move moves
  end

  def move_right
  end

  def move_left
  end

  def move_forward
  end

  def position
  end
end

Since the position is made of coordinates and a direction, we have to deconstruct the data in order to exploit it later easily.

  def initialize position
    @coordinates = position.split(' ')[0, 2].map(&:to_i)
    @direction = position.split(' ')[2]
  end

Same here, we have a trick with the types. Coordinates are a tuple of integers. If we don't make the conversion, we will have issues when modifying data as you'll notice later.

Robot, move!

Next, we want to work on the basic moves.

Learning the moves

The simplest are the directions. I went with a basic array of strings that contains the 4 possible directions, ordered. Then, we can simulate the rotation by calculating the new index according to the new direction.

  DIRECTIONS = ['N', 'E', 'S', 'W']

  def move_right
    index = (DIRECTIONS.find_index(@direction) + 1) % DIRECTIONS.count
    @direction = DIRECTIONS[index]
  end

  def move_left
    index = (DIRECTIONS.find_index(@direction) - 1) % DIRECTIONS.count
    @direction = DIRECTIONS[index]
  end

Then comes the forward move. This is a little bit more complex. What I mean is I did not find an elegant way of doing. I'm opened to suggestions!

Basically, you change by +/- 1 either x or y according the direction the Robot is pointing toward when moving forward.

  def move_forward
    case @direction
    when 'N'
      @coordinates = [@coordinates[0], @coordinates[1] + 1]
    when 'E'
      @coordinates = [@coordinates[0] + 1, @coordinates[1]]
    when 'S'
      @coordinates = [@coordinates[0], @coordinates[1] - 1]
    when 'W'
      @coordinates = [@coordinates[0] - 1, @coordinates[1]]
    end
  end

Nothing difficult but a lot more verbose than what we've seen so far.

Execute the moves!

Now that our Robot knows how to move, let's actually move. No big deal coming here. A hash containing the different moves as keys, associated to the method that knows how to execute the given move.

  MOVES = {
    L: 'move_left',
    R: 'move_right',
    F: 'move_forward'
  }

Then, all that's left is the method move which calls the method with the help of the MOVES hash.

  def move moves
    moves.split('').each do |move|
      self.send(MOVES[move.to_sym])
    end
  end

The arrival

Now that we know how to parse the journeys, we just have to reconstruct the calculated position to be able to compare it to the journeys data.

  def position
    [@coordinates.flatten, @direction].join(' ')
  end

And there we are.

The ending word

This was a fun test. Since it's been a while since I have been writing some ruby, I had to check the doc for simple stuff. It took about 30 minutes. Nothing crazy, I'm a normal human being. I'm not an alien :P

Hope you enjoyed the journey! If you have suggestion to make it better, I am willing to learn as always!

Cheers!