195 Rhyming Words

Description

Salutations fellow Rubyists,

This weeks quiz comes courtesy of Redd Vinylene:

How do I match words that rhyme, like end rhymes, last syllable
rhymes, double rhymes, beginning rhymes and first syllable rhymes?

Like rhymer.com. I'm looking to improve my freestyle skills :)

http://www.youtube.com/watch?v=SmqXKbxDoJ0

Your task is to create a Ruby program that when given a word and a type of rhyme (end rhymes, last syllable rhymes, double rhymes, beginning rhymes, first syllable rhymes) returns a list of rhyming words.

Have Fun!

Summary

This week’s quiz had an excellent solution from Eugene Kalenkovich.

In order to get this to work on my machine in 1.8 or 1.9 I had to add the following:

# Adding 1.9 and 1.8 compatibility
if :a.respond_to? :to_proc
  class String
    include Enumerable
    alias :each :each_line
  end
else
  class Symbol
    def to_proc
      proc { |obj, *args| obj.send(self, *args) }
    end
  end
end

This is probably not the best overall solution to make code 1.9 compatible, but it does illustrate some of the differences between Ruby versions. It appears as though the code was written for 1.8 but with Facets or another library defining Symbol#to_proc.

Eugene’s solution requires a database (plain text file of words and pronunciations) to operate. The file contains entries in the following form:

# ...
MONGOLS  M AA1 NG G AH0 L Z
MONGOOSE  M AA1 NG G UW0 S
# ...
REPARATIONS  R EH2 P ER0 EY1 SH AH0 N Z
REPARTEE  R EH2 P ER0 T IY1
# ...

These are phonetic representations of the words. The readme file that accompanies the database describes the pronunciations in detail if you are interested in learning more.

# lookup tables to store the words and rhyming words
$words = Hash.new{|h,k| h[k]=[]}
$rhymes = Hash.new{|h,k| h[k]=[]}

The perfect_key method is what determines what words perfectly rhyme with one another. It iterates through the reverse of the sound list and stops after adding the first stress. So if the word is ‘mongoose’ perfect_key will return ["S", "UW0", "G", "NG", "AA1"].

# Returns an array of sounds starting at the end and 
# going through the first stress
def perfect_key(pron)
  key = []
  pron.reverse.each do |snd|
    key << snd
    break if snd =~ /1$/
  end
  key
end

The rhymes method is used to display the rhyming words in the output. It gets all the rhyming words for the given word’s perfect key and returns the list minus the word itself.

def rhymes(word)
  wup = word.upcase
  $rhymes[perfect_key($words[wup])] - [wup]
end

Reading the data file and creating the tables of pronunciation and words is the bulk of the processing. This section iterates through each line in the word data file and creates a mapping from the word to the pronunciation, skipping comment lines or lines that have words with non-alpha characters. After mapping the word to the pronunciation it adds the word to the list of rhymes that is mapped by the perfect key of the pronunciation in the $rhymes hash.

File.open('mpron/cmudict0.3') {|f| f.readlines}.each do |l|
  w, *pron = l.strip.split(' ')
  next unless !w.empty? and w.grep(/[^A-Z]/).empty?
  pron.map!{|sound| sound.sub(/2/,'1')}
  $words[w] = pron
  $rhymes[perfect_key(pron)] << w
end

The program will find rhymes for all command line arguments passed in, or some example arguments if none are given. Having already loaded the data into a mapping that is convenient, all that is left is to lookup the lists of words that match the given words perfect rhymes and print them out.

input = ARGV.empty? ? ['laughter', 'soaring', 'antelope'] : ARGV
results = input.map do |w|
  w + ': ' + rhymes(w).map(&:downcase).join(', ') + "\n"
end.join("\n")

print results

Thank you, Eugene, for a very cool solution!


Saturday, March 07, 2009