Group Array of Hashes by Key

Group array of hashes by key

Use group_by.

array.group_by{|h| h[:user]}.values

Ruby group array of hashes by numeric key

Injecting into a default hash:

arr = [{1=>6}, {1=>5}, {4=>1}]
arr.inject(Hash.new{|h,k| h[k]=[]}){|h, e| h[e.first[0]] << e.first[1]; h}

# => {1=>[6, 5], 4=>[1]}

Or, as suggested in the comments:

arr.each.with_object(Hash.new{|h, k| h[k] = []}){|e, h| h[e.first[0]] << e.first[1]}

# => {1=>[6, 5], 4=>[1]}

Grouping an array of hashes

Refactored solution

Here's a longer but possibly better solution, with 3 helper methods :

class Array
# Remove key from array of hashes
def remove_key(key)
map do |h|
h.delete(key)
h
end
end

# Group hashes by values for given key, sort by value,
# remove key from hashes, apply optional block to array of hashes.
def to_grouped_hash(key)
by_key = group_by { |h| h[key] }.sort_by { |value, _| value }
by_key.map do |value, hashes|
hashes_without = hashes.remove_key(key)
new_hashes = block_given? ? yield(hashes_without) : hashes_without
[value, new_hashes]
end.to_h
end

# Convert array to indexed hash
def to_indexed_hash(first = 0)
map.with_index(first) { |v, i| [i, v] }.to_h
end
end

Your script can then be written as :

data.to_grouped_hash(:year) do |year_data|
year_data.to_grouped_hash(:month) do |month_data|
month_data.to_indexed_hash(1)
end
end

It doesn't need Rails or Activesupport, and returns :

{2015=>
{12=>
{1=>{:account_id=>145, :balance=>4}, 2=>{:account_id=>163, :balance=>11}}},
2016=>
{11=>
{1=>{:account_id=>134, :balance=>3}, 2=>{:account_id=>135, :balance=>0}},
12=>{1=>{:account_id=>133, :price=>5}}}}

Refinements could be use to avoid polluting the Array class.

Original one-liner

# require 'active_support/core_ext/hash'
# ^ uncomment in plain ruby script.

data.group_by{|h| h[:year]}
.map{|year, year_data|
[
year,
year_data.group_by{|month_data| month_data[:month]}.map{|month, vs| [month, vs.map.with_index(1){|v,i| [i,v.except(:year, :month)]}.to_h]}
.to_h]
}.to_h

It uses Hash#except from ActiveSupport.

It outputs :

{
2016 => {
12 => {
1 => {
:account_id => 133,
:price => 5
}
},
11 => {
1 => {
:account_id => 134,
:balance => 3
},
2 => {
:account_id => 135,
:balance => 0
}
}
},
2015 => {
12 => {
1 => {
:account_id => 145,
:balance => 4
},
2 => {
:account_id => 163,
:balance => 11
}
}
}
}

How can I group this array of hashes?

The &:age means that the group_by method should call the age method on the array items to get the group by data. This age method is not defined on the items which are Hashes in your case.

This should work:

array.group_by { |d| d[:age] }

Ruby - Group an array of hashes by key

Here is my try

#!/usr/bin/env ruby

data = [
{"name"=> "Amy", "win" => 1, "defeat" => 0},
{"name"=> "Amy", "win" => 1, "defeat" => 3},
{"name"=> "Carl", "win" => 0, "defeat" => 1},
{"name"=> "Carl", "win" => 2, "defeat" => 1}
]

merged_hash = data.group_by { |h| h['name'] }.map do |_,val|
val.inject do |h1,h2|
h1.merge(h2) do |k,o,n|
k == 'name' ? o : o + n
end
end
end

merged_hash
# => [{"name"=>"Amy", "win"=>2, "defeat"=>3},
# {"name"=>"Carl", "win"=>2, "defeat"=>2}]

Answer to the edited post :-

#!/usr/bin/env ruby

data = [
{id: 1, name: "Amy", win: 1, defeat: 0},
{id: 1, name: "Amy", win: 1, defeat: 3},
{id: 2, name: "Carl", win: 0, defeat: 1},
{id: 2, name: "Carl", win: 2, defeat: 1}
]

merged_hash = data.group_by { |h| h.values_at(:name, :id) }.map do |_,val|
val.inject do |h1,h2|
h1.merge(h2) do |k,o,n|
%i(id name).include?(k) ? o : o + n
end
end
end

merged_hash
# => [{:id=>1, :name=>"Amy", :win=>2, :defeat=>3},
# {:id=>2, :name=>"Carl", :win=>2, :defeat=>2}]

