[SUMMARY] Morse Code (#121)

R

Ruby Quiz

I always forget how much I love the simple problems. Everyone plays when we
have them and the solutions tend to be quite creative. This time was no
different. Everyone did see MenTaLguY's state machine parser, right?

Solving the problem, even with the suggested extra credit, is easy stuff though.
Let's have a look at Bob Showalter's solution to see just how easy. Here's the
start of the code:

# set to dictionary file to load
DICT = '/usr/share/dict/words'

class Morse

@@words = nil

LETTER = Hash[*%w/
A .- N -.
B -... O ---
C -.-. P .--.
D -.. Q --.-
E . R .-.
F ..-. S ...
G --. T -
H .... U ..-
I .. V ...-
J .--- W .--
K -.- X -..-
L .-.. Y -.--
M -- Z --..
/]

# ...

Here we see some setup work. Bob defines a constant to point at the dictionary
on his system. The comment encourages you to tweak this for your system.

Next we have a the start of a class definition. This class is really just used
as a namespace. No objects are ever constructed from it. Given that, a module
definition might have better represented it's purpose.

The class variable will hold the actual dictionary words, assuming the user
requests that we load it. More on that in a bit.

Finally, we have a wonderful little trick that many solvers picked up on. It's
possible to write some Ruby that will allow you to paste the translation chart
from the quiz directly into your code and actually have that be a meaningful
data structure. Here we see the whole thing converted into a whitespace
delimited Array, which is then splatted into Hash form. Every other element
becomes a key or value, so we end up with Morse code values keyed by the English
letter it translates to. Very clever stuff.

Here's the dictionary loading code:

# ...

# loads dictionary file to limit the words output
def self.load_dictionary(path)
@@words = {}
File.open(path, 'r') do |f|
while word = f.gets
@@words[word.chomp.upcase] = true
end
end
end

# ...

This method creates a Hash to hold the words, reads the indicated file line by
line, peels off line endings, and normalizes word case, filling the Hash as it
reads. Most solutions that included dictionary handling had a chunk of code
very similar to this. This particular version could be simplified a touch by
using File.foreach().

The last method of Bob's class does the heavy lifting:

# ...

# returns list of words starting with prefix that can be made from code
def self.words(code = @code, prefix = '')
results = []
if code == ''
results << prefix if @@words.nil? || @@words.include?(prefix)
else
LETTER.sort.each do |l, p|
if code[0, p.length] == p
results += words(code[p.length,code.length], prefix + l)
end
end
end
results
end

end

# ...

This is a very straightforward recursive decoder. We get two parameters here:
the remaining code (ignore that unused default) and any prefix we should apply
to words generated from that code. You can see that the method defines, fills,
and returns an Array of results. How those results are generated is the
interesting bit.

The if statement branch comes into play when we run out of code to translate.
In that case, the word is added to the results, as long as no dictionary was
loaded or the word is in the loaded dictionary. This means that you can run
this code without using a dictionary to see all options or with a dictionary to
see only likely matches.

The else branch handles all cases where we have some code left. When that
happens, each letter is tried at the start of the code. If it matches, the
method recurses using the code after the matched letter and the existing prefix
plus the new letter. All words generated from the recursive calls are added to
the current result set. This generates all possible combinations.

I should note here that there was some discussion over similar code from Donald
A. Ball Jr. that yielded the words to a block instead of collecting them in an
Array. This is probably a good move for this particular decoder, since there
are a surprising 5,104 possible translations of the simple code provided in the
quiz. Collecting larger translations in an Array might get a bit resource
intensive. Bob's method is easily converted:

# changed to use a block by JEG2
def self.words(code, prefix = '', &block)
if code == ''
block[prefix] if @@words.nil? || @@words.include?(prefix)
else
LETTER.sort.each do |l, p|
if code[0, p.length] == p
results += words(code[p.length,code.length], prefix + l, &block)
end
end
end
end

Note the three simple changes: I added the block as a parameter, I pass
finished words to the block instead of placing them in an Array, and I hand the
block down the stack when we recurse. This removes the need for the results
Array, so I have also trimmed that code.

Getting back to Bob's solution, here is the application code that makes it run:

# ...

Morse.load_dictionary(DICT) if ARGV.delete('-d')
abort "Usage: #{$0} [-d] code [...]" if ARGV.empty?
ARGV.each do |code|
puts "#{code}:"
puts Morse.words(code) # Morse.words(code) { |w| puts w } with the block
end

First we see the optional dictionary flag interpretation. We've already talked
about how the code changes depending on whether or not a word list is loaded.

The next line is a usage statement printed by abort(). We don't see that method
too often in quiz solutions, but it prints a message to STDERR, when provided,
and then terminates the program. Note that the usage statement tells us this
code differs slightly from the quiz interface, taking the code as a command-line
argument instead of reading it from STDIN.

That last iterator just walks the provided codes printing the translations
returned from a call to Morse.words().

---.-- -.....--.-.-... ---- .-.-...-.. .--....--- -.-..--. .-...--..
-----.-..... -.-.----... (.-.-.-.----...-.. -...-.-- --.-.-.-.- -...--.--...
...---.-....--..----.)

Tomorrow we have another easy, though more practical, challenge...
 
B

Bob Showalter

block[prefix] if @@words.nil? || @@words.include?(prefix)

I didn't understand that when I first saw it. I kept looking for
"yield". Now I see that "block[prefix]" is the same as "yield prefix".
Or are there differences?
# returns list of words starting with prefix that can be made from code
def self.words(code = @code, prefix = '')

(ignore that unused default)

Oops. That's a leftover from my original approach that had words as an
instance method instead of a class method.
The next line is a usage statement printed by abort(). We don't see that method
too often in quiz solutions, but it prints a message to STDERR, when provided,
and then terminates the program.

That's an old Perl'er used to "die()". raise() is so brutal for this
kind of thing :~)
 
J

James Edward Gray II

block[prefix] if @@words.nil? || @@words.include?(prefix)

I didn't understand that when I first saw it. I kept looking for
"yield". Now I see that "block[prefix]" is the same as "yield prefix".
Or are there differences?

There are two ways to handle blocks passed to methods. One is to
yield to it as needed. Another is to ask Ruby to wrap it up in a
Proc object for you.

yield isn't ideal in this case because the method recurses and we
need to use the same block for all of those invocations. By asking
for the object we have it to pass along.

Does that make sense?
That's an old Perl'er used to "die()". raise() is so brutal for this
kind of thing :~)

I liked it. I just had to look it up, so I thought I would share. ;)

James Edward Gray II
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,755
Messages
2,569,536
Members
45,013
Latest member
KatriceSwa

Latest Threads

Top