Passing a block to String#gsub in Ruby

Eric Turner
Eric Turner’s Blog
3 min readJan 22, 2017

--

Let’s say you have the following string:

“the quick brown fox jumped over the lazy dog”

and you want to make the following substitutions:

  • “e” with “3”
  • “o” with “0”
  • “l” with “1”

In ruby, the most naive way to do this would be to call String#gsub! three times:

str = “the quick brown fox jumped over the lazy dog”str.gsub!(“e”, “3”)=> “th3 quick brown fox jump3d ov3r th3 lazy dog”str.gsub!(“o”, “0”)=> “th3 quick br0wn f0x jump3d 0v3r th3 lazy d0g”str.gsub!(“l”, “1”)=> “th3 quick br0wn f0x jump3d 0v3r th3 1azy d0g

Or, if you don’t want to mutate the original string, by chaining the non-bang version, String#gsub:

str = “the quick brown fox jumped over the lazy dog”str.gsub(“e”, “3”).gsub(“o”, “0”).gsub(“l”, “1”)=> “th3 quick br0wn f0x jump3d 0v3r th3 1azy d0g”

This approach works, but it has two problems:

  1. We’re hardcoding the replacements in the gsub method; I’d prefer to have a list of the replacements I want to make, and then iterate through that list.
  2. We’re iterating through the string three times.

First, to solve problem #1 we could use a Hash, and then iterate through it with Hash#each:

str = "the quick brown fox jumped over the lazy dog"  mappings = { "e" => "3", "o" => "0", "l" => "1" }  mappings.each do |key, val|     
str.gsub!(key, val)
end
=> "th3 quick br0wn f0x jump3d 0v3r th3 1azy d0g"

I like this better since there’s now less mental overhead involved in changing the list of values I want to replace. Instead of having to look in the code that’s doing the replacements itself, I have one central place I can go to make changes.

This is a good thing, but we’re still iterating through the string three times, which seems inefficient.

It may not be obvious at first glance, but the String#gsub method allows you to specify a hash argument, so you can do the following:

str = "the quick brown fox jumped over the lazy dog"mappings = { "e" => "3","o" => "0", "l" => "1" }str.gsub(/[eol]/, mappings)  => "th3 quick br0wn f0x jump3d 0v3r th3 1azy d0g"

The /[eol]/ regex matches a single occurrence of any of e, o, or l, and passing the mappings hash tells gsub to check the matched value, and if the hash has that value as a key, replace it with the hash value at that key. Now we're only doing a single pass of the string, and we're relying on fast (O(1)) hash lookups instead.

If you want, you could also go one step further to DRY up this code and do something like the following:

str = "the quick brown fox jumped over the lazy dog"mappings = { "e" => "3", "o" => "0", "l" => "1" }mapping_keys = mappings.keys.join("")str.gsub(/[#{mapping_keys}]/, mappings)=> "th3 quick br0wn f0x jump3d 0v3r th3 1azy d0g"

That way the only place you need to make changes is the mappings hash.

str = "the quick brown fox jumped over the lazy dog"str.tr("eol", "301")# => "th3 quick br0wn f0x jump3d 0v3r th3 1azy d0g"

but I digress.

--

--