Using group_by but return an array of hashes

The reason the grouped values are Rails objects (your models) is due to the fact that you also start with these objects. You can use the attributes method to retrieve the attributes of a model instance as a hash.

The following achieves the result you want:

City.all.group_by { |city| city.state.name }
.transform_values { |cities| cities.map(&:attributes) }

If you only want specific attributes, use slice instead:

City.all.group_by { |city| city.state.name }
.transform_values { |cities| cities.map { |city| city.slice(:id, :name) } }

Note that slice will return an ActiveSupport::HashWithIndifferentAccess instance. Which mostly can be used in the same manner as a normal hash, but returns the same value for both hash[:name] and hash['name']. If you rather use a normal hash append a to_hash call after the slice call.

Ruby group hashes based on matching keys and store the value of non matching keys in an array

You can try grouping by key and value and then map the revenue values:

foos
.group_by { |e| e.values_at(:key, :value) }
.map do |(key, value), values|
{ key: key, value: value, revenue: values.map { |e| e[:revenue] } }
end
# [{:key=>"Foo", :value=>1, :revenue=>[2, 4]}, {:key=>"Bar", :value=>2, :revenue=>[7]}, {:key=>"bar", :value=>2, :revenue=>[9]}, {:key=>"Zampa", :value=>4, :revenue=>[9]}]

Group Array of Hashes by Key followed by values

input.reduce { |e, acc| acc.merge(e) { |_, e1, e2| [*e2, *e1] } }
#⇒ {:name=>["sam", "max", "joe"],
# :animal=>["dog", "cat", "snake"],
# :gender=>["male", "female", "male"]}

Group and display occurrences of array of hashes based on some key in ruby

frequency_distribution

You could define Enumerable#frequency_distribution by using each_with_object with a Hash and a default occurence value of 0 :

module Enumerable
def frequency_distribution
each_with_object(Hash.new(0)) { |element, count| count[element] += 1 }
end
end

It works this way :

require 'pp'
data = [
{"a"=>"first", "b"=>[{"lt"=>7, "lg"=>8}]},
{"a"=>"first", "b"=>[{"lt"=>7, "lg"=>8}]},
{"a"=>"second", "b"=>[{"lt"=>7, "lg"=>8}]},
{"a"=>"third", "b"=>[{"lt"=>9, "lg"=>10}]}
]

pp data.frequency_distribution
# {{"a"=>"first", "b"=>[{"lt"=>7, "lg"=>8}]}=>2,
# {"a"=>"second", "b"=>[{"lt"=>7, "lg"=>8}]}=>1,
# {"a"=>"third", "b"=>[{"lt"=>9, "lg"=>10}]}=>1}

If you don't want to monkey-patch Enumerable :

def frequency_distribution(array)
array.each_with_object(Hash.new(0)) { |e, h| h[e] += 1 }
end

frequency_distribution(data)
# {{"a"=>"first", "b"=>[{"lt"=>7, "lg"=>8}]}=>2,
# {"a"=>"second", "b"=>[{"lt"=>7, "lg"=>8}]}=>1,
# {"a"=>"third", "b"=>[{"lt"=>9, "lg"=>10}]}=>1}

Note that the output is one single hash. Keys are hashes and values are integers. I cannot think of any good reason to convert it to an array of 1-pair hashes. Lookup would be much slower and less readable.

count_by

For a more generic method, you could define Enumerable#count_by, with the same syntax as group_by or sort_by :

module Enumerable
def count_by(&block)
each_with_object(Hash.new(0)) { |element, count| count[block.call(element)] += 1 }
end
end

require 'pp'
data = [
{"a"=>"first", "b"=>[{"lt"=>7, "lg"=>8}]},
{"a"=>"first", "b"=>[{"lt"=>7, "lg"=>8}]},
{"a"=>"second", "b"=>[{"lt"=>7, "lg"=>8}]},
{"a"=>"third", "b"=>[{"lt"=>9, "lg"=>10}]}
]

pp data.count_by(&:itself)
# {{"a"=>"first", "b"=>[{"lt"=>7, "lg"=>8}]}=>2,
# {"a"=>"second", "b"=>[{"lt"=>7, "lg"=>8}]}=>1,
# {"a"=>"third", "b"=>[{"lt"=>9, "lg"=>10}]}=>1}

pp data.count_by(&:keys)
#=> {["a", "b"]=>4}

pp data.count_by{|key, value| key["a"]}
#=> {"first"=>2, "second"=>1, "third"=>1}


Related Topics



Leave a reply



Submit