DEV Community

Amy Henning
Amy Henning

Posted on • Edited on • Originally published at amyhenning.codes

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"
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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

key_string = "pokemon.pikachu.height"
Enter fullscreen mode Exit fullscreen mode

Finally, an expected value:

expected_value = 4
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Questions

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

  • What should I do with the prepended hash name, knowing I'd be passed a pokemon hash?
  • How could I make this flexible enough to handle an abitrary number of keys in the string? I.e. someone could pass in a string like "pokemon.pikachu.types.one.name" and this method would need to be able to handle that as well as "pokemon.pikachu.id".
  • How will I turn the keys in this string into a sensible way to search through a hash, especially knowing that the hash could have any number of keys and layers of nested keys?

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"]
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

(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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)