🎉 Exercism Research is now launched. Help Exercism, help science and have some fun at research.exercism.io 🎉
Avatar of chrisbodhi

chrisbodhi's solution

to Robot Name in the Ruby Track

Published at Jul 13 2018 · 8 comments
Instructions
Test suite
Solution

Note:

This solution was written on an old version of Exercism. The tests below might not correspond to the solution code, and the exercise may have changed since this code was written.

Manage robot factory settings.

When robots come off the factory floor, they have no name.

The first time you boot them up, a random name is generated in the format of two uppercase letters followed by three digits, such as RX837 or BC811.

Every once in a while we need to reset a robot to its factory settings, which means that their name gets wiped. The next time you ask, it will respond with a new random name.

The names must be random: they should not follow a predictable sequence. Random names means a risk of collisions. Your solution must ensure that every existing robot has a unique name.

In order to make this easier to test, your solution will need to implement a Robot.forget method that clears any shared state that might exist to track duplicate robot names.

Bonus points if this method does not need to do anything for your solution.


For installation and learning resources, refer to the exercism help page.

For running the tests provided, you will need the Minitest gem. Open a terminal window and run the following command to install minitest:

gem install minitest

If you would like color output, you can require 'minitest/pride' in the test file, or note the alternative instruction, below, for running the test file.

Run the tests from the exercise directory using the following command:

ruby robot_name_test.rb

To include color from the command line:

ruby -r minitest/pride robot_name_test.rb

Source

A debugging session with Paul Blackwell at gSchool. http://gschool.it

Submitting Incomplete Solutions

It's possible to submit an incomplete solution so you can see how others have completed the exercise.

robot_name_test.rb

require 'minitest/autorun'
require_relative 'robot_name'

class RobotTest < Minitest::Test
  NAME_REGEXP = /^[A-Z]{2}\d{3}$/

  def setup
    Robot.forget
  end

  def test_can_create_a_robot
    skip
    refute_nil Robot.new
  end

  def test_has_name
    skip
    assert_match NAME_REGEXP, Robot.new.name
  end

  def test_name_sticks
    skip
    robot = Robot.new
    original_name = robot.name
    assert_equal original_name, robot.name
  end

  def test_reset_changes_name
    skip
    robot = Robot.new
    original_name = robot.name
    robot.reset
    refute_equal original_name, robot.name
  end

  def test_reset_before_name_called_does_not_cause_an_error
    skip
    robot = Robot.new
    robot.reset
    assert_match NAME_REGEXP, Robot.new.name
  end

  def test_reset_multiple_times
    skip
    robot = Robot.new
    names = []
    5.times do
      robot.reset
      names << robot.name
    end
    # This will probably be 5, but name uniqueness is only a requirement
    # accross multiple robots and consecutive calls to reset.
    assert names.uniq.size > 1
  end

  def test_different_robots_have_different_names
    skip
    refute_equal Robot.new.name, Robot.new.name
  end

  # This test assumes you're using Kernel.rand as a source of randomness
  def test_different_name_when_chosen_name_is_taken
    skip
    same_seed = 1234
    Kernel.srand same_seed
    robot_1 = Robot.new
    name_1  = robot_1.name
    Kernel.srand same_seed
    robot_2 = Robot.new
    name_2 = robot_2.name
    refute_equal name_1, name_2
  end

  def test_generate_all_robots
    skip
    all_names_count = 26 * 26 * 1000
    time_limit = Time.now + 60 # seconds
    seen_names = Hash.new(0)
    robots = []
    while seen_names.size < all_names_count && Time.now < time_limit
      robot = Robot.new
      seen_names[robot.name] += 1
      robots << robot
    end
    timeout_message = "Timed out trying to generate all possible robots"
    assert_equal all_names_count, robots.size, timeout_message
    assert seen_names.values.all? { |count| count == 1 }, "Some names used more than once"
    assert seen_names.keys.all? { |name| name.match(NAME_REGEXP) }, "Not all names match #{NAME_REGEXP}"
  end

  def test_version
    assert_equal 3, BookKeeping::VERSION
  end
