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
Differencebetween a Process' Pid, Ppid, Uid, Euid, Gid and Egid
Ruby on Rails, Two Models in One Form
Reading the First Line of a File in Ruby
Which Ruby Gems Support the Facebook API
How to Create a Rest API for a Ruby Application
How to Check If a Variable Is an Instance of a Class
"Rmagick" Gem Installation Issue
Rails Routing (Root :To => ...)
What Is Double Method in Rspec For
Find Records with Datetime That Match Today's Date - Ruby on Rails
What Alternatives to Irb Are There