Harry Sohie
Harry Sohie

Reputation: 23

Get unique properties from array of hashes in ruby

Given an array of hashes, I want to create a method that returns a hash where the keys are the unique values of the hashes in the array.

For example, I'd like to take

[
  {foo: 'bar', baz: 'bang'},
  {foo: 'rab', baz: 'bang'},
  {foo: 'bizz', baz: 'buzz'}
]

and return

{
  foo: ['bar', 'rab', 'bizz'],
  baz: ['bang', 'buzz']
}

I am currently accomplishing this using:

def my_fantastic_method(data)
  response_data = { foo: [], baz: []}
  data.each { |data| 
    data.attributes.each { |key, value| 
      response_data[key.to_sym] << value 
    } 
  }
  response_data.each { |key, value| response_data[key] = response_data[key].uniq }
  response_data
end

Is there a more elegant way of doing this? Thanks!

Upvotes: 1

Views: 518

Answers (5)

Mark Thomas
Mark Thomas

Reputation: 37507

Here are a couple of one-liners. (I'm pretty sure @eiko was being facetious, but I'm proving him correct)

This one reads well and is easy to follow (caveat: requires Ruby 2.4+ for transform_values):

array.flat_map(&:entries).group_by(&:first).transform_values{|v| v.map(&:last).uniq}

Here's another, using the block form of merge to specify an alternate merge method, which in this case is combining the values into a uniq array:

array.reduce{|h, el| h.merge(el){|k, old, new| ([old]+[new]).flatten.uniq}}

Upvotes: 2

Cary Swoveland
Cary Swoveland

Reputation: 110675

If, as in the example, all hashes have the same keys, you could do as follows.

arr = [{ foo: 'bar', baz: 'bang' },
       { foo: 'rab', baz: 'bang' },
       { foo: 'bizz', baz: 'buzz' }]

keys = arr.first.keys
keys.zip(arr.map { |h| h.values_at(*keys) }.transpose.map(&:uniq)).to_h
  #=> {:foo=>["bar", "rab", "bizz"], :baz=>["bang", "buzz"]}

The steps are as follows.

keys = arr.first.keys
  #=> [:foo, :baz]
a = arr.map { |h| h.values_at(*keys) }
  #=> [["bar", "bang"], ["rab", "bang"], ["bizz", "buzz"]]
b = a.transpose
  #=> [["bar", "rab", "bizz"], ["bang", "bang", "buzz"]]
c = b.map(&:uniq)
  #=> [["bar", "rab", "bizz"], ["bang", "buzz"]]
d = c.to_h
  #=> <array of hashes shown above>

Upvotes: 0

Radix
Radix

Reputation: 2747

Try this:

array.flat_map(&:entries)
  .group_by(&:first)
  .map{|k,v| {k => v.map(&:last)} }

OR

a.inject({}) {|old_h, new_h|
  new_h.each_pair {|k, v|
    old_h.key?(k) ? old_h[k] << v : old_h[k]=[v]};
    old_h}

Upvotes: 1

thesecretmaster
thesecretmaster

Reputation: 1984

You already have a pretty good answer, but I felt golfy and so here is a shorter one:

def the_combiner(a)
  hash = {}
  a.map(&:to_a).flatten(1).each do |k,v|
    hash[k] ||= []
    hash[k].push(v)
  end
  hash
end

Upvotes: 1

Tom Lord
Tom Lord

Reputation: 28285

Your current approach is already pretty good; I don't see much room for improvement. I would write it like this:

def my_fantastic_method(data_list)
  data_list.each_with_object(Hash.new { |h, k| h[k] = Set.new }) do |data, result| 
    data.attributes.each do |key, value|
      result[key.to_sym] << value
    end
  end
end
  • By setting a default value on each hash value, I have eliminated the need to explicitly declare foo: [], bar: [].
  • By using each_with_object, I have eliminated the need to declare a local variable and explicitly return it at the end.
  • By using Set, there is no need to call uniq on the final result. This requires less code, and is more performant. However, if you really want the final result to be a mapping to Arrays rather than Sets, then you would need to call to_a on each value at the end of the method.
  • I have used different variable names for data_list and data. Call these whatever you like, but it's typically considered bad practice to shadow outer variables.

Upvotes: 6

Related Questions