Early Steps

My content targeted at the newer Rubyists among us.

2

AUG
2006

RegexpChallenge

Just recently I have been working with two different people to improve their regular expression skills. To help me in this endeavor, I built a trivial little script we have been using in IRb. To get started, you construct a new challenge object and add a couple of challenges:

>> reg_chal = RegexpChallenge.new
No challenges.
=> 
>> reg_chal.challenge("Gray, James", "James", "The names can vary.")
=> nil
>> reg_chal.challenge("abbbbbbbc bc", 10)
=> nil
>> reg_chal.challenge( "    \n\t  ", nil,
?>                     "We want to test for non-space data." )
=> nil
>> reg_chal.challenge( "cogs 9, widgets 12, ...", "12",
?>                     "The numbers can vary." )
=> nil
>> reg_chal.challenge( "I'm a simple sentence, with words.",
?>                     %w[I'm a simple sentence with words] )
=> nil

You can ask for challenges to see what you would like to solve:

>> reg_chal.challenges
Challenge #0:
   Input:  "Gray, James"
  Output:  "James"
    Note:  "The names can vary."
Challenge #1:
   Input:  "abbbbbbbc bc"
  Output:  10
Challenge #2:
   Input:  "    \n\t  "
  Output:  nil
    Note:  "We want to test for non-space data."
Challenge #3:
   Input:  "cogs 9, widgets 12, ..."
  Output:  "12"
    Note:  "The numbers can vary."
Challenge #4:
   Input:  "I'm a simple sentence, with words."
  Output:  ["I'm", "a", "simple", "sentence", "with", "words"]
=> nil

Finally, you attempt solutions by giving the system the index of the challenge, a method name to call on the input String, a Regexp to pass, and any other needed parameters:

>> reg_chal.solve(0, :=~, /, \w+/)
That is not a valid solution.
Expected output:  "James"
    Your output:  4
=> nil
>> reg_chal.solve(0, :[], /\w+$/)
Correct.  Nice job.
=> nil
>> reg_chal.solutions
Solution #0:
     Input:  "Gray, James"
    Output:  "James"
      Note:  "The names can vary."
  Solution:  [](/\w+$/)
=> nil

After you have played with it for a while you will probably build up some solutions and challenges. You can use the save() method to dump those to a YAML file and even load() them back later, if needed.

Finally, for some real fun, the challenger supports sharing the challenges over a network with another user. The host should just change the initial construction call to:

reg_chal = RegexpChallenge.host

And then another user can get a copy of the challenge object with:

reg_chal = RegexpChallenge.join("HOST IP ADDRESS HERE")

If that sounds like something you would like to play with, here's the actual code for the library you load into IRb:

#!/usr/bin/env ruby -w

require "yaml"
require "drb"

$SAFE = 1
PORT  = 61676

class RegexpChallenge
  def self.host(challenges = nil)
    hosted_challenges = challenges || new
    DRb.start_service("druby://0.0.0.0:#{PORT}", hosted_challenges)
    puts "Now serving challenges on port #{PORT}..."
    hosted_challenges
  end

  def self.join(ip_address)
    DRb.start_service
    puts "Connecting to challenges on port #{PORT} at IP #{ip_address}..."
    challenges = DRbObject.new_with_uri("druby://#{ip_address}:#{PORT}")
    $stdout.extend(DRbUndumped)
    challenges.drb_stdout = $stdout
    challenges
  end

  def self.load(file_name = "regexp_challenge.yaml")
    File.open(file_name) { |file| YAML.load(file) }
  end

  def initialize
    @challenges = Array.new
    @solutions  = Array.new

    @drb_stdout = nil
  end

  attr_writer :drb_stdout

  def io
    if caller.any? { |call| call =~ /\bdrb\b/ }
      @drb_stdout || $stdout
    else
      $stdout
    end
  end

  def challenge(input, output, note = nil)
    @challenges << [input, output, note] and nil
  end

  def challenges
    if @challenges.empty?
      io.puts "No challenges."
    else
      @challenges.each_with_index do |challenge, i|
        io.puts describe_challenge(challenge, i)
      end
    end && nil
  end
  alias_method :inspect, :challenges

  def solve(challenge_number, method_call, regexp, *additional_args)
    answer = @challenges[challenge_number][0].send( method_call,
                                                    regexp,
                                                    *additional_args )
    if answer == @challenges[challenge_number][1]
      io.puts "Correct.  Nice job."
      @solutions << @challenges.slice!(challenge_number) + [ method_call,
                                                             regexp,
                                                             additional_args ]
    else
      io.puts "That is not a valid solution."
      io.puts "Expected output:  #{@challenges[challenge_number][1].inspect}"
      io.puts "    Your output:  #{answer.inspect}"
    end && nil
  end

  def solutions
    @solutions.each_with_index do |solution, i|
      io.puts describe_challenge(solution, i, "Solution").
              map { |line| line.sub(/^  /, "\\0\\0") }
      description = "  Solution:  #{solution[3]}(#{solution[4].inspect}"
      unless solution[5].empty?
        description << ", #{solution[5].map { |arg| arg.inspect }.join(', ')}"
      end
      io.puts description + ")"
    end && nil
  end

  def save(file_name = "regexp_challenge.yaml")
    File.open(file_name, "w") { |file| YAML.dump(self, file) }
  end

  private

  def describe_challenge(challenge, number, title = "Challenge")
    description = [ "#{title} ##{number}:",
                    "   Input:  #{challenge[0].inspect}",
                    "  Output:  #{challenge[1].inspect}", ]
    unless challenge[2].nil?
      description << "    Note:  #{challenge[2].inspect}"
    end
    description
  end
end

Feel free to post challenges for people to try in the comments of this post.

Comments (5)
  1. Jeff
    Jeff August 2nd, 2006 Reply Link

    Great idea, James. Can I ask a quick question about the code? I didn't understand the purpose behind this line in the 'solutions' method:

    .map { |line| line.sub(/^  /, "\\\\0\\\\0") }
    

    You're replacing leading spaces in each line with what exactly (and why)?

    Thanks!

    1. Reply (using GitHub Flavored Markdown)

      Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

      Ajax loader
    2. James Edward Gray II
      James Edward Gray II August 2nd, 2006 Reply Link

      \0 is the entire match in a replacement String ($& after the match). I'm just doubling the leading space so that the longer word "Solution," lines up neatly with the others.

      1. Reply (using GitHub Flavored Markdown)

        Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

        Ajax loader
      2. Jeff
        Jeff August 3rd, 2006 Reply Link

        (waps forehead) of course! Thanks, James.

        1. Reply (using GitHub Flavored Markdown)

          Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

          Ajax loader
  2. Ryan Bates
    Ryan Bates August 3rd, 2006 Reply Link

    Very cool. I'll definitely be playing around with this. Thanks!

    Interestingly, I created something similar for my Rails Day project. You can download the source here: http://svn.railsday2006.com/railsday/team61/regex_tutor/trunk/

    You may need to use the latest version of Safari or Firefox to see it properly though. Maybe someday I'll turn it into a real site.

    1. Reply (using GitHub Flavored Markdown)

      Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

      Ajax loader
  3. Gregory
    Gregory August 16th, 2006 Reply Link

    Here is my (completed untested) modification which allows you to use a block for your solution and match any ruby object.

    This does all sorts of destruction to the pretty 'solutions' stuff you do, but was a fun hack on the plane on the way back from OKC

    http://stonecode.org/drb_challenger.rb

    1. Reply (using GitHub Flavored Markdown)

      Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

      Ajax loader
Leave a Comment (using GitHub Flavored Markdown)

Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

Ajax loader