Getting a Hash Value from a String of Concatenated Keys
Tags: ruby, tutorial, beginners
Published: 2020-11-23 03:55:55

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:

```ruby
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:

```ruby
key_string = "pokemon.pikachu.height"
```

Finally, an expected value:

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

```ruby
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`](https://ruby-doc.org/core-2.7.0/String.html#method-i-split) method (aka `String#split`) to get an array of substrings.

```ruby
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)`](https://ruby-doc.org/core-2.7.0/Array.html#method-i-drop) method, which returns a copy of the original array with the first `n` elements removed.

```ruby
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:

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

The [`&`](https://www.brianstorti.com/understanding-ruby-idiom-map-with-symbol/) in Ruby will convert a method (represented by a symbol - in this case, `:to_sym`) into a [`Proc`](https://ruby-doc.org/core-2.7.0/Proc.html) - basically, finding a method with the same name as the symbol. Passing `&:to_sym` to Ruby's [`Enumerable#map`](https://ruby-doc.org/core-2.7.0/Enumerable.html#method-i-map) resulted in the [`String#to_sym`](https://ruby-doc.org/core-2.7.0/String.html#method-i-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`](https://ruby-doc.org/core-2.7.0/Hash.html#method-i-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`:

```ruby
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](https://ruby-doc.org/core-2.7.0/doc/syntax/calling_methods_rdoc.html#label-Array+to+Arguments+Conversion).

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

```ruby
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:

```ruby
# 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.