Ruby has a very beatutiful expressiveness and offers a great standard library. This helps a lot when dealing with data structures. Especially when you have to convert, merge or combine Arrays and Hashes. This post is showing just a little example out of the big area of possibilities.
The target
One thing we likely stumble upon when creating data structures is the task to create a Hash
from two Arrays where one Array
holds the keys
and the other one the values
of the resulting Hash
. Based upon our example Arrays, one result could look like this:
{
"Verena" => "reading",
"Kiana" => "swimming",
"Naya" => "painting"
}
Or even:
{
"Verena" => "rock",
"Kiana" => [
[0] "folk",
[1] "disco"
],
"Naya" => "metal"
}
The following sections will describe how we can create these data structures.
The data
We have three simple Arrays with the following structure:
names = %w(Verena Kiana Naya)
[
[0] "Verena",
[1] "Kiana",
[2] "Naya"
]
hobbies = %w(reading swimming painting)
[
[0] "reading",
[1] "swimming",
[2] "painting"
]
music = ['rock', %w(folk disco), 'metal']
[
[0] "rock",
[1] [
[0] "folk",
[1] "disco"
],
[2] "metal"
]
These Arrays are the base for all subsequent steps to create our Hash
.
Combine data with the Array#zip
method
From the documentation
Converts any arguments to arrays, then merges elements of self with corresponding elements from each argument. …
So when using our data structures, we can do something like this:
names.zip(hobbies)
[
[0] [
[0] "Verena",
[1] "reading"
],
[1] [
[0] "Kiana",
[1] "swimming"
],
[2] [
[0] "Naya",
[1] "painting"
]
]
As you can see, Array#zip
is creating a new Array from our two Arrays name
and hobbies
. The resulting Array consists of pairs from name[0] -> hobbies[0]
, name[1] -> hobbies[1]
and so on. If no corresponding value can be found, nil
will be inserted in the resulting Array.
The zip
method is very helpful for creating data structures as we will see when we will create a Hash.
A flat Array from a nested Array with Array#zip
From the documentation
Returns a new array that is a one-dimensional flattening of self (recursively).
The resulting Array of Arrays created with the Array#zip
method is not yet in the correct form to be able to throw it into our resulting Hash
. We have to convert it into a one dimensional Array
because later we will create the key
- value
pairs like this:
key0 = Array[0]
value1 = Array[1]
key1 = Array[2]
value1 = Array[3]
I think you got the idea. Array#flatten
will help us to get the job done. It is as simple as this:
names.zip(hobbies).flatten
[
[0] "Verena",
[1] "reading",
[2] "Kiana",
[3] "swimming",
[4] "Naya",
[5] "painting"
]
That was easy. Now let’s see how we create the final Hash
in the next section.
Reaching the target: creating the Hash
from Arrays
We are nearly done. In the last section we saw, how to create a flattened Array. This Array has the correct form to throw it into our Hash. So let’s do it:
Hash[names.zip(hobbies).flatten]
(irb):47: warning: wrong element type String at 0 (expected array)
(irb):47: warning: ignoring wrong elements is deprecated, remove them explicitly
(irb):47: warning: this causes ArgumentError in the next release
(irb):47: warning: wrong element type String at 1 (expected array)
Oh WTF? That is not the expected result. Why is this?
The problem is, that a Hash expects to receive a list of arguments. And here comes the splat
operator *
into play. It is able to convert a list into a group of parameters and is able to fill an Array with a group of parameters. Following we look at some examples.
Convert a Hash into an Array
hash = *{'Andy' => 'Daddy'}
[
[0] [
[0] "Andy",
[1] "Daddy"
]
]
Convert a (flat) Array into a Hash
Hash[*n.flatten]
{
"Andy" => "Daddy"
}
Interesting is to examing the result with the class
method:
hash = {'Andy' => 'Daddy'}.class
=> Hash
hash = *{'Andy' => 'Daddy'}.class
=> [Hash]
As you can see, the second one is a Hash
in a list (Array).
You can also do other cool stuff with the splat operator. Please have a look at the 4loc Blog for more details.
But now: the final Hash
After we understood why the error occured and know how to circumvent it, we are finally able to create our Hash
:
Hash[*names.zip(hobbies).flatten]
{
"Verena" => "reading",
"Kiana" => "swimming",
"Naya" => "painting"
}
Woot! That’s the result we expected. Cool. But what if the hobbies
data structure would have nested Arrays? Let’s see what happens, when we use the music
Array instead of the hobbies
Array:
Hash[*names.zip(music).flatten]
ArgumentError: odd number of arguments for Hash
from (irb):79:in `[]'
from (irb):79
from /Users/andwen/.rvm/rubies/ruby-2.0.0-p353/bin/irb:12:in `<main>'
Hm - that did not work. Ruby does not know what to do with the not flat
data structure of music
. We need to create the Hash a bit differently:
result = {}
Hash[names.zip(music).each {|a,b| result[a] = b }]
{
"Verena" => "rock",
"Kiana" => [
[0] "folk",
[1] "disco"
],
"Naya" => "metal"
}
That did the trick. Please note that this is working for an Array one level deep nested. I leave it to you to also find a solution for n-level deep nested Arrays :-) .
Further info for Array and Hash
Dealing with Arrays and Hashes is daily business for developers. That for sure also counts for Rubyists. You should definitely dive into the documentation for the Array and Hash Classes included in the Ruby standard library.
Conclusion
In this post we examined how to convert two Arrays with simple data structures in a Hash with a key - value data structure
. This is very helpful when dealing with data and we have seen, that Ruby offers good solutions to handle these tasks.
How are you creating similar data structures? I am curious and would love to receive a message from you … simply send it to andy@nms.de.
##UPDATE
Thanks to Nathan Wallace who pointed out that you can do this since Ruby 2.1
names.zip(music).to_h
{
"Verena" => "rock",
"Kiana" => [
[0] "folk",
[1] "disco"
],
"Naya" => "metal"
}