20

I'm wondering if there is a more canonical way to do this in ruby 1.9

I have an array with a bunch of objects and I want to group them into a Hash using a property of each object in the array.

Very simplified example:

> sh = {}
 => {} 
> aers = %w(a b c d ab bc de abc)
 => ["a", "b", "c", "d", "ab", "bc", "de", "abc"] 
> aers.each do |aer|
>     sh[aer.size] = [] if sh[aer.size].nil?
>     sh[aer.size] << aer
>   end
=> ["a", "b", "c", "d", "ab", "bc", "de", "abc"] 
> sh
 => {1=>["a", "b", "c", "d"], 2=>["ab", "bc", "de"], 3=>["abc"]} 

I tried this, but its output is wrong (as you can see):

 sh = Hash.new([])
 => {} 
> aers.each do |aer|
>     sh[aer.size] << aer
>   end
 => ["a", "b", "c", "d", "ab", "bc", "de", "abc"] 
> sh
 => {} 
Phrogz
  • 296,393
  • 112
  • 651
  • 745
user141146
  • 3,285
  • 7
  • 38
  • 54
  • 2
    And the reason your code isn't working as expected is explained here: http://stackoverflow.com/questions/2698460/strange-ruby-behavior-when-using-hash-new and here: http://stackoverflow.com/questions/2552579/ruby-method-array-not-updating-the-array-in-hash (quite a common pitfall in Ruby). – Mladen Jablanović Jan 14 '11 at 15:23

3 Answers3

43

Ruby has anticipated your need, and has got you covered with Enumerable#group_by:

irb(main):001:0> aers = %w(a b c d ab bc de abc)
#=> ["a", "b", "c", "d", "ab", "bc", "de", "abc"]

irb(main):002:0> aers.group_by{ |s| s.size }
#=> {1=>["a", "b", "c", "d"], 2=>["ab", "bc", "de"], 3=>["abc"]}

In Ruby 1.9, you can make this even shorter with:

irb(main):003:0> aers.group_by(&:size)
#=> {1=>["a", "b", "c", "d"], 2=>["ab", "bc", "de"], 3=>["abc"]}
Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • 3
    This is why Ruby is so cool; It knows what we want even before we do. – the Tin Man Jan 14 '11 at 23:42
  • Please not that in 1.8 symbol to proc is much slower than just using the block. http://confreaks.net/videos/427-rubyconf2010-zomg-why-is-this-code-so-slow check 31:18. In 1.9 they are not differences in performance. – Claudio Acciaresi Jan 15 '11 at 17:46
3

Phrogz is correct, group_by is there for the taking. Your code contains one of ruby's gotcha's.

aers = %w(a b c d ab bc de abc)
sh = Hash.new([]) # returns the _same_ array everytime the key is not found. 
# sh = Hash.new{|h,v| h[v] = []}  # This one works
p sh, sh.default

aers.each do |aer|
  sh[aer.size] << aer  #modifies the default [] every time
end
p sh, sh.default
p sh[5]

Output

{}
[]
{}
["a", "b", "c", "d", "ab", "bc", "de", "abc"]
["a", "b", "c", "d", "ab", "bc", "de", "abc"]
steenslag
  • 79,051
  • 16
  • 138
  • 171
1

You can also accomplish this by chaining methods... which might only be of academic interest for this problem, but is still a good technique to be familiar with.

irb(main):017:0> sh = {}
=> {}
irb(main):018:0> aers.collect{|k| k.size}.uniq!.each{|k| sh[k] = aers.select{|j| j.size == k}}
=> [1, 2, 3]
irb(main):019:0> sh
=> {1=>["a", "b", "c", "d"], 2=>["ab", "bc", "de"], 3=>["abc"]}
irb(main):020:0> 
philosodad
  • 1,808
  • 14
  • 24