Getting a Hash Value from a String of Concatenated Keys

Recently, I was tasked with determining if an expected value was present in a hash containing an arbitrary (and unknown) number of nested keys, using a string containing relevant keys to search for. Here’s how I approached the problem and some useful Ruby methods I used along the way.

The Input

The method I was working on would require a few things. First, a hash to be searched. For example:

pokemon = {
  :pikachu => {
    :id => 25,
    :height => 4,
    :types => {
      :one => {
        :name => "electric"
      }
    }
  },
  :kubfu => {
    :id => 891,
    :height => 6,
    :types => {
      :one => {
        :name => "fighting"
      }
    }
  }
}

Second, a string of keys, prepended with the name of the hash:

key_string = "pokemon.pikachu.height"

Finally, an expected value:

expected_value = 4

So in this case, I would need to check if the height value of pikachu within the pokemon hash was equal to 4. In code terms…

if pokemon[:pikachu][:height] == 4
  puts "Success!"
end

Questions

I usually write down questions that need answers when solving problems like this. Here are a few I considered:

Fortunately, Ruby has some useful methods built in that made for a pretty concise solution.

Step 1: Split the String

The first thing to do was make that string into individual keys. I used the String class’ split method (aka String#split) to get an array of substrings.

strings = key_string.split(".") # break apart the string wherever a . appears
=> ["pokemon", "pikachu", "height"]

Step 2: Get Rid of the Hash Name

I didn’t actually need the “pokemon” element of that array since I already had the pokemon variable in scope for the code I was writing. I removed it using the handy Array#drop(n) method, which returns a copy of the original array with the first n elements removed.

keys = strings.drop(1)
# => ["pikachu", "height"]

(If the concatenated string had only contained keys within the given hash, I wouldn’t have needed to do this step.)

First question - answered! ✅

Step 3: Convert the Strings to Symbols

To search the keys in the provided hash (which are symbols), I needed the elements of keys array to be symbols. When I split them up, they were still strings, so I needed a quick way to switch their type. I settled on this:

keys.map(&:to_sym)
# => [:pikachu, :height]

The & in Ruby will convert a method (represented by a symbol - in this case, :to_sym) into a Proc - basically, finding a method with the same name as the symbol. Passing &:to_sym to Ruby’s Enumerable#map resulted in the String#to_sym method being called on all elements of the keys array to produce the output above.

Step 4: Pass the Symbols to the Hash#dig Method

The last step involved looking for the value associated with the particular sequence of keys within the pokemon hash. I briefly became overwhelmed with how to take an arbitrarily long array and pass each item as a key to a hash before I learned about the Hash#dig method.

dig() takes any number of keys as arguments and looks for those keys, in sequence, within the hash. If any of the keys can’t be found, it returns nil. I tried this, expecting it to return true:

pokemon.dig(keys) == 4
# => false

This did not work, though. Why? Passing keys to dig essentially amounted to saying “Find the value for the [:pikachu, :height] key within the pokemon hash.” But there is no single key called [:pikachu, :height] in the pokemon hash. Enter the splat operator (*), which turns an array into arguments.

Doing the following was like saying “find the :pikachu key; then beneath that, find the :height key. Then, check if that value equals 4.”

pokemon.dig(*keys) == 4
# => true

Second and third questions - answered! ✅✅

The Code

What does this look like when not broken down step-by-step? Something like this:

# Inputs
pokemon = {
  :pikachu => {
    :id => 25,
    :height => 4,
    :types => {
      :one => {
        :name => "electric"
      }
    }
  },
  :kubfu => {
    :id => 891,
    :height => 6,
    :types => {
      :one => {
        :name => "fighting"
      }
    }
  }
}
key_string = "pokemon.pikachu.height"
expected_value = 4

# Array of symbols from the key string, excluding "pokemon"
keys = key_string.split(".").drop(1).map(&:to_sym)

if pokemon.dig(*keys) == expected_value
  puts "Success!"
end
# => Success!

Reflections

I’ve been working with Ruby for over 2 years, and I certainly don’t have every method memorized. Some of these methods are tools I reach for regularly, whereas others like drop were fun discoveries while I worked on this problem.

The code snippet on which this post is based was part of a larger feature at work, but it was fun to write and, in the process, use some Ruby methods that made my life much easier.