A basic Ruby solution to the robot journeys coding exercise
Tags: development, exercise, ruby, beginners
Published: 2021-08-27 08:36:05

_Originally posted on [writtenby.adriengiboire.com](https://writtenby.adriengiboire.com/2019/10/28/a-basic-ruby-solution/)_

A fellow developer, which is an employee of a client of mine, published this [link](https://github.com/mikehadlow/Journeys) 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 (**E**ast, **S**outh, **W**est, **N**orth).

### Moves
A move is either the Robot moving **F**orward, or turning on itself either **L**eft or **R**ight.

## 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:

```ruby
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:

```ruby
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.

```ruby
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.

```ruby
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.

```ruby
  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.

```ruby
  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.

```ruby
  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.

```ruby
  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.

```ruby
  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.

```ruby
  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!