In Ruby, how do I make a hash from an array?
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?
Related videos on Youtube
Wizzlewott
Updated on September 26, 2020Comments
-
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 about 14 yearsI think you meant
... { |v| [v, f(v)] }
, but this did the trick! -
Jeriko about 14 yearsJust one thing - why's there a
*
next to*arr.collect
? -
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 about 14 yearsSo
Hash[]
takes a list (with an even number of items) in terms ofkey,value,key,value...
? -
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 about 14 years
ArgumentError: odd number of arguments for Hash
:) Thanks -
Telemachus about 14 yearsAfter looking at Jörg's answer and thinking this over some more, note that you can remove both
*
andflatten
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 about 14 yearsI like
zip
as much as the next guy, but since we're already callingmap
, 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 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 about 14 yearsIn Ruby 1.8.7, the ugly
Hash[*key_pairs.flatten]
is simplyHash[key_pairs]
. Much nicer, andrequire 'backports'
if you haven't updated from 1.8.6 yet. -
kaichanvong over 10 yearsThis reads a lot more concisely than using Hash[arr.collect{...}]
-
dmastylo over 9 yearsThis is incredibly slow, check out my post below: stackoverflow.com/a/27962063/1761067
-
Alexander Bird over 9 yearsYou, my friend, are awesome for doing your homework and posting it :)
-
android.weasel over 6 yearsSlightly 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 almost 5 yearsI 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 about 4 yearsThis does a
merge
for every step of the iteration. Merge is O(n), as is the iteration. Thus, this isO(n^2)
while the problem itself is obviously linear. In absolute terms, I just tried this on an array with 100k elements and it took730 seconds
, while the other methods mentioned in this thread took anywhere from0.7
to1.1 seconds
. Yes, that's a slowdown by Factor 700! -
Matt VanEseltine about 3 yearsI'm a little confused at the difference between
arr.map
andarr.collect
considering that.collect
is merely an alias to.map