Home » Ruby » In Ruby, how do I make a hash from an array?

In Ruby, how do I make a hash from an array?

Posted by: admin November 30, 2017 Leave a comment

Questions:

I have a simple array:

arr = ["apples", "bananas", "coconuts", "watermelons"]

I also have a function f that will perform an operation on a single string input and return a value. This operation is very expensive, so I would like to memoize the results in the hash.

I know I can make the desired hash with something like this:

h = {}
arr.each { |a| h[a] = f(a) }

What I’d like to do is not have to initialize h, so that I can just write something like this:

h = arr.(???) { |a| a => f(a) }

Can that be done?

Answers:

Say you have a function with a funtastic name: “f”

def f(fruit)
   fruit + "!"
end

arr = ["apples", "bananas", "coconuts", "watermelons"]
h = Hash[ *arr.collect { |v| [ v, f(v) ] }.flatten ]

will give you:

{"watermelons"=>"watermelons!", "bananas"=>"bananas!", "apples"=>"apples!", "coconuts"=>"coconuts!"}

Updated:

As mentioned in the comments, Ruby 1.8.7 introduces a nicer syntax for this:

h = Hash[arr.collect { |v| [v, f(v)] }]

Questions:
Answers:

Did some quick, dirty benchmarks on some of the given answers. (These findings may not be exactly identical with yours based on Ruby version, weird caching, etc. but the general results will be similar.)

arr is a collection of ActiveRecord objects.

Benchmark.measure {
    100000.times {
        Hash[arr.map{ |a| [a.id, a] }]
    }
}

Benchmark @real=0.860651, @cstime=0.0, @cutime=0.0, @stime=0.0, @utime=0.8500000000000005, @total=0.8500000000000005

Benchmark.measure { 
    100000.times {
        h = Hash[arr.collect { |v| [v.id, v] }]
    }
}

Benchmark @real=0.74612, @cstime=0.0, @cutime=0.0, @stime=0.010000000000000009, @utime=0.740000000000002, @total=0.750000000000002

Benchmark.measure {
    100000.times {
        hash = {}
        arr.each { |a| hash[a.id] = a }
    }
}

Benchmark @real=0.627355, @cstime=0.0, @cutime=0.0, @stime=0.010000000000000009, @utime=0.6199999999999974, @total=0.6299999999999975

Benchmark.measure {
    100000.times {
        arr.each_with_object({}) { |v, h| h[v.id] = v }
    }
}

Benchmark @real=1.650568, @cstime=0.0, @cutime=0.0, @stime=0.12999999999999998, @utime=1.51, @total=1.64

In conclusion

Just because Ruby is expressive and dynamic, doesn’t mean you should always go for the prettiest solution. The basic each loop was the fastest in creating a hash.

Questions:
Answers:
h = arr.each_with_object({}) { |v,h| h[v] = f(v) }

Questions:
Answers:

This is what I would probably write:

h = Hash[arr.zip(arr.map(&method(:f)))]

Simple, clear, obvious, declarative. What more could you want?

Questions:
Answers:

I’m doing it like described in this great article http://robots.thoughtbot.com/iteration-as-an-anti-pattern#build-a-hash-from-an-array

array = ["apples", "bananas", "coconuts", "watermelons"]
hash = array.inject({}) { |h,fruit| h.merge(fruit => f(fruit)) }

More info about inject method: http://ruby-doc.org/core-2.0.0/Enumerable.html#method-i-inject

Questions:
Answers:

Another one, slightly clearer IMHO –

Hash[*array.reduce([]) { |memo, fruit| memo << fruit << f(fruit) }]

Using length as f() –

2.1.5 :026 > array = ["apples", "bananas", "coconuts", "watermelons"]
 => ["apples", "bananas", "coconuts", "watermelons"] 
2.1.5 :027 > Hash[*array.reduce([]) { |memo, fruit| memo << fruit << fruit.length }]
 => {"apples"=>6, "bananas"=>7, "coconuts"=>8, "watermelons"=>11} 
2.1.5 :028 >