end
class Robot
  
  def initialize
    @name = ''
  end
  
  def name
    2.times {@name << ALPHA_ARR.sample}
    3.times {@name << NUM_ARR.sample.to_s}
    @name
  end

  def reset
    @name = ''
  end

  private

  NUM_ARR   = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] # (0..9).to_a
  ALPHA_ARR = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"] # ('A'..'Z').to_a
end

Community comments

Find this solution interesting? Ask the author a question to learn more.
Avatar of lhagemann

Does the reset function pass the tests?

What is your reasoning for fully defining the array of numbers & letters instead of the more compact (range).to_a which you've noted?

Avatar of chrisbodhi
chrisbodhi
Solution Author
commented over 5 years ago

Hi @lhagemann,

The tests do pass: 4 tests, 7 assertions, 0 failures, 0 errors, 0 skips Is there something in the code that looks off?

My thinking is that leaving the arrays as constants would be faster [not necessarily at this scale!]: since there's nothing about them that's changing, there's no need to rebuild the array each time the name method is run.

Avatar of lhagemann

hmm, yeah, I don't know what it was about the reset method now that caused me to question it. For some reason I thought the test would end up with an empty string for the name.

Yes, a static array is faster, but at this scale I would go for the more condensed code. just my preference. logic makes sense.

Avatar of franrodalg

@chrisbodhi I also find it difficult to understand how the test cases are passed. I've checked them and they do, but in my opinion test_name_sticks should fail as every call to name will append a new random name to the previous one instead of returning the same string. For example, if we run a simple test: robot = Robot.new 2.times{puts robot.name}

we get: QF658 QF658YV199

making it clear that the test should fail.

I agree with @lhagemann in that code looks much nicer using the Range syntax. I don't think that the potential speed boost is big enough here.

@lhagemann the reset test passes because the new name is generated with the call to robot.name, so the blank string is "overwritten" (in fact, appended) with a new random name string.

Avatar of chrisbodhi
chrisbodhi
Solution Author
commented over 5 years ago

@franrodalg Thanks for taking the time to share your thought process. Let's look at test_name_sticks: def test_name_sticks robot = Robot.new robot.name assert_equal robot.name, robot.name end

If I'm understanding the question, it looks like that assert_equal calls robot.name twice; since the two return values are different, the test should fail. I wasn't sure how that method works, so I peeked at the source here [thankfully, it's also written in Ruby]. The method takes two values: the expected output and the actual output. It uses the system's diff tool to compare the two arguments and then proceeds based on the value returned by the diff.

I haven't used a lot of the Unix tools before, so I fired up pry and copied in my Robot class. I created a new robot instance [you can see in the returned object that the @name value is instantiated as an empty string] and used %x to run system [Unix] commands in the REPL, passing in the string-interpolated robot.name twice. robot = Robot.new =>#<0x007fff134761b0> %x(diff -u #{robot.name} #{robot.name}) diff: XP677XA768: No such file or directory diff: XP677XA768: No such file or directory =&gt;"" </0x007fff134761b0> I initially thought the diff would compare robot.name as a static object [something like nombre = robot.name and then diff -u #{nombre} #{nombre}], so I was surprised to see that the method gets called twice. However, it seems that the methods are running before the diff tool does its thing. I'm willing to wager that the object ID is the same for both return values. So, even though the return value doesn't meet the requirements for a robot name [two letter, three numbers], the value does persist and the test passes.

Avatar of zenspider

It is a bad / weak test.

String#<< is mutative, so while it is being called multiple times and returning different values, it's always the same instance. This is mainly about ruby being an applicative order evaluation language (like most languages). See #1.

diff runs against files, not arg contents, hence the error above. Minitest writes out 2 temp files and then runs diff against them.

Avatar of chrisbodhi
chrisbodhi
Solution Author
commented over 5 years ago

Thanks for the clarifications, @zenspider. What would you do to improve the test?

Avatar of zenspider

@chrisbodhi a simple dup of the results would ensure that mutation is tested against.

What can you learn from this solution?

A huge amount can be learned from reading other people’s code. This is why we wanted to give exercism users the option of making their solutions public.

Here are some questions to help you reflect on this solution and learn the most from it.

  • What compromises have been made?
  • Are there new concepts here that you could read more about to improve your understanding?