August 16, 2023
Abbrev: A Hidden Ruby Gem
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
3I 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?
Subscribe to get future posts via email (or grab the RSS feed)