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.
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.
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.