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

81,784

Solution 1

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)] }]

Solution 2

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.

Solution 3

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

Solution 4

Ruby 2.6.0 enables a shorter syntax by passing a block to the to_h method:

arr.to_h { |a| [a, f(a)] }

Solution 5

This is what I would probably write:

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

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

Share:
81,784

Related videos on Youtube

Wizzlewott
Author by

Wizzlewott

Updated on September 26, 2020

Comments

  • Wizzlewott
    Wizzlewott almost 4 years

    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?

  • Wizzlewott
    Wizzlewott about 14 years
    I think you meant ... { |v| [v, f(v)] }, but this did the trick!
  • Jeriko
    Jeriko about 14 years
    Just one thing - why's there a * next to *arr.collect?
  • Telemachus
    Telemachus about 14 years
    @Jeriko - the splat operator * collects a list into an array or unwinds an array into a list, depending on context. Here it unwinds the array into a list (to be used as the items for the new hash).
  • Jeriko
    Jeriko about 14 years
    So Hash[] takes a list (with an even number of items) in terms of key,value,key,value...?
  • Telemachus
    Telemachus about 14 years
    @Jeriko Yes - if you give Hash[] an odd number of arguments, it blows up. See here for more on the splat operator (which is really very useful in a number of contexts): 4loc.wordpress.com/2009/01/16/the-splat-operator-in-ruby
  • Jeriko
    Jeriko about 14 years
    ArgumentError: odd number of arguments for Hash :) Thanks
  • Telemachus
    Telemachus about 14 years
    After looking at Jörg's answer and thinking this over some more, note that you can remove both * and flatten for a simpler version: h = Hash[ arr.collect { |v| [ v, f(v) ] } ]. I'm not sure if there's a gotcha I'm not seeing, however.
  • Telemachus
    Telemachus about 14 years
    I like zip as much as the next guy, but since we're already calling map, why not leave it at this? h = Hash[ arr.map { |v| [ v, f(v) ] } ] Is there an advantage to your version that I'm not seeing?
  • Jörg W Mittag
    Jörg W Mittag about 14 years
    @Telemachus: With all the Haskell code I've been reading, I just got used to point-free programming, that's all.
  • Marc-André Lafortune
    Marc-André Lafortune about 14 years
    In Ruby 1.8.7, the ugly Hash[*key_pairs.flatten] is simply Hash[key_pairs]. Much nicer, and require 'backports' if you haven't updated from 1.8.6 yet.
  • kaichanvong
    kaichanvong over 10 years
    This reads a lot more concisely than using Hash[arr.collect{...}]
  • dmastylo
    dmastylo over 9 years
    This is incredibly slow, check out my post below: stackoverflow.com/a/27962063/1761067
  • Alexander Bird
    Alexander Bird over 9 years
    You, my friend, are awesome for doing your homework and posting it :)
  • android.weasel
    android.weasel over 6 years
    Slightly faster to use a manually incremented loop variable: I don't have your dataset - I just cooked a trivial object with an @id accessor and more-or-less matched your numbers - but direct iteration shaved a couple of % off. Stylistically, I prefer {}.tap { |h| .... } to assigning a hash, because I like encapsulated chunks.
  • ruud
    ruud almost 5 years
    I benchmarked the two versions: the use of merge doubles the execution time. The above inject version is a comparabe to the collect version of microspino
  • Matthias Winkelmann
    Matthias Winkelmann about 4 years
    This does a merge for every step of the iteration. Merge is O(n), as is the iteration. Thus, this is O(n^2) while the problem itself is obviously linear. In absolute terms, I just tried this on an array with 100k elements and it took 730 seconds, while the other methods mentioned in this thread took anywhere from 0.7 to 1.1 seconds. Yes, that's a slowdown by Factor 700!
  • Matt VanEseltine
    Matt VanEseltine about 3 years
    I'm a little confused at the difference between arr.map and arr.collect considering that .collect is merely an alias to .map