Keeping it Small and Simple

2008.01.24

Scrambling words in Ruby

Filed under: Ruby Programming — Tags: , , , — Lorenzo E. Danielsson @ 22:50

Somebody needed a program that takes an input string and scrambles the letters in each of the words. Capitalization and punctuation should be intact. I don’t normally solve people’s exercises for them, but I did decide to write something of my own. I get severe allergic attacks from typing too much, so instead of Java, I chose Ruby as an implementation language.

Scramble a string

The first part of the problem is to scramble a string. The following seems to work fairly well, and should be easy to understand:


1 def scramble(str)
2   s = str.split(//).sort_by { rand }.join()
3   s =~ /[A-Z]/ && s =~ /[a-z]/ ? s.capitalize : s
4 end
5
6 puts scramble("Hello world")

The first line in the method does exactly what you would think it does: splits a string into an array of characters, “sorts” them randomly (whatever that means), and joins the characters back into a string.

The second line maybe deserves a comment. I use a very naive (not to mention stupid) way of deciding whether or not to capitalize the string. If I can find both upper-case and lower-case letters then I capitalize, otherwise I don’t. It works well enough for normal English, but if you pass some camelCased strings then it probably won’t do what you expect. Then again, that wasn’t in my friends specification..

One thing before we continue. Scrambling looks like an interesting candidate for the String class, don’t you think? Okay, let’s fix that right away!


1 class String
2   def scramble
3     s = self.split(//).sort_by { rand }.join()
4     s =~ /[A-Z]/ && s =~ /[a-z]/ ? s.capitalize : s
5   end
6 end
7
8 puts "Hello world".scramble

Now, that looks so much better, wouldn’t you agree? Now we can go on and figure out how to scramble individual words instead of strings.

Scrambling words

Just as a reminder: punctuation characters are supposed to be left in place. So, if I have a string like Hello, world! then the comma and the exclamation mark (or the bang, if you have been using UNIX for too long) need to be left in place.

So the procedure will be to split the string on white-space. That way we get an array of words, possibly with punctuation characters attached. We then break out the actual word and scramble it. The following would do just that:


 1 def scramble_words
 2   ret = []
 3   self.split(/\s+/).each { |nws|
 4     nws.scan(/^(\W*)(\w*)(\W*)$/) { |pre, word, post|
 5       ret << pre + word.scramble + post
 6     }
 7   }
 8
 9   ret.join " "
10 end
11

Of course, this method should be added to the String class as well. You can find the full code below. The first thing we do is split on one or more white-spaces. Since I join the array of words at the end of the method separated by a single string this means that you lose multiple consecutive white-spaces in the string. This may be a problem. The good news is that it is trivial to fix. Can you figure it out? Good, I knew you would.

Once I have split out the “words”, I use String#scan to separate any punctuation characters from the word itself. You may wonder why I use ^(\W*)(\w*)(\W*)$/ as the regex. It would appear to be better to use ^(\W*)(\w+)(\W*)$/. Well, the fact is that you could end up having one or more lone punctuation characters, which means the match would fail with a \w+.

Now we are ready to put it all together.

The scramble-words program

The String class, extended with the scramble and scramble_words methods follows. There is also a little loop there that lets the user enter strings, which get passed to scramble_words.


 1 #! /usr/bin/ruby
 2
 3 # Scramble words.
 4
 5 class String
 6   def scramble
 7     s = self.split(//).sort_by { rand }.join()
 8     s =~ /[A-Z]/ && s =~ /[a-z]/ ? s.capitalize : s
 9   end
10
11   def scramble_words
12     ret = []
13     self.split(/\s+/).each { |nws|
14       nws.scan(/^(\W*)(\w*)(\W*)$/) { |pre, word, post|
15         ret << pre + word.scramble + post
16       }
17     }
18
19     ret.join " "
20   end
21 end
22
23 loop do
24   print "Enter something: "
25   str = gets.chomp
26   exit if str.empty?
27   puts str.scramble_words
28 end

Okay. So how does this fare?


% ruby scramble.rb
Enter something: "Hello, world!", said the C programmer.
"Hello, dlorw!", adis the C mreorrgapm.

First test worked okay. Let’s try something else.


Enter something: Hey! Hey! My, my, Rock'n'roll will never die!
Hey! Eyh! My, my, llwi nvree die!

Uh-oh! Seems we lost something. If you think about it, it’s obvious why. I don’t really expect my friend would be parsing words like Rock’n’roll, but words with an apostrophe are likely: I’m, I’ll, let’s, he’s, don’t and so on. Considering the frequency of this character you could fix this by changing the regex in line 14 to look like this:


/^(\W*)([\w|’]*)(\W*)$/

That fixes words with an apostrophe, but for some reason capitalization on such words gets screwed. I haven’t looked into it yet. Feel free to fix it as an exercise. If you’ve solved that and need more, figure out how scrambling of words containing apostrophe should behave. Should the position of the apostrophe be maintained? If so, the program currently does not behave well. So go on and fix it! 😉

If we disregard a few warts here and there the program does mostly what it’s supposed to. Or at least it appears so. I cannot say I have given it any extensive amount of testing.

But wait! We’re being a bit rude by only testing it on ruby 1.8. Let’s try it out on a few other implementations of Ruby (the language).

Does it work with ruby 1.9?

Ruby 1.9 works fine, but the program goes to gets without displaying the prompt. I’m not sure why, but I’ve seen it before. The fix is simple. You just need to flush stdout as shown below.


loop do
  print "Enter something: "
  $stdout.flush
  str = gets.chomp
  exit if str.empty?
  puts str.scramble_words
end

Does it work with JRuby?

What a silly question. Of course it does. What did you expect? 😉

Does it work with Rubinius?

Well, we won’t know unless we try it, will we?


% rbx scramble.rb
Enter something: An exception has occurred:
undefined local variable or method `gets' for main (NameError)

Backtrace:
Object#gets (method_missing_cv) at kernel/core/object.rb:117
main.__script__ at ./scramble.rb:26
CompiledMethod#as_script at kernel/bootstrap/primitives.rb:35
Compile.single_load at kernel/core/compile.rb:204
Compile.unified_load {} at kernel/core/compile.rb:124
Array#each at kernel/core/array.rb:557
Compile.unified_load at kernel/core/compile.rb:107
Kernel(Object)#load at kernel/core/compile.rb:329
main.__script__ at kernel/loader.rb:171

Obviously not. Nothing we can do here. Go home, folks. No, but wait! Those error messages are actually there for our benefit, so why not read them? This looks interesting: undefined local variable or method `gets' for main (NameError). If Rubinius doesn’t know of Kernel.gets, what would happen if we were a little more precise?


loop do
  print "Enter something: "
  str = $stdin.gets.chomp
  exit if str.empty?
  puts str.scramble_words
end


% rbx scramble.rb
Enter something: "Hello, world!", said the C programmer.
"Llhoe, lwrod!", said teh C arpmergmor.

Sweet!


% rbx -v
rubinius 0.8.0 (ruby 1.8.6 compatible) (r) (01/23/2008) [i686-pc-linux-gnu]

Conclusion

I need coffee.


Update 2008.01.27: with Rubinius checked out and compiled this morning, $stdin.gets is no longer necessary. I guess Kernel.gets is working now.

Advertisements

Leave a Comment »

No comments yet.

RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Create a free website or blog at WordPress.com.

%d bloggers like this: