2
AUG2006
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)
-
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!
-
\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.-
(waps forehead) of course! Thanks, James.
-
-
-
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.
-
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