[SOLUTION] Madlibs (#28)

Here is my solution to the current quiz. While implementing this I learned more about the power of gsub, which I don't think I've ever used so much in one program as small as this one :wink:

I output the result in either text or PDF if given the proper command-line options. To use the PDF feature you will need to install PDF::Writer, available from here: http://rubyforge.org/projects/ruby-pdf/

What a fun quiz this was, especially using the program to create some Madlibs. I also decided to create an additional madlib file for testing/having fun with. This is below following the code. The source for this was http://www.geocities.com/pezheadpaul/madlibs/prints/printprojects.htm and all credit goes to the author of that site. This is a good test because it has multiple paragraphs.

I hereby decree than an additional rule for this quiz would be that every solution include a new madlib file :slight_smile:

Ryan Leavengood

CODE (beware of wrapping):

def do_madlib(madlib_file, output_pdf, underline)
    substitutions = {}
    # Put paragraphs all on one line (there has got to be a better way!)
    madlib = IO.readlines(madlib_file).join('').split(/\n\n/).collect{|l|l.gsub(/\n/,' ')}.join("\n\n")
    madlib_result = madlib.gsub(/\(\([^)]*\)\)/) do |match|
       match.gsub!(/\(|\)/,'') # Bye bye parens
       if substitutions[match]
          substitution = substitutions[match]
       else
          if match =~ /(.*):(.*)/
             key = $1
             match = $2
          end
          print "Please enter #{match}: "
          substitution = $stdin.gets.chomp
          if key
             substitutions[key] = substitution
          end
       end
       if output_pdf and underline
          "<u>#{substitution}</u>"
       else
          substitution
       end
    end
    if output_pdf
       filename = madlib_file+".pdf"
       print "Outputting PDF file #{filename}..."
       require 'pdf/ezwriter'
       pdf = PDF::EZWriter.new
       pdf.select_font("pdf/fonts/Helvetica")
       pdf.ez_text(madlib_result, 14)
       File.open(filename, File::RDWR|File::CREAT|File::TRUNC) do |file|
          file.print(pdf.ez_output)
       end
       puts "done."
    else
       puts "\n Your MadLib:\n\n"
       madlib_result.split("\n").each do |paragraph|
          # Lazy man's wrapping
          puts paragraph.gsub(/.{72}[^ ]* /){|l|"#{l}\n"}
       end
    end
end

if $0 == __FILE__
    if ARGV.length < 1
       puts "Usage: #$0 [-pdf] [-u] <madlib file>"
       puts " -pdf Output a PDF file."
       puts " -u Underline the replaced words in the PDF output file."
       exit(1)
    end
    output_pdf = false
    underline = false
    filename = nil
    while ARGV.length > 0
       arg = ARGV.shift
       case arg
          when '-pdf': output_pdf = true
          when '-u': underline = true
          else filename = arg
       end
    end
    if filename
       if test(?e, filename)
          do_madlib(filename, output_pdf, underline)
       else
          puts "Provided madlib file does not exist!"
       end
    else
       puts "No madlib filename provided!"
    end
end

Science_Fair.madlib:

