2008.01.24

Scrambling words in Ruby

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:

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!

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:

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.

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.

% 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?