Convert array of 2-element arrays into a hash, where duplicate keys append additional values

29,807

Solution 1

Using functional baby steps:

irb:01.0> array = [[:a,:b],[:a,:c],[:c,:b]]
#=> [[:a, :b], [:a, :c], [:c, :b]]

irb:02.0> array.group_by(&:first)
#=> {:a=>[[:a, :b], [:a, :c]], :c=>[[:c, :b]]}

irb:03.0> array.group_by(&:first).map{ |k,a| [k,a.map(&:last)] }
#=> [[:a, [:b, :c]], [:c, [:b]]]

irb:04.0> Hash[ array.group_by(&:first).map{ |k,a| [k,a.map(&:last)] } ]
#=> {:a=>[:b, :c], :c=>[:b]}

Using imperative style programming:

irb:10.0> h = Hash.new{ |h,k| h[k]=[] }
#=> {}

irb:11.0> array.each{ |k,v| h[k] << v }
#=> [[:a, :b], [:a, :c], [:c, :b]]

irb:12.0> h
#=> {:a=>[:b, :c], :c=>[:b]}

As an imperative one-liner:

irb:13.0> h = Hash.new{ |h,k| h[k]=[] }.tap{ |h| array.each{ |k,v| h[k] << v } }
#=> {:a=>[:b, :c], :c=>[:b]}

Or using everyone's favorite inject:

irb:14.0> array.inject(Hash.new{ |h,k| h[k]=[] }){ |h,(k,v)| h[k] << v; h }
#=> {:a=>[:b, :c], :c=>[:b]}

If you really want to have single values not collided as an array, you can either un-array them as a post-processing step, or use a different hash accumulation strategy that only creates an array upon collision. Alternatively, wrap your head around this:

irb:17.0> hashes = array.map{ |pair| Hash[*pair] } # merge many mini hashes
#=> [{:a=>:b}, {:a=>:c}, {:c=>:b}]

irb:18.0> hashes.inject{ |h1,h2| h1.merge(h2){ |*a| a[1,2] } }
#=> {:a=>[:b, :c], :c=>:b}

Solution 2

EDIT: In Ruby 2.1+, you can use Array#to_h

pry(main)> [[:a,:b],[:a,:c],[:c,:b]].to_h
=> {:a=>:c, :c=>:b}

END EDIT

The public [] method on the Hash class accepts a key-value pair array and returns a hash with the first element of the array as key and the second as value.

The last value in the key-value pair will be the actual value when there are key duplicates.

Hash[[[:a,:b],[:a,:c],[:c,:b]]]
    => {:a=>:c, :c=>:b}

This syntax is valid in 1.9.3+ ; I'm not sure about earlier Ruby versions (it's not valid in 1.8.7)

ref: http://www.ruby-doc.org/core-2.1.0/Hash.html#method-c-5B-5D

Another interesting way of doing it would be using the inject method: (obviously the method above is more succinct and recommended for this specific problem)

[ [:a, :b], [:a, :c], [:c, :b] ].inject({}) { |memo, obj| 
   memo[obj.first] = obj.last
   memo 
}

=> {:a=>:c, :c=>:b}

inject iterates over the enumerable, your array in this case, starting with the injected parameter, in this case the empty hash {}.

For each object in the enumerable, the block is called with the variables memo and obj:

  • obj is the current object in the array

  • memo is the value that has been returned by your block's last iteration (for the first iteration, it's what you inject)

Solution 3

This can be done fairly succinctly using each_with_object.

array.each_with_object({}) { |(k, v), h| h[k] = (h[k] || []) + [v] }

Demonstrating in irb:

irb(main):002:0> array = [[:a,:b],[:a,:c],[:c,:b]]
=> [[:a, :b], [:a, :c], [:c, :b]]
irb(main):003:0> array.each_with_object({}) { |(k, v), h| h[k] = (h[k] || []) + [v] }
=> {:a=>[:b, :c], :c=>[:b]}

Solution 4

This kind of operations is very common in our project, so we added to_group_h to Enumerable. We can use it like:

[[:x, 1], [:x, 2], [:y, 3]].to_h
# => { x: 2, y: 3 }

[[:x, 1], [:x, 2], [:y, 3]].to_group_h
# => { x: [1, 2], y: [3] }

The following is the implementation of Enumerable#to_group_h:

module Enumerable
  if method_defined?(:to_group_h)
    warn 'Enumerable#to_group_h is defined'
  else
    def to_group_h
      hash = {}
      each do |key, value|
        hash[key] ||= []
        hash[key] << value
      end
      return hash
    end
  end
end
Share:
29,807

Related videos on Youtube

smallsense
Author by

smallsense

Updated on July 09, 2022

Comments

  • smallsense
    smallsense almost 2 years

    For example

    Given an array:

    array = [[:a,:b],[:a,:c],[:c,:b]]
    

    Return the following hash:

    hash = { :a => [:b,:c] , :c => [:b] }
    

    hash = Hash[array] overwrites previous associations, producing:

    hash = { :a => :c , :c => :b }
    
    • tadman
      tadman over 12 years
      Is this some flavour of homework? How do you intend to do this? Hashes aren't delimited with square brackets.
    • mu is too short
      mu is too short over 12 years
      Have you tried using group_by?
    • smallsense
      smallsense over 12 years
      Sorry - you're right about hashes, I'm still learning. This isn't a flavour of homework, though it is a part of a solution I'm working on for a wider problem. Our lecturer recommended SO for programming help.
  • Phrogz
    Phrogz over 9 years
    It's not the syntax that is wrong in 1.8.7, it's just that Hash.[] before 1.9 only accepted a flat list of k/v arguments, not a single array of key/value pairs. For 1.8 you had to do: Hash[ *my_array.flatten ]
  • mkataja
    mkataja almost 9 years
    Doesn't answer the question.
  • Abdo
    Abdo almost 9 years
    @mkataja care to elaborate?
  • mkataja
    mkataja almost 9 years
    @Abdo sure. As far as I can see, the asker's specific problem was that "Hash[array] overwrites previous associations". Instead of :a => :c he wanted :a => [:b,:c]. Using to_h or Hash[] doesn't get you that.