According to ((principal:a school principal's name)), the school science fair this year was 'very educational.' At the same time, ((principal)) announced plans to quit the school system and become a ((a dangerous job)). 'It sounds like a safer job,' the principal said.

Several ((an adjective)) projects were disqualified this year. The experiment on 'Animal Magnetism' by ((a girl's name)) was canceled before she could plug in her ((an animal)).

The project by ((a male bully's name)) on 'Gravity's Effect on First Graders' was canceled when the custodians wouldn't let him borrow a ladder.

And the Nuclear Powered ((a noun)) built by ((name:a proper noun)) was taken away by the police, who said ((name)) will be back in school 'any day now.'

((your name)) won second prize with an experiment that asked, 'Can ((animals:a plural animal)) Learn Karate?' The answer was 'yes.' The ((animals)) tossed ((principal)) over a ((a noun)) and left the science fair. Anyone who sees them should call the main office.

((a girl's name)) won first prize with her TNT ((veggies:a plural vegetable)). By planting seeds in gunpowder and watering them with nitroglycerin, ((veggies)) grew that explode when you drop them. 'What a dynamite idea,' the principal joked ((an adverb)).

So far, nobody has figured out how the prize-winning ((veggies)) got into the salad served to ((principal)) at lunchtime. Just to be safe, though, the 'Vegetable Surprise' has been taken off tomorrow's lunch menu.

Hello,

My implementation does two passes. The first one gets the questions and changes the tags by a label (if it does not have a label it's created with the name "anonymous<number>". After answering questions the second pass just translates the tags by the answers. I had to add an array to keeping the questions order as I was using a hash to store the questions and answers. I had also to strip return characters, I know it should be a more elegant solution. The problem was the tags with return characters in it.

Here is the code

--8<------
class QAPair
   attr_accessor :question, :answer
   def initialize(q, a="empty")
     @question=q
     @answer=a
   end
end

class MadLib

   def initialize(text)
     @original=text.tr("\r\n", " ") # take care of multiline
     @anon_number=0
     @qa=Hash.new # Questions and Answers
     @qa_order=Array.new

     process
   end

   def process
     @processed=@original.gsub(/\(\((.*?)\)\)/) {|matched|
       question=matched[2..-3] # strip parentheses
       name="anonymous"+@anon_number.to_s
       if res=question.match(/(.*?):(.*)/)
         question=res[2]
         name=res[1]
       elsif @qa.include?(question)
         name=question
       else
         @anon_number+=1
       end
       @qa[name]=QAPair.new(question)
       @qa_order << name if !@qa_order.include? name
       "(("+name+"))"
     }
   end

   def make_questions
     @qa_order.each {|name|
       pair=@qa[name]
       print "Give me a "+pair.question+": "
       pair.answer=STDIN.gets.chop
     }
   end

   def create_text
     @processed.gsub(/\(\(.*?\)\)/) {|matched|
       name=matched[2..-3]
       @qa[name].answer
     }
   end

end

if ARGV[0]
   begin
     test=MadLib.new(File.read(ARGV[0]))
   rescue
     puts "File #{ARGV[0]} not found."
     exit -1
   end
else
   puts "madlib file as parameter is needed"
   exit -1
end

test.make_questions
puts test.create_text
------>8--

Here's the code running on rubyquiz.com.

James Edward Gray II

#!/usr/local/bin/ruby

MADLIBS = "../public_html/madlibs"
REPLACE = /\(\(\s*((?:\w+\s*:\s*)?)(.+?)\s*\)\)/m

require "cgi"
require "erb"

query = CGI.new("html4")
files = Dir[File.join(MADLIBS, "*.madlib")].
         map { |f| File.basename(f, ".madlib").tr("_", " ") }
title = nil

content = case query["mode"]
when "questions"
  title = query["madlib"]
  madlib = File.read( File.join( MADLIBS,
                                 "#{query['madlib'].tr(' ', '_')}.madlib" ) )
  count = 0
  seen = Hash.new(false)

  query.form("post") do
    query.hidden("mode", "display") +
    query.hidden("madlib", query["madlib"]) +
    query.dl do
      madlib.scan(REPLACE).inject("") do |fields, (key, question)|
        key = if key.length > 0
          key[/\w+/]
        else
          next fields if seen[question]
          (count += 1).to_s
        end
        seen[key] = true

        fields += query.dt("style" => "font-weight: normal") do
          "Give me #{query.b { question }}."
        end
        fields += query.dd { query.text_field(key) }
      end
    end +
    query.submit("finish")
  end
when "display"
  title = query["madlib"]
  madlib = File.read( File.join( MADLIBS,
                                 "#{query['madlib'].tr(' ', '_')}.madlib" ) )
  count = 0

  madlib.split(/\n(?:\s*\n)+/).inject("") do |result, para|
    result += query.p do
      para.gsub(REPLACE) do
        if $1.length > 0
          query[$1[/\w+/]]
        elsif query.has_key?($2)
          query[$2]
        else
          query[(count += 1).to_s]
        end
      end
    end
  end +
  query.p { "&nbsp;" } * 7
else # choose
  query.p { "Please choose a Madlib from the following list:" } +
  query.form("get") do
    query.hidden("mode", "questions") +
    query.popup_menu("madlib", *files) + " " +
    query.submit("choose")
  end +
  query.p { "&nbsp;" } * 10
end

include ERB::Util
page = ERB.new(DATA.read, nil, "%")
query.out { page.result(binding) }

__END__
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
                       "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
  <title>Ruby Quiz</title>
  <link rel="stylesheet" type="text/css" href="../quiz.css" />
</head><body>
  <div id="page">
    <div id="header"><span class="ruby">Ruby</span>
                     <span class="quiz">Quiz</span></div>
    <div id="content">
      <span class="title"><%= title || "Ruby Quiz Madlibs"%></span>
      <%= content %>
    </div>
    <div id="logo"><img src="../images/ruby_quiz_logo.jpg" alt=""
                        width="157" height="150" /></div>
    <div id="links">
      <span class="title">Madlibs</span>
      <ol>
% files.each do |file|
        <li><a href="madlib.cgi?mode=questions&madlib=<%= u file %>"><%=
            file
        %></a></li>
% end
      </ol>
    </div>
    <div id="footer">&nbsp;</div>
  </div>
</body>
</html>

Awesome idea. Wish I had thought of that one, because I would have used it!

I've added your Madlib to those served on rubyquiz.com.

Here's my run with it:

Science Fair

According to Mr. McFee, the school science fair this year was 'very educational.' At the same time, Mr. McFee announced plans to quit the school system and become a Plutonium Expert. 'It sounds like a safer job,' the principal said.

Several fuzzy projects were disqualified this year. The experiment on 'Animal Magnetism' by Dana was canceled before she could plug in her Shih Tzu.

The project by Ken on 'Gravity's Effect on First Graders' was canceled when the custodians wouldn't let him borrow a ladder.

And the Nuclear Powered computer built by Coke Classic was taken away by the police, who said Coke Classic will be back in school 'any day now.'

James won second prize with an experiment that asked, 'Can fleas Learn Karate?' The answer was 'yes.' The fleas tossed Mr. McFee over a snowboard and left the science fair. Anyone who sees them should call the main office.

Tina won first prize with her TNT tomatoes. By planting seeds in gunpowder and watering them with nitroglycerin, tomatoes grew that explode when you drop them. 'What a dynamite idea,' the principal joked hesitantly.

So far, nobody has figured out how the prize-winning tomatoes got into the salad served to Mr. McFee at lunchtime. Just to be safe, though, the 'Vegetable Surprise' has been taken off tomorrow's lunch menu.

···

On Apr 17, 2005, at 8:25 AM, Ryan Leavengood wrote:

I hereby decree than an additional rule for this quiz would be that every solution include a new madlib file :slight_smile:

----

That was great! Thanks.

James Edward Gray II

Here is my solution for the Madlibs Ruby Quiz. I tried to keep the
process of asking the questions separate from assembling the completed
madlib with the answers.

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

--Sean McCardell

Here's mine, its probably a bit javaish compared to most, but I'm
getting there :slight_smile:

template="Our ((var1:a)) favorite ((var2:bb)) language ((var1)) is ((a
gemstone))."
vars={}

rep = template.gsub(/\(\((.*?)\)\)/) { |match|

  toks = match.split(":")
  if toks.length == 1
    varName = nil
    varPrompt=toks[0][2..(toks[0].length-3)]
  else
    varName=toks[0][2..(toks[0].length-1)]
    varPrompt=toks[1][0..(toks[1].length-3)]
  end

# Alternative but just a many lines
# ndx = match.index(":")
# if ndx == nil
# varName = nil
# varPrompt = match[2..(match.length-3)]
# else
# varName = match[2..(ndx-1)]
# varPrompt = match[(ndx+1)..(match.length-3)]
# end

  if varName == nil && vars[varPrompt] != nil
    userInp=vars[varPrompt]
  else
    print "#{varPrompt} "
    userInp=gets.chomp
    vars[varName]=userInp
  end

  userInp
}

puts rep

Version 1 is a function, and requires serial input. It looks a lot like
Dominik's solution, though I didn't see his before completing this. I think
mine is prettier, though Dominik seems to have terseness down pat :slight_smile:

Cheers,
Dave

···

#
# This function is the solution to the quiz. It takes a madlib string, fills
# the placeholders using input() (defined below) and returns the result.
#
def madlib(string)
names = {}
string.gsub /\(\(.*?\)\)/ do |token|
  a, b = *token[2...-2].split(':')
  if names.has_key? a
   names[a]
  elsif b
   names[a] = input(b)
  else
   input(a)
  end
end
end

#
# Ask the user for thing, and return the user's response.
#
def input(thing)
print "Enter #{thing}: "
gets.chomp
end

#
# Here's another interface - you can run this ruby script from the command
line
# and pass it a madlib filename as an argument, or pass the text on STDIN.
#
if $0 == __FILE__
puts madlib(ARGF.read)
end

Here's a different approach that came to me while writing the summary.

James Edward Gray II

#!/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

I wrote earlier:
# Version 1 is a function, and requires serial input.

Version 2 is a class, and handles random input, which means you can easily
use it to power your new graphical interface.

Cheers,
Dave

class MadLib

# You can enumerate the placeholders... not too useful, though.
include Enumerable

def initialize(string)

  # the original madlib string is kept
  @s = string

  # a placeholder for each user entry required
  # maps a tag name or number onto a [question, answer] array.
  @placeholders = {}

  # an index for each set of brackets in @s
  # the key into @placeholders for each respective set of brackets
  @index = []

  # the initial scan gets tag names and assigns numbers for tags with no
name
  i = '0'
  @s.scan /\(\(([^:)]*):?(.*?)\)\)/ do |a, b|
   if @placeholders.has_key? a
    @index << a
   elsif b.size > 0
    @index << a
    @placeholders.update a => [b, nil]
   else
    @index << i
    @placeholders.update i => [a, nil]
    i = i.succ
   end
  end
end

def []=(index, answer)
  @placeholders[index][1] = answer
  @placeholders[index]
end

def [](index)
  @placeholders[index]
end

def outstanding
  @index.select {|i| @placeholders[i][1].nil? }.uniq
end
def outstanding_questions
  outstanding.map {|i| @placeholders[i][0] }
end
def each_outstanding
  outstanding.each do |i|
   yield @placeholders[i]
  end
end

def all
  @index.uniq
end
def all_questions
  all.map {|i| @placeholders[i][0] }
end
def each
  all.each do |i|
   yield @placeholders[i]
  end
end

def done?
  @placeholders.all? {|k, v| v[1] }
end

def collect!
  all.each do |i|
   self[i] = yield @placeholders[i]
  end
  self
end

def to_s
  if done?
   story
  else
   ""
  end
end

private
  def story
   i = 0
   @s.gsub /\(\(.*?\)\)/ do |token|
    i = i.succ
    @placeholders[@index[i - 1]][1]
   end
  end
end

if $0 == __FILE__
m = MadLib.new(ARGF.read)
m.collect! do |question, answer|
  answer or begin
   print "#{question}? "
   gets.chomp
  end
end
puts m
end