Well, if nothing else these are a fun little distraction, eh? Actually, I was
surprised to discover (when writing the quiz), how practical this challenge is.
Madlibs are really just a templating problem and that comes up in many aspects
of programming. Have a look at the "views" in Rails, for a strong real-world
example.
Looking at the problem that way got me to thinking, doesn't Ruby ship with a
templating engine? Yes, it does. We could use that to build our solution:
#!/usr/local/bin/ruby -w
# use Ruby's standard template engine
require "erb"
# storage for keyed question reuse
$answers = Hash.new
# asks a madlib question and returns an answer
def q_to_a( question )
question.gsub!(/\s+/, " ") # noramlize spacing
if $answers.include? question # keyed question
$answers[question]
else # new question
key = if question.sub!(/^\s*(.+?)\s*:\s*/, "") then $1 else nil end
print "Give me #{question}: "
answer = $stdin.gets.chomp
$answers[key] = answer unless key.nil?
answer
end
end
# usage
unless ARGV.size == 1 and test(?e, ARGV[0])
puts "Usage: #{File.basename(__FILE__)} MADLIB_FILE"
exit
end
# load Madlib, with title
madlib = "\n#{File.basename(ARGV[0], '.madlib').tr('_', ' ')}\n\n" +
File.read(ARGV[0])
# convert ((...)) to <%= q_to_a('...') %>
madlib.gsub!(/\(\(\s*(.+?)\s*\)\)/, "<%= q_to_a('\\1') %>")
# run template
ERB.new(madlib).run
The main principal here is to convert ((...)) to <%= ... %>, so we can use
Ruby's own template engine. Of course, <%= a noun %> isn't going to be valid
Ruby code, so a helper method is needed. That's where q_to_a() comes in. It
takes the Madlib replacements as an argument and returns the user's answer. To
use that we actually need to convert ((...)) to <%= q_to_a('...') %>. From
there, ERb does the rest of the work for us.
Now for simple Madlibs, you don't really need something as robust as ERb. It's
easy to roll your own solution and most people did just that. Let's examine
Sean E. McCardell's code:
class Madlib
# Given the madlib text as a string, builds a list of questions and
# a map of questions to "blanks"
def initialize(txt)
@questions = []
@story_parts = []
@answer_list = []
@answers = []
stored = {}
txt.split(/\((\([^)]*\))\)/).each do |item|
if item[0] == ?(
item = item[1..-2].gsub("\n", ' ')
if item.index(':')
name, question = item.split(':')
stored[name] = @questions.length
@questions << question
else
name, question = item, item
end
@answer_list << (stored[name] || @questions.length)
@questions << question unless stored[name]
else
@story_parts << item
end
end
end
# Calls a block with the index and text of each question
def list_questions(&block)
@questions.each_index do |i|
yield(i, @questions[i])
end
end
# Stores the answer for a given question index
def answer_question(i, answer)
@answers[i] = answer
end
# Returns a string with the answers filled-in to their respective blanks
def show_result
real_answers = @answer_list.collect {|i| @answers[i]}
@story_parts.zip(real_answers).flatten.compact.join
end
end
# Example that reads the madlib text from a file specified on the
# command line
madlib = Madlib.new(IO.read(ARGV.shift))
answers = []
madlib.list_questions do |i, q|
print "Give me " + q + ": "
answers[i] = gets.strip
end
answers.each_index {|i| madlib.answer_question(i, answers[i]) }
puts madlib.show_result
The Madlib object handles the heavy lifting here. initialize() really does a
lot of the work. It breaks the story down into an internal format which is
primarily a list of @story_parts, @questions, and @answers. Since the answer to
a question may be used in more than one place, an @answer_list is also built as
a mapping between the actual answers and all their replacements.
You can see this chunking process in the bottom half of initialize(). It
basically split()s the story around ((...)) replacement sections. The split()
Regexp uses capturing parentheses to ensure that the replacements themselves are
returned, in addition to the story parts.
Inside the iterator, the outer if branches to handle either questions (starting
with a "(" character) or story parts. Each item is added to the correct list.
Questions are also examined for the extra label and the stored Hash resolves
these repeats as they occur.
The next method, list_questions(), provides iteration over the list of
questions. (Note that the &block parameter isn't used in the method and could
be removed.) The block is yielded an index and the current question. The index
can be used to feed an answer to the sister method, answer_question(), which
just stores answers.
The final method of the class, show_result(), uses the @answer_list map to
construct a list of real_answers. That list is zip()ed with @story_parts to
produce the final output.
The final chunk of code just puts the class to work. An object is constructed
from the file passed as a command-line argument. Next, the code walks the
questions, asking each one in turn and collecting answers. Those answers are
passed to answer_question(), and the final results are printed. I believe you
could do away with the extra Array in this section and simplify a little:
madlib = Madlib.new(IO.read(ARGV.shift))
madlib.list_questions do |i, q|
print "Give me " + q + ": "
madlib.answer_question(i, gets.strip)
end
puts madlib.show_result
Well, there's a look at a couple of the solutions. Other solutions involved
CGI, PDF output (very cool!), and even a little golf action. Don't miss looking
over them.
My thanks to all the spongy Madlibers who took the time to fill out my
fire-hose.
Tomorrow we'll use the quiz to start a new library for Ruby that will hopefully
ease the ins and outs of common coding...