[ruby] What is the best way to convert an array to a hash in Ruby

Summary & TL;DR:

This answer hopes to be a comprehensive wrap-up of information from other answers.

The very short version, given the data from the question plus a couple extras:

flat_array   = [  apple, 1,   banana, 2  ] # count=4
nested_array = [ [apple, 1], [banana, 2] ] # count=2 of count=2 k,v arrays
incomplete_f = [  apple, 1,   banana     ] # count=3 - missing last value
incomplete_n = [ [apple, 1], [banana   ] ] # count=2 of either k or k,v arrays


# there's one option for flat_array:
h1  = Hash[*flat_array]                     # => {apple=>1, banana=>2}

# two options for nested_array:
h2a = nested_array.to_h # since ruby 2.1.0    => {apple=>1, banana=>2}
h2b = Hash[nested_array]                    # => {apple=>1, banana=>2}

# ok if *only* the last value is missing:
h3  = Hash[incomplete_f.each_slice(2).to_a] # => {apple=>1, banana=>nil}
# always ok for k without v in nested array:
h4  = Hash[incomplete_n] # or .to_h           => {apple=>1, banana=>nil}

# as one might expect:
h1 == h2a # => true
h1 == h2b # => true
h1 == h3  # => false
h3 == h4  # => true

Discussion and details follow.


Setup: variables

In order to show the data we'll be using up front, I'll create some variables to represent various possibilities for the data. They fit into the following categories:

Based on what was directly in the question, as a1 and a2:

(Note: I presume that apple and banana were meant to represent variables. As others have done, I'll be using strings from here on so that input and results can match.)

a1 = [  'apple', 1 ,  'banana', 2  ] # flat input
a2 = [ ['apple', 1], ['banana', 2] ] # key/value paired input

Multi-value keys and/or values, as a3:

In some other answers, another possibility was presented (which I expand on here) – keys and/or values may be arrays on their own:

a3 = [ [ 'apple',                   1   ],
       [ 'banana',                  2   ],
       [ ['orange','seedless'],     3   ],
       [ 'pear',                 [4, 5] ],
     ]

Unbalanced array, as a4:

For good measure, I thought I'd add one for a case where we might have an incomplete input:

a4 = [ [ 'apple',                   1],
       [ 'banana',                  2],
       [ ['orange','seedless'],     3],
       [ 'durian'                    ], # a spiky fruit pricks us: no value!
     ]

Now, to work:

Starting with an initially-flat array, a1:

Some have suggested using #to_h (which showed up in Ruby 2.1.0, and can be backported to earlier versions). For an initially-flat array, this doesn't work:

a1.to_h   # => TypeError: wrong element type String at 0 (expected array)

Using Hash::[] combined with the splat operator does:

Hash[*a1] # => {"apple"=>1, "banana"=>2}

So that's the solution for the simple case represented by a1.

With an array of key/value pair arrays, a2:

With an array of [key,value] type arrays, there are two ways to go.

First, Hash::[] still works (as it did with *a1):

Hash[a2] # => {"apple"=>1, "banana"=>2}

And then also #to_h works now:

a2.to_h  # => {"apple"=>1, "banana"=>2}

So, two easy answers for the simple nested array case.

This remains true even with sub-arrays as keys or values, as with a3:

Hash[a3] # => {"apple"=>1, "banana"=>2, ["orange", "seedless"]=>3, "pear"=>[4, 5]} 
a3.to_h  # => {"apple"=>1, "banana"=>2, ["orange", "seedless"]=>3, "pear"=>[4, 5]}

But durians have spikes (anomalous structures give problems):

If we've gotten input data that's not balanced, we'll run into problems with #to_h:

a4.to_h  # => ArgumentError: wrong array length at 3 (expected 2, was 1)

But Hash::[] still works, just setting nil as the value for durian (and any other array element in a4 that's just a 1-value array):

Hash[a4] # => {"apple"=>1, "banana"=>2, ["orange", "seedless"]=>3, "durian"=>nil}

Flattening - using new variables a5 and a6

A few other answers mentioned flatten, with or without a 1 argument, so let's create some new variables:

a5 = a4.flatten
# => ["apple", 1, "banana", 2,  "orange", "seedless" , 3, "durian"] 
a6 = a4.flatten(1)
# => ["apple", 1, "banana", 2, ["orange", "seedless"], 3, "durian"] 

I chose to use a4 as the base data because of the balance problem we had, which showed up with a4.to_h. I figure calling flatten might be one approach someone might use to try to solve that, which might look like the following.

flatten without arguments (a5):

Hash[*a5]       # => {"apple"=>1, "banana"=>2, "orange"=>"seedless", 3=>"durian"}
# (This is the same as calling `Hash[*a4.flatten]`.)

At a naïve glance, this appears to work – but it got us off on the wrong foot with the seedless oranges, thus also making 3 a key and durian a value.

And this, as with a1, just doesn't work:

a5.to_h # => TypeError: wrong element type String at 0 (expected array)

So a4.flatten isn't useful to us, we'd just want to use Hash[a4]

The flatten(1) case (a6):

But what about only partially flattening? It's worth noting that calling Hash::[] using splat on the partially-flattened array (a6) is not the same as calling Hash[a4]:

Hash[*a6] # => ArgumentError: odd number of arguments for Hash

Pre-flattened array, still nested (alternate way of getting a6):

But what if this was how we'd gotten the array in the first place? (That is, comparably to a1, it was our input data - just this time some of the data can be arrays or other objects.) We've seen that Hash[*a6] doesn't work, but what if we still wanted to get the behavior where the last element (important! see below) acted as a key for a nil value?

In such a situation, there's still a way to do this, using Enumerable#each_slice to get ourselves back to key/value pairs as elements in the outer array:

a7 = a6.each_slice(2).to_a
# => [["apple", 1], ["banana", 2], [["orange", "seedless"], 3], ["durian"]] 

Note that this ends up getting us a new array that isn't "identical" to a4, but does have the same values:

a4.equal?(a7) # => false
a4 == a7      # => true

And thus we can again use Hash::[]:

Hash[a7] # => {"apple"=>1, "banana"=>2, ["orange", "seedless"]=>3, "durian"=>nil}
# or Hash[a6.each_slice(2).to_a]

But there's a problem!

It's important to note that the each_slice(2) solution only gets things back to sanity if the last key was the one missing a value. If we later added an extra key/value pair:

a4_plus = a4.dup # just to have a new-but-related variable name
a4_plus.push(['lychee', 4])
# => [["apple",                1],
#     ["banana",               2],
#     [["orange", "seedless"], 3], # multi-value key
#     ["durian"],              # missing value
#     ["lychee", 4]]           # new well-formed item

a6_plus = a4_plus.flatten(1)
# => ["apple", 1, "banana", 2, ["orange", "seedless"], 3, "durian", "lychee", 4]

a7_plus = a6_plus.each_slice(2).to_a
# => [["apple",                1],
#     ["banana",               2],
#     [["orange", "seedless"], 3], # so far so good
#     ["durian",               "lychee"], # oops! key became value!
#     [4]]                     # and we still have a key without a value

a4_plus == a7_plus # => false, unlike a4 == a7

And the two hashes we'd get from this are different in important ways:

ap Hash[a4_plus] # prints:
{
                     "apple" => 1,
                    "banana" => 2,
    [ "orange", "seedless" ] => 3,
                    "durian" => nil, # correct
                    "lychee" => 4    # correct
}

ap Hash[a7_plus] # prints:
{
                     "apple" => 1,
                    "banana" => 2,
    [ "orange", "seedless" ] => 3,
                    "durian" => "lychee", # incorrect
                           4 => nil       # incorrect
}

(Note: I'm using awesome_print's ap just to make it easier to show the structure here; there's no conceptual requirement for this.)

So the each_slice solution to an unbalanced flat input only works if the unbalanced bit is at the very end.


Take-aways:

  1. Whenever possible, set up input to these things as [key, value] pairs (a sub-array for each item in the outer array).
  2. When you can indeed do that, either #to_h or Hash::[] will both work.
  3. If you're unable to, Hash::[] combined with the splat (*) will work, so long as inputs are balanced.
  4. With an unbalanced and flat array as input, the only way this will work at all reasonably is if the last value item is the only one that's missing.

Side-note: I'm posting this answer because I feel there's value to be added – some of the existing answers have incorrect information, and none (that I read) gave as complete an answer as I'm endeavoring to do here. I hope that it's helpful. I nevertheless give thanks to those who came before me, several of whom provided inspiration for portions of this answer.

Examples related to ruby

Uninitialized Constant MessagesController Embed ruby within URL : Middleman Blog Titlecase all entries into a form_for text field Ruby - ignore "exit" in code Empty brackets '[]' appearing when using .where find_spec_for_exe': can't find gem bundler (>= 0.a) (Gem::GemNotFoundException) How to update Ruby Version 2.0.0 to the latest version in Mac OSX Yosemite? How to fix "Your Ruby version is 2.3.0, but your Gemfile specified 2.2.5" while server starting Is the server running on host "localhost" (::1) and accepting TCP/IP connections on port 5432? How to update Ruby with Homebrew?

Examples related to arrays

PHP array value passes to next row Use NSInteger as array index How do I show a message in the foreach loop? Objects are not valid as a React child. If you meant to render a collection of children, use an array instead Iterating over arrays in Python 3 Best way to "push" into C# array Sort Array of object by object field in Angular 6 Checking for duplicate strings in JavaScript array what does numpy ndarray shape do? How to round a numpy array?

Examples related to hashmap

How to split a string in two and store it in a field Printing a java map Map<String, Object> - How? Hashmap with Streams in Java 8 Streams to collect value of Map How to convert String into Hashmap in java Convert object array to hash map, indexed by an attribute value of the Object HashMap - getting First Key value The type java.util.Map$Entry cannot be resolved. It is indirectly referenced from required .class files Sort Go map values by keys Print all key/value pairs in a Java ConcurrentHashMap creating Hashmap from a JSON String