Ruby Standard Library: The Abbrev Module

Abbrev: A Hidden Ruby Gem

The Abbrev module in the Ruby standard library helps you find out all the possible and unique abbreviations for one or more strings. In this post, we'll learn how it works along with a practical example. We'll also take a peek behind the scenes to see how it's implemented.

4 min read
Update: I realized, only after publishing the post, that people might confuse the wordplay on Gem with a Ruby Gem. Oops. FYI, in the title, I mean the jewel...

Reading Ruby's standard library is a fantastic way I've found to improve my Ruby chops. I've also since learned that the way Japanese developers write Ruby is very different from the Ruby you'll typically see in Rails, or many other popular Ruby gems. It's not that one way is better than the other, but I bet you'll learn a lot by studying both dialects.

Anyway, I digress. Recently, while browsing the standard library classes, I came across the Abbrev module. Although it didn't look that useful at a first glance, I quickly discovered it had a great use case for elegantly handling the user input (thank you, Programming Ruby!).

Let's take a quick look at this module and how you could use it. We'll wrap up this short post by taking a peek at the underlying source code along with a real-world example.

What is Abbrev?

The Abbrev module helps you find out all the possible and unique abbreviations for one or more strings so that there are no duplicate abbreviations.

If you're confused, here's an example:

require 'abbrev'

puts Abbrev.abbrev(['ruby'])

puts Abbrev.abbrev(['ruby', 'rust'])

# Output:

# {"ruby"=>"ruby", "rub"=>"ruby", "ru"=>"ruby", "r"=>"ruby"}
# {"ruby"=>"ruby", "rub"=>"ruby", "rust"=>"rust", "rus"=>"rust"}

Input: A set of words, such as [ruby, rust]

Output: A hash consisting of unambiguous abbreviations for the above string.

💡
Note the resulting hash in the second example omitted ru as a key as it would be confusing to find which word it refers to, ruby or rust.

It also works as an array extension, i.e. it adds an abbrev method to the Array class. This lets you call the abbrev method directly on an array.

irb(main):003:0> ["ruby", "rust"].abbrev
=> {"ruby"=>"ruby", "rub"=>"ruby", "rust"=>"rust", "rus"=>"rust"}

Pretty handy.

Nice. But Why Do I Care?

Let's look at a practical use-case to solidify our understanding.

Imagine you're writing a command-line program which has a set of commands. To make it easy for the users, you also want to allow them to type as few keys as possible when entering the commands.

For example, suppose the program has only activate command to begin with. The users could type act or even a, and your program should work as expected. However, if you want to add an action command, the users shouldn't be able to type act or a or even acti, as that would be ambiguous with activate. But they could type actio and that'd be a valid input for action.

In this case, you can use the Abbrev module to get the non-conflicting abbreviations for your commands.

require 'abbrev'

puts Abbrev.abbrev(['activate', 'action'])

# Output:
# {
#   "activate"=>"activate", "activat"=>"activate", "activa"=>"activate", "activ"=>"activate", 
#   "action"=>"action", "actio"=>"action"
# }

Real-World Example

Here is a practical code example from Programming Ruby that demonstrates this feature.

require 'abbrev'

COMMANDS = %w{ sample send start status stp }.abbrev

while line = gets
  line = line.chomp

  case COMMANDS[line]
  when "sample" then
    puts "Executing sample command"
  when "send" then
    puts "Executing send command"
  when "start" then
    puts "Executing start command"
  when "status" then
    puts "Executing status command"
  when "stp" then
    puts "Executing stp command"
  else
    STDERR.puts "Unknown command: #{line}"
  end
end

# Output
> sa
Executing sample command
> st
Unknown command: st
> sta
Unknown command: sta
> star
Executing start command
If you haven't read Programming Ruby, stop reading and buy a copy. I'll wait. If you want more book recommendations, check out the post A List of Books to Learn Programming with Ruby and Rails.
💡
Update: There's a very interesting discussion on Hacker News regarding this post which I suggest you check out. In short, instead of directly applying the short commands as shown above, it's better to use tab-completion (which still uses the above logic) to complete the intended command, which the user can choose to apply or not.

Nice! How Does It Work?

Here is a simple version of the code implemented in the standard library, in the lib/abbrev.rb directory.

Note: To keep things simple, I've skipped the optional pattern parameter, which can be a string or a regex. It includes only strings that match the pattern or start with the string.

module Abbr
  def Abbr.abbr(words)
    table = {}
    seen = Hash.new(0)

    words.each do |word|
      next if word.empty?
      word.size.downto(1) do |len|
        abbrev = word[0...len]

        case seen[abbrev] += 1
        when 1
          table[abbrev] = word
        when 2  # found duplicate abbreviation
          table.delete(abbrev)
        else  # no need to go further
          break
        end
      end
    end

    words.each do |word|
      table[word] = word
    end

    table
  end
end

By the way, did you notice the downto(limit) method? It calls the given block with each integer value from self down to limit and returns self, which is the integer you call it on. With no block given, it returns an Enumerator.

5.downto(3).each { |i| puts i }

# Output
5
4
3

I didn't know about it until I was reading the source. Have I mentioned already that reading the source (both gems and the standard library) will make you a better developer?


That's a wrap. I hope you liked this article and you learned something new. If you're new to the blog, check out the full archive to see all the posts I've written so far or the favorites page for the most popular articles on this blog.

As always, if you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I look forward to hearing from you.

If you'd like to receive future articles directly in your email, please subscribe to my blog. If you're already a subscriber, thank you.