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:
- 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"]
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.
Top comments